An AI agent can take an action. An AI employee needs to know what happens next.
Most AI agents look something like this:
Think → Act → Observe → Repeat
That's fine for short-lived tasks.
But an AI employee needs to work across hours, days, and weeks.
It needs to remember:
- What happened
- Who owns the work
- What is waiting
- What changed
- What should happen next
- When it should wake up
- When a human needs to approve something
That's where graph engineering becomes interesting.
This is the architecture behind
Roster:
software that can own work the way an employee does, not just fire off a single tool call.
Events wake someone up. A graph holds state, ownership, and history.
The agent reasons, acts, writes the result back, then sleeps until the next event.
For Roster, the loop looks like this:
Event
↓
Graph
↓
Agent
↓
Action
↓
Graph Update
↓
Sleep
↓
Wake Again
Let's build a tiny version.
Table of Contents
- 1. Model the Work
- 2. Build the Graph
- 3. Add Events
- 4. Build the Agent Loop
- 5. Add Scheduling
- 6. Build a Tiny AI Employee
- 7. Put It Together
- 8. The Bigger Idea
1. Model the Work
Imagine an AI employee called Maya.
Her job is simple:
Follow up with sales leads.
Her world contains:
Maya
↓ owns
Lead
↓ belongs_to
Company
↓ contacted
Email
↓ replied_to
Customer
We don't need a massive graph database.
We just need nodes and relationships.
2. Build the Graph
Here's a minimal TypeScript graph:
type Node = {
id: string;
type: string;
data: Record<string, unknown>;
};
type Edge = {
from: string;
to: string;
type: string;
};
class Graph {
nodes = new Map<string, Node>();
edges: Edge[] = [];
addNode(node: Node) {
this.nodes.set(node.id, node);
}
connect(from: string, type: string, to: string) {
this.edges.push({ from, type, to });
}
neighbors(id: string) {
return this.edges
.filter((edge) => edge.from === id)
.map((edge) => ({
relationship: edge.type,
node: this.nodes.get(edge.to),
}));
}
}
Now create Maya and a lead:
const graph = new Graph();
graph.addNode({
id: "maya",
type: "employee",
data: {
name: "Maya",
role: "sales",
},
});
graph.addNode({
id: "lead-123",
type: "lead",
data: {
company: "Acme",
status: "qualified",
},
});
graph.connect("maya", "owns", "lead-123");
Our graph now knows:
Maya ──owns──→ Lead #123
That's already more useful than two disconnected database records.
3. Add Events
Employees shouldn't constantly run.
Something should wake them up.
For example, Sarah replies to an email:
const event = {
id: "event-1",
type: "email.replied",
data: {
leadId: "lead-123",
message: "Sounds interesting. Follow up next Tuesday.",
},
};
Now we can find the employee responsible for that lead:
function findOwner(event: typeof event) {
const leadId = event.data.leadId;
return graph.edges.find(
(edge) => edge.to === leadId && edge.type === "owns"
)?.from;
}
const employeeId = findOwner(event);
console.log(employeeId);
// maya
We just answered:
Who should wake up?
The flow becomes:
Email Reply
↓
Event
↓
Find Lead
↓
Find Owner
↓
Wake Maya
4. Build the Agent Loop
Now we give Maya an actual agent loop.
async function runEmployee(employeeId: string, event: any) {
const employee = graph.nodes.get(employeeId);
const context = {
employee,
event,
relationships: graph.neighbors(employeeId),
};
const decision = await agent(context);
const result = await execute(decision);
recordResult(employeeId, decision, result);
}
The important part is the sequence:
Wake
↓
Read Graph
↓
Reason
↓
Act
↓
Record
The graph gives the agent persistent context.
5. Add Scheduling
Now imagine Sarah says:
Follow up with me next Tuesday.
Maya shouldn't stay running until Tuesday.
She schedules a future event.
type Job = {
employeeId: string;
runAt: Date;
event: any;
};
const jobs: Job[] = [];
function schedule(job: Job) {
jobs.push(job);
}
Maya can schedule her next action:
schedule({
employeeId: "maya",
runAt: new Date("2026-09-01T09:00:00Z"),
event: {
id: "followup-1",
type: "followup.due",
data: {
leadId: "lead-123",
},
},
});
Then Maya sleeps.
When the time arrives:
async function processJobs() {
const now = new Date();
for (const job of jobs) {
if (job.runAt <= now) {
await runEmployee(job.employeeId, job.event);
}
}
}
Now we have two ways to wake an employee:
Customer Reply ──────┐
│
Approval Granted ────┼──→ Wake Employee
│
Schedule Due ────────┘
6. Build a Tiny AI Employee
Now let's connect an LLM.
The agent gets the relevant graph context and decides what to do.
async function agent(context: any) {
const prompt = `
You are Maya, a sales employee.
Your job is to follow up with leads.
Event:
${JSON.stringify(context.event)}
Graph:
${JSON.stringify(context.relationships)}
Decide the next action.
Return JSON:
{
"action": "...",
"reason": "...",
"runAt": "..."
}
`;
return llm.generateObject(prompt);
}
For Sarah's message, Maya might return:
{
"action": "schedule_followup",
"reason": "Sarah requested a follow-up next Tuesday.",
"runAt": "2026-09-01T09:00:00Z"
}
Then we execute it:
async function execute(decision: any) {
switch (decision.action) {
case "schedule_followup":
schedule({
employeeId: "maya",
runAt: new Date(decision.runAt),
event: {
id: crypto.randomUUID(),
type: "followup.due",
data: decision,
},
});
return {
success: true,
};
case "send_email":
return sendEmail(decision);
default:
throw new Error(`Unknown action: ${decision.action}`);
}
}
Finally, record what happened:
function recordResult(employeeId: string, decision: any, result: any) {
graph.addNode({
id: crypto.randomUUID(),
type: "agent_action",
data: {
employeeId,
decision,
result,
createdAt: new Date(),
},
});
}
Now the employee has memory.
Not necessarily memory as a giant conversation transcript.
Memory as state and relationships.
7. Put It Together
Let's walk through the entire workflow.
Sarah replies:
Sounds interesting. Follow up next Tuesday.
- Email arrives
Gmail
↓
email.replied
- Roster finds the relevant lead
Email
↓
related_to
↓
Lead #123
- Roster finds the owner
Lead #123
↓
owned_by
↓
Maya
- Maya wakes up
Maya
↓
Read Graph
↓
Understand Context
- Maya reasons
Sarah wants a follow-up next Tuesday.
- Maya schedules it
Task
↓
scheduled_for
↓
Tuesday 9:00 AM
Maya sleeps 💤
Tuesday arrives
Scheduler
↓
followup.due
↓
Wake Maya
- Maya reads the graph
Maya
↓
Lead #123
↓
Sarah
↓
Previous Conversation
- Maya sends the email
Maya
↓
send_email()
↓
Sarah
- Graph updates
Task #123
status = completed
Email #43
status = sent
Lead #123
last_contacted = today
Then:
Maya
↓
Sleep
That's a tiny AI employee.
8. The Bigger Idea
The architecture is surprisingly simple:
┌─────────────┐
│ Events │
└──────┬──────┘
↓
┌─────────────┐
│ Graph │
│ │
│ State │
│ Relations │
│ History │
└──────┬──────┘
↓
┌─────────────┐
│ AI Employee │
└──────┬──────┘
↓
┌───────┴───────┐
↓ ↓
Tools Scheduler
│ │
└───────┬───────┘
↓
Events
│
└────→ Graph
The important part is the final arrow:
Agent
↓
Action
↓
Event
↓
Graph
↓
Next Decision
The agent changes the world.
The graph records the change.
The next time the employee wakes up, it doesn't start over.
It continues.
The Takeaway
I think the future of AI employees looks less like:
Prompt → LLM → Tool
and more like:
World
↓
Graph
↓
Agent
↓
Action
↓
Event
↓
Graph
The LLM provides reasoning.
The tools provide capabilities.
The scheduler provides time.
Events provide wake-ups.
The graph provides continuity.
That's the interesting part.
We're not just building agents that can do things.
We're building software that can own work.
That's what Roster is for.
Try Roster
If the same follow-ups, handoffs, and waiting loops keep eating your week, give them to an AI employee.
This article was originally published by DEV Community and written by Bobby Hall Jr .
Read original article on DEV Community