Skip to content

The platform is live.

Take a look
The Lab
Architecture essay · 12 min read

Graph engineering: draw the shape of the work

Why a single agent runs in a straight line, and the graph I draw instead: fan out, verify, converge.

Claude Code workflowsSubagent fleetJS orchestrationAdversarial verify

Drawn from how I run engineering on this platform. The audits that gate every change here are agent graphs, not single agents: a reviewer per lens fans out, each finding is checked by an adversarial pass, and the result lands in a dated log. The patterns below are the ones I reach for. The concrete instrument is Claude Code's dynamic workflows; the shape underneath them is fifty years old.

01The failure

A single agent runs in a straight line

Most multi-step agents are a straight line. Step one, step two, step three, each waiting politely for the last to finish before it starts. It runs. It also runs slowly, and it forgets: one head, one context window, one thing at a time, until the window fills and the agent loses the thread of what it was doing.

The tell is that half those steps never needed to wait. They do not route, they do not branch, they do not run at the same time. They queue, because a line is the shape that matches how we type, not the shape of the work.

The work has a shape of its own. What runs before what, what can run together, what has to wait for everything else. That shape is a graph. The move this piece is about is drawing it on purpose, then running it wide.

Topology · the same work, four shapes

The line

One agent, one line.

Four steps, each waiting for the last. It is correct, and it is slow: some of these edges carry no data at all. They are just the order you happened to type things in.

02The shape

The shape is fifty years old. The author is new.

Be honest about what is new here, because a senior reader will check. Shaping work as a graph of jobs joined by their data dependencies is not an invention. It is dataflow programming from 1974, MapReduce from 2004, and Pregel's round-based graph convergence from 2010. Every DAG workflow engine since, Airflow, Dagster, Prefect, Temporal, is a version of it.

The agent-specific version was named before the current tooling too. Anthropic's "Building Effective Agents" laid out routing, parallelization, orchestrator-workers, and evaluator-optimizer a full year before a model could write the orchestration for you. "Graph engineering" is a newer, louder label, and a contested one. Treat it as a lens, not a discovery.

What is genuinely new is narrow and worth stating precisely. A model now writes the orchestration itself, as a plain, rerunnable script, and the coordination logic in that script is ordinary code. That one shift is what makes the graph cheap to draw and cheaper to change. The rest of this piece is the shapes worth drawing, and the honest cost of running them.

1974
Dataflow, Kahn process networksA program as a static graph of units joined by data-carrying channels. The topology is fixed by data dependency, not by the order you wrote it.
2004
MapReduceMap the same transform across every shard, reduce the results into one output. Fan out, fan in, written once and reused across jobs.
2010
PregelGraph computation in synchronized rounds, terminating when every vertex votes to halt. Loop until convergence, at cluster scale.
2014
Airflow and the DAG enginesThe orchestration graph as code. Dagster, Prefect, and Temporal each drew the same shape a different way after it.
2024
Building Effective AgentsAnthropic named the agent version: routing, parallelization, orchestrator-workers, evaluator-optimizer. A year before a model could write the script.
03The unit

Nodes are jobs. Edges are what flows.

A graph has two things, and getting them straight removes most of the confusion. A node is one unit of work: one agent, one bounded job, one input in and one output out. An edge is a dependency: this node's output feeds that node's input. That is all an edge is.

The mistake is treating "and then" as an edge. "Summarize the file, and then tell me the weather" has no edge between the two, because the weather does not read the summary. Those are two independent nodes a linear script chains for no reason. The edge exists only when data actually moves across it.

So for every "and then" in an agent, ask one question: does the next step read the last step's output? If not, there is no edge, and the wait is wasted. That question is the whole method, and the rest is learning to exploit the independence it finds.

Does data cross?
Summarize the file
Tell me the weather
verdictNo edge

The weather does not read the summary. These are two independent nodes a line chains for nothing. Fan them out and they finish at the same time.

04The contract

Give every node a contract

A node you cannot reason about is a node you cannot parallelize. The fix is a contract: bounded input, bounded output, one job. The input is passed in explicitly, never assumed from a shared window. The output is a defined shape, validated, so the next node consumes it without guessing.

In a workflow that contract is a schema. Hand a node a JSON schema and the subagent is forced to return validated structured data. The validation happens at the tool-call layer, so a mismatch is retried, not handed back as free text you have to parse and hope over. That is the line between a node you can wire into a graph and one that only works when a human reads its output.

One node · a contract, not a hopeagent()
job"research one source"

One bounded job. A node that does three things cannot be reasoned about, reused, or safely run next to others.

inputsource.prompt

Passed in explicitly. A node never reaches into a shared window for its input; everything it needs crosses an edge.

schemaITEM // { title, url, impact }

The output contract. The subagent is forced to return this shape, and a mismatch is retried at the tool-call layer, not handed back as free text.

output{ title, url, impact: "high" }

A validated shape the next node consumes without guessing or parsing. This is what makes the edge trustworthy.

model"cheaper for a bounded job"

Set per node. Repetitive extraction runs on a small model; the judgment nodes keep the expensive one.

isolation"worktree" // only if it writes

A node that edits files in parallel gets its own git worktree so two nodes cannot collide. The seatbelt for the one topology that needs it, not a default tax.

Why

Bounded in, validated out, one job. That is what makes a node safe to run next to a hundred others, and what makes its edge trustworthy.

05The workhorse

Fan out, then fan in: the diamond

This is the move that pays for everything. When N nodes are independent, N sources to check, N files to review, N routes to audit, you do not chain them. You fan them out and run them at once, then gather the results. One node splits, many nodes work in parallel, one node merges. That split-work-merge shape is the diamond, and it is the workhorse of every serious agent graph.

Two details make it hold. The gather is a barrier: it waits for every branch before the next stage sees the set, which is what a real merge needs. And a branch that fails resolves to nothing rather than sinking the batch, so one flaky node cannot take down the run. You drop the empty results and keep going.

The merge in between is usually not a node at all. Flatten, dedupe, filter: that is plain code operating on the shapes the nodes returned. A large share of what people spend model tokens on is really an edge, and edges are free. Spend agents on judgment, not on plumbing.

orchestration script
// Nine independent sources, nine agents, all at once.
const found = (await parallel(
  SOURCES.map((s) => () => agent(s.prompt, { schema: ITEM })),
)).filter(Boolean);            // drop the branches that failed

// The merge is an edge, not a node: plain code, zero model tokens.
const items = found.flatMap((r) => r.items);
The fan-out is code the model wrote, not a conversation it holds. Each subagent carries its own context; only the results come back.
06The router

Route the edge at runtime

Not every graph is fixed. Sometimes the edge to take depends on what a node found. A router inspects a result and decides which downstream path fires: classify the input, then branch to the right handler; check the size of a change, then either do a quick pass or a full audit.

The classification can be a model's judgment, but the routing is code. So it runs the same way every time for the same classification. You get the model's judgment at the node and the script's determinism at the edge. There is no emergent "it decided to skip the audit", because a skip would have to be written into the graph, and it is not.

orchestration script
// A node classifies; code picks the edge.
const { risk } = await agent(`Classify this diff:\n${diff}`, { schema: RISK });

const review = risk === "high"
  ? await parallel(files.map((f) => () => agent(`Audit ${f}`)))  // full fan-out
  : await agent(`Quick review of ${diff}`);                      // one pass
Judgment at the node, determinism at the edge.
Input

A one-line copy fix in the footer

classified as

diff is tiny, low risk

routes toQUICK PASS

One node reads it and moves on. Spinning up a fleet for a typo pays rent on wiring you do not need.

07The verifier

Verify before it counts

The real gain from a graph is not more agents. It is the structure you can wrap around them to earn confidence. A verifier sits on the edge before a result is allowed downstream, and its one job is to try to kill the finding. If it survives, it passes. If not, it never reaches the answer.

Three shapes are worth having in hand. Adversarial verify: for each finding, spawn independent skeptics prompted to refute it, and keep it only if a majority survive. Perspective-diverse verify: give each verifier a different lens, correctness, security, does-it-reproduce, because diversity catches failure modes that identical checks never will. Judge panel: generate several attempts from different angles, score them with parallel judges, and build the answer from the winner while grafting the best of the rest.

This is the same separation of powers I reach for in a grounded RAG system, where an agent proposes and a verifier and a human decide. The research workflow bundled in Claude Code is that shape run wide: it fans out searches, cross-checks the sources, and marks each claim verified, refuted, or unverified, so a claim it could not check is flagged as unchecked rather than quietly passed. A verifier that says "I could not confirm this" is worth more than one that rubber-stamps.

A finding, three skeptics, each told to refute

This route is missing its auth check

correctnesssurvives

Confirmed: the gate is absent before the DB write.

securitysurvives

A non-member can reach it. Real.

reproducesurvives

Reproduced against a foreign workspace.

3 of 3 survivedSURVIVESMajority held. It passes the edge.
08The cycle

A loop that converges, not one that runs forever

Sometimes you do not know how big the job is until you are in it: a bug sweep where finding one bug reveals three more. That needs a cycle, an edge back to an earlier node. The danger is obvious. A cycle that does not converge is an infinite loop that spawns agents until the budget is gone.

The shape that converges is loop-until-dry: keep spawning finders until a few rounds in a row surface nothing new, then stop. The one detail that makes or breaks it, and the mistake almost everyone makes the first time, is what you dedupe against. Dedupe against everything seen, not just against what survived. Otherwise rejected findings reappear every round, the loop never runs dry, and you have built a machine that pays to rediscover the same dead ends.

orchestration script
const seen = new Set(); const kept = []; let dry = 0;
while (dry < 2) {                                    // stop after 2 empty rounds
  const found = (await parallel(FINDERS.map((f) => () =>
    agent(f.prompt, { schema: BUGS })))).filter(Boolean).flatMap((r) => r.bugs);
  const fresh = found.filter((b) => !seen.has(key(b)));
  if (!fresh.length) { dry++; continue; }           // nothing new, toward dry
  dry = 0;
  fresh.forEach((b) => seen.add(key(b)));            // dedupe vs SEEN, not kept
  kept.push(...(await verify(fresh)));
}
Dedupe against seen, not against what survived. That one line is the difference between converging and paying to rediscover dead ends.
Loop until dry · round 1
5found
5fresh
5seen
0dry rounds

First pass. Five findings, all new. Add them to seen, verify, keep the survivors.

09The lever

The shape is the cost, and sometimes the cost is a straight line

Topology is not cosmetic. It is the single biggest lever on both wall-clock time and money. A barrier makes everything wait for the slowest node before the next stage starts. A pipeline streams each item through all its stages independently, so a fast item finishes instead of idling behind a slow one. Default to the pipeline; reach for a barrier only when a stage truly needs the whole set at once.

The other lever is the model. Not every node needs the strongest one. Run the bounded, repetitive nodes, extract this field, classify this input, on a cheap model, and keep the expensive tokens for the judgment nodes that adjudicate a finding or write the synthesis. A graph makes that split obvious in a way a single agent never does.

And the honest part. A graph is not always the answer. Tightly coupled work, where every step needs the last step's full context, belongs in one agent; splitting it turns the phases into a game of telephone that degrades at every handoff. A fleet also costs several times to an order of magnitude more tokens than one conversation doing the same task: Anthropic has reported its own multi-agent research system running roughly fifteen times the tokens of a single chat. Fan-out buys thoroughness, not correctness. When the work is a straight line, the straight line is the right architecture.

agent()one node
Spawns one subagent for one bounded job. With a schema, it returns validated structured data instead of free text.
parallel()fan out at a barrier
Runs an array of nodes at once and waits for all of them. A failed branch resolves to nothing instead of sinking the batch.
pipeline()stream, no barrier
Runs each item through every stage independently, so a fast item finishes instead of idling behind a slow one. The default.
a schemathe edge contract
Forces a node's output into a validated shape the next node can trust, retried at the tool-call layer on a mismatch.
plain codethe free edges
Flatten, dedupe, filter, branch, loop. The coordination runs without calling the model, so the wiring is free even when the nodes are not.
worktreeparallel writes
Each node that edits files gets its own git checkout so two cannot collide. Only for the topology that writes in parallel.
model per nodethe cost lever
Cheap models for bounded nodes, the strong one for judgment. Set per agent() call, not per run.
10The point

Stop asking the agent to do more. Ask the graph to do it wider.

The linear agent was never the ceiling. It was the first shape, the one everyone reaches for because it matches how we type. Once you can see the nodes and the edges, the question changes. You stop asking one agent to do more steps and start asking where the work splits, where it merges, and where a verifier has to stand before a result counts.

The coordination is code, so drawing the graph is cheap and changing it is cheaper. The fleet still costs real tokens, so you draw it only where the work is actually wide. That is the whole discipline: fan out where the work is independent, gate the edges where confidence matters, tier the models where judgment does not, and keep a straight line where a straight line was always fine.

Building something a single agent cannot hold?

Drawing the graph is the work I do on real systems. If you want that judgment pointed at yours, let us talk.

The Lab, by email

New tools and notes, when they land.

I publish when there's something worth your time, not on a schedule. Drop your email and I'll send new tools and notes from the bench.

No noise. Unsubscribe in one click. See the privacy policy.