🏛️ Separate the org chart from the work
Two graphs coexist in the same agent system, and they are not the same graph. The org graph is the lasting structure: long-lived agents with named roles —researcher, writer, validator, publisher—each owning a domain, with accumulated memory and stable edges that change only when you redeploys the system.
Think of it as the company org chart, except each box is an agent running its own loop. The org graph answers “who exists and what each one does” — not “what are we doing now?” That second question belongs to another graph, which you’ll see in the next topic.
🆕 New here? Four words before you continue
- Org graph: the stable structure of long-running agents—the org chart. It changes only when you redeploy the system.
- Work graph: the graph built for a specific task. It’s created, run, and discarded.
- Redeploy: redeploy — publish a new version of the system. This is the only point when the org graph changes shape.
- Deterministic: same input, always the same output. The opposite of "it depends on the model's luck that day."
What to look at: the dotted divider in the middle doesn’t separate two systems—it separates two perspectives of the same system. Notice that “researcher” and “validator” appear on both sides: they’re the same agents. On the left, you see all the possible relationships between them (the org chart). On the right, you see only the narrow path that today’s task actually followed—a subset assembled on the fly.
✓ It's an org graph when
- ✓The agent has a fixed name and role; it doesn’t change from task to task
- ✓It accumulates memory — it knows things it learned in past runs
- ✓The edge represents "can ask for"—a long-term relationship
✗ It’s not an org graph when
- ✗You're designing the steps of a single task — that's a work graph
- ✗The "structure" changes every time a different request comes in
- ✗There is no accumulated memory, only the context of that run
Key concepts
The org chart, stable
Agent with a named role
Learns between executions
Doesn't change at runtime
📋 Build today's request work graph
O work graph is ephemeral: assembled for a specific task and discarded afterward. It changes with each run—today it calls for a researcher and validator, tomorrow just the writer, then all four in a row. There’s no “the” work graph for the system; there’s a new work graph for every incoming request.
💡 The most common confusion in a messy system
Confusing the two graphs is the most common cause of a messy system. Some people recreate the entire organization with every request — instantiate a new agent, redefine roles, redesign the whole structure each time a request comes in. Others do the opposite: they lock the task into the org chart, treating every variation in a request as if it needs to become a new, permanent agent.
- •Recreating the organization for every request is expensive and slow, and you lose the accumulated memory that made the agent good.
- •Hardwiring the task into the org chart = the system becomes rigid, unable to handle a request that doesn’t follow the planned script.
Request arrives
A user or external system triggers a task. The work graph doesn’t exist yet.
The router chooses who participates
Look at the available org graph and select a subset of agents for this specific task.
A work graph runs and finishes
The task’s fine-grained path runs from start to finish, delivers the result, and is discarded.
Org graph remains intact
The agents that participated continue to exist, ready for the next work graph—with the memory they have accumulated.
Key concepts
Ephemeral, one per request
It starts, runs, disappears
Uses only part of the org graph
Confusing it with the org chart
🛡️ Apply zone defense
Zone defense is the pattern that makes an org graph worthwhile: each agent owns a domain, with persistent context. The security agent handles authentication, permissions, and audit logs. The data agent handles schemas, migrations, and query standards. The API agent handles endpoints, rate limits, and contracts. The frontend agent handles components, state, and interface standards.
Each one runs its own loop within the zone. A request from one zone to another goes through the work graph—never directly. No agent leaks into another’s context. That’s what makes a long-lived agent accumulate real value: a security agent that has already reviewed 300 changes implicitly knows that code’s threat model, without needing anything re-explained with each request.
📊 The four classic zones
- Security — authentication, permissions, audit logs
- Data — schema, migrations, query patterns
- API — endpoints, rate limits, contracts
- Frontend — components, state, interface patterns
🆕 New here? Zone defense
Zone defense: a term borrowed from sports—each player covers an area of the field, not a specific person on the opposing team. In graph engineering, each agent covers a domain of the system, not a specific task. It defends the entire zone, task after task.
✓ Zone defense works when
- ✓The domain has a clear boundary (data isn’t API, and API isn’t frontend)
- ✓Handoffs between zones become an explicit edge in the work graph
- ✓Each agent accumulates real context (decision history, not just instructions)
✗ When zone defense breaks
- ✗An agent reads or writes directly in another zone’s context (leakage)
- ✗The zones overlap—two agents “owning” the same domain
- ✗A zone is left ownerless and becomes a no-man’s-land
Key concepts
One owner per domain
Doesn't reset with every task
Handoff = explicit edge
Accumulated experience counts
🔀 Create a real fan-out and fan-in
Fan-out is to open a task into N branches that run at the same time. Fan-in is to bring the N results back together into one. It’s something a loop simply doesn’t do, since a loop is sequential by nature—it processes one thing at a time, always.
The join is where the danger lies. A faulty merge silently swallows a branch, and the result looks complete even when something is missing. That’s why every join needs to check cardinality: did I receive the N I dispatched?
🆕 New here? Fan-out, fan-in, cardinality
- Fan-out: one node dispatches work to several parallel branches at the same time.
- Fan-in: the parallel branches converge back into a single join node.
- Cardinality: the count—how many items went out versus how many came back. If you dispatched 5 and received 4, the cardinality doesn’t match.
What to look at: four pulsing branches arrive cleanly at the join; the fifth, marked with a red X, never arrives. If the join doesn’t count how many branches it dispatched versus how many it received, it will accept “I received 4 of 5” as if it were a complete result. This is the merge that swallows a source — the result looks ready, and no one notices a piece is missing.
⚠️ Warning: join without a cardinality check
Whenever you write a fan-in, explicitly ask “how many did I expect?” before moving on. If the answer doesn’t match what arrived, stop and handle the case—don’t let the work graph continue as if it were complete. A branch that fails silently is worse than one that fails loudly.
Key concepts
A node becomes N branches
N branches become one node
Did I receive the N I dispatched?
A merge that silently swallows
🚦 Route by condition
With conditional routing, the edge chooses the destination based on the previous node's result. It's the mechanism that lets the work graph “decide” without an agent having to interpret everything again at each step. The classic example is customer support.
Low complexity goes to the canned response base (FAQ). Medium complexity goes to the AI agent. High or risky complexity escalates directly to a human. There is also a "not resolved" edge (returns to the next level) and a learning edge (returns to the classifier with feedback).
🎯 Why the learning edge matters more than it seems
The first three edges (primary, if-unresolved, escalation) make the system work today. Only the green edge makes the system work better tomorrow — because it’s the only one that returns information to the node that decides. Without it, the classifier keeps making the same mistake forever; with it, every human correction becomes a signal for the next routing decision.
Key concepts
An edge chooses based on the result
High risk goes straight to a human
Move up to the next level
The only edge that closes the cycle
⚙️ Let Code Decide What’s Predictable
Model routing what one if resolve is expensive, slow, and nondeterministic.
The division that works: code controls predictable routing; the model handles steps that require interpretation
or judgment. Every edge you can turn into a written rule is an edge that stops costing tokens
and varying between runs.
✓ Leave it to the code
- ✓"If complexity == low, route to the FAQ" — an explicit rule with no ambiguity
- ✓"If cardinality doesn't match, stop and alert" — a mechanical check
- ✓"If category == payment, route to the finance agent" — fixed mapping
✗ Leave it to the model
- ✗"Does the message's tone suggest frustration?" — requires interpretation, not a rule
- ✗"Does this answer actually answer the question?" — a quality judgment
- ✗"What's the real intent behind this ambiguous sentence?" — interpretation
🧪 Practical example: support routing graph in JSON
Objective: write the support routing graph with explicit conditional edges, escalation and unresolved edges, a learning edge — and a join that checks cardinality. Run it in the browser console (F12 → Console) or in Node.
// 1) O grafo de roteamento: cada aresta tem uma condicao explicita
const roteador = {
"classificacao": [
{ "para": "faq", "quando": "complexidade == 'baixa'" },
{ "para": "agente_ia", "quando": "complexidade == 'media'" },
{ "para": "humano", "quando": "complexidade == 'alta' || risco == 'alto'" }
],
"faq": [ { "para": "agente_ia", "quando": "resolvido == false" } ],
"agente_ia": [ { "para": "humano", "quando": "resolvido == false" } ],
"humano": [ { "para": "classificacao", "quando": "feedback_dado == true", "tipo": "aprendizado" } ]
};
// 2) Juncao com checagem de cardinalidade (fan-in de N tickets em lote)
function fanIn(despachados, recebidos) {
if (recebidos.length !== despachados.length) {
throw new Error(`cardinalidade nao bate: recebi ${recebidos.length} de ${despachados.length}`);
}
return recebidos;
}
// 3) Rota um ticket de exemplo
function roteia(no, ctx) {
const arestas = roteador[no] || [];
for (const a of arestas) {
// avaliacao simplificada da condicao 'quando' contra o contexto
if (eval(a.quando.replace(/complexidade/g, `'${ctx.complexidade}'`).replace(/risco/g, `'${ctx.risco}'`).replace(/resolvido/g, ctx.resolvido).replace(/feedback_dado/g, ctx.feedback_dado))) {
return a.para;
}
}
return null; // nenhuma condicao casou -- pedido some, ver "como verificar"
}
console.log(roteia("classificacao", { complexidade: "media", risco: "baixo" })); // "agente_ia"
How to verify:
- 1. Every edge leaving a decision node (
classificacao) hasquando— no edge “changes” without a written condition. - 2. The conditions cover all the cases — test
roteia("classificacao", <contexto sem match>); if it returnsnull, there’s an entry that doesn’t match any condition, and the request silently disappears. - 3. There is a path to a human from any node — follow the edges of
faqeagente_iaand confirm that both reachhumanowhenresolvido == false.
Now swap it for yours: replace the nodes with
<etapas do seu suporte> is the conditions under which
<campos reais do seu ticket>. Run the test from item 2 before deploying to production.
Quick check (doesn’t block anything): a fan-in received 3 responses from a fan-out that dispatched 5 branches and continued as if it were complete. What was the error?
Key concepts
What is a rule becomes an `if`
What requires interpretation
Every input has a destination
A written rule costs no tokens
📌 Module Summary
Next Module:
4.3 — Failures, harness, and observability: a merge that swallows sources, routing that doesn’t converge, and a monitor showing who ran what.