Reading Archive
← 返回文章列表
seeco @seeconvm · 2026-09-07

Graph Engineering: Stop Chaining Your Agents

Almost every multi step agent I have ever opened is a queue. Step one, step two, step three, each one politely waiting for the last one to finish. And once you look closely, roughly half of those steps never had anything to wait for.

They don't route. They don't split. They don't run anything side by side. They just stand in line, one head, one context, one thing at a time, until the window fills up and the agent quietly forgets what it was doing in the first place.

Here is the part nobody spells out for you. A prompt is a sentence. A loop is a cycle. A harness is the floor your agent stands on. But the shape of the work itself, what runs before what, what can run at the same time, what genuinely has to wait for everything else, that shape is a graph. Nodes do the thinking. Edges carry the results.

Claude Code shipped the tooling for building these graphs directly: dynamic workflows. Claude writes a plain JavaScript orchestration script, then spawns a coordinated fleet of subagents to execute it. The coordination itself costs zero model tokens, because it is code, not another conversation.

This is the 14 step roadmap I use to turn a single file agent into a graph that fans out across a fleet, checks its own findings, and lands on a result one agent could never hold.

AT A GLANCE

  1. Nodes are jobs. Edges are what flows.

  2. Your linear script is already a graph, just a bad one.

  3. Every node gets a contract.

  4. Every edge gets a data contract.

  5. Fan out with parallel().

  6. Fan in at a barrier, and only when you must.

  7. The diamond: split, work, merge.

  8. Route the edge at runtime with a conditional.

  9. Put a verifier on the edge.

  10. Isolate nodes so one failure stays local.

  11. Add a cycle, but make it converge.

  12. Tier your models across the nodes.

  13. Topology is your cost and your latency.

  14. Let Claude draw the graph for you.

01. Nodes and Edges, or Why "And Then" Is Not a Dependency

A graph has exactly two pieces, and getting them straight clears up most of the confusion.

A node is a unit of work. One agent, one bounded job, one input in, one output out.

An edge is a dependency. It says this node's output feeds that node's input. That is the whole definition.

The mistake almost everyone makes is treating "and then" as an edge. "Summarize this file and then tell me the weather" has no edge in it anywhere. The weather does not consume the summary. Those are two disconnected nodes that a linear script chained together for no reason at all.

The question to ask. For every and then in your agent: does the next step actually read the previous step's output? If it doesn't, there is no edge there, and the wait is pure waste.

plaintext Draw it as boxes and arrows. A box is one agent() call. An arrow is a variable that leaves one call's return and enters another call's prompt. If you cannot draw the arrow, if no variable crosses, those two boxes are independent. That independence is the thing you are going to spend the rest of this article exploiting.

02. Your Linear Script Is a Degenerate Graph

When you write an agent as "do A, then B, then C, then D," you already drew a graph. You drew the worst one available: a single unbranching chain where every node has exactly one edge in and one edge out.

It runs. It also runs slowly, and it breaks badly, because a chain has no redundancy. If C stalls, D never happens, and everything A produced is stuck upstream with nowhere to go.

Article image

The first real skill here is redrawing the chain. Take your linear agent, walk every arrow, and ask the question from step 01. In practice you will find two or three arrows that carry no data at all. They only exist because that is the order you happened to type things in.

Cut those arrows and the chain collapses sideways into something much wider: a handful of independent nodes that can all run at once, feeding into a single node that needs all of them.

03. 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, exactly one job.

The input is whatever that node reads, passed in explicitly. Never assumed from some shared window it happens to be sitting in. The output is a defined shape, ideally validated, so the next node can consume it without guessing.

Article image

In a workflow you enforce this with a schema. When you hand Claude an agent() call with a JSON schema attached, the subagent it spawns is forced to return validated structured data. Validation happens down at the tool call layer, so Claude retries on a mismatch instead of handing you free text you have to parse and pray over.

json // A node with a real contract: bounded in, validated out, one job. const FINDING = { type: 'object', additionalProperties: false, properties: { title: { type: 'string' }, url: { type: 'string' }, impact: { type: 'string', enum: ['high', 'medium', 'low'] }, }, required: ['title', 'url', 'impact'], };

const result = await agent(source.prompt, { label: research:${source.key}, schema: FINDING, // forces validated structured output agentType: 'general-purpose', }); // result is now a shape the next node can trust, not free text.

That is the entire difference between a node Claude can wire into a graph and a node that only works when a human reads its output.

04. The Edge Is a Data Contract Too

An edge is not "B comes after A." It is a promise about what crosses: A produces this shape, B was built to consume this shape. Name your edges by their data instead of their order and two things get much easier.

You can instantly tell whether the edge is even real, meaning does data actually move across it. And you can swap the node on either end without touching the rest of the graph, as long as the shape holds.

In practice the edge lives in plain JavaScript. The reduce step between a fan out and a synthesis, flatten, dedupe, filter, is just code operating on the shapes your nodes returned.

No agent needed. This is one of the quiet wins of thinking in graphs: a huge amount of what people burn model tokens on is really just an edge, and edges are free.

json // Tempting: spawn an agent to "combine the results. "Don't. // If combining means flatten and dedupe, that is flatMap plus a Set. // Deterministic, instant, zero tokens. const flat = collected.flatMap((c) => c.items); const clean = [...new Map(flat.map((i) => [i.url, i])). values()];

Save agents for judgment. Not for plumbing. A graph where every edge is an agent is a graph paying rent on its own wiring.

05. Fan Out With parallel()

This is the move that pays for everything else. When you have N independent nodes, N sources to check, N files to review, N routes to audit, you do not chain them.

You tell Claude to fan them out and run them at once. In a workflow that is parallel(): Claude takes an array of thunks, spawns one subagent per thunk, runs them concurrently, and hands you back the array of results.

Two details make it robust. First, parallel() is a barrier, so it waits for every thunk before it returns and the next stage always sees the complete set. Second, a thunk that throws resolves to null instead of rejecting the whole batch, so one flaky agent cannot sink the run.

Always .filter(Boolean) on the way out. Concurrency is capped around your core count and the overflow queues up, so you can hand it a hundred thunks and they will all finish, just a handful at a time.

json phase('Research');

// Nine sources, nine agents, all at once. const raw = await parallel( SOURCES.map((s) => () => agent(s.prompt, { label: research:${s.key}, phase: 'Research', schema: ITEM_SCHEMA, // each node returns validated JSON agentType: 'general-purpose', }), ), );

const collected = raw.filter(Boolean); // drop nulls from failed agents

The fan out lives in code Claude wrote, not in a model conversation. Claude's own context never holds nine sources at once. Each subagent carries its own, and only the final answer comes back. That is what lets a workflow scale to dozens or hundreds of subagents without drowning the session, and the orchestration layer costs nothing, because it is not another turn of Claude thinking.

06. Fan In at a Barrier

A fan out is only worth anything if something gathers it. The fan in is the node where your edges converge, where one agent or one piece of code sees all the upstream results at the same time and does something that genuinely requires the whole set: dedupe across sources, rank by impact, exit early if everything came back empty.

That is the one place a barrier earns its wall clock cost.

The rule that keeps graphs fast: use a barrier only when a stage truly needs every prior result together. Deduping across all sources qualifies.

json // The edge: plain JS, no agent, zero tokens. const flat = collected.flatMap((c) => c.items); log(Collected ${flat.length} items);

phase('Curate'); // The barrier node: needs the WHOLE set to dedupe and rank. const curated = await agent( Dedupe and rank these by impact:\n${JSON.stringify (flat)}, { phase: 'Curate', schema: CURATED_SCHEMA }, );

Just flattening a list is not a barrier, it is an edge, do it inline. The smell test is brutally simple: if you wrote parallel, then a transform, then parallel again, and that middle transform has no cross item dependency, you should have used a pipeline and skipped the barrier entirely.

07. The Diamond: Split, Work, Merge

Put a fan out and a fan in together and you get the workhorse topology of every serious agent graph: the diamond.

One node splits the job. Many nodes do the work in parallel. One node merges. That is the shape behind a market scan, a dependency audit, a code review, a research report. Swap the sources and the prompts and the same skeleton adapts to all of them.

The canonical form is worth memorizing: fan out, reduce, synthesize. Fan out to gather breadth. Reduce with plain code to compress it. Synthesize with a final agent to write the answer.

Once you can see the diamond, you stop asking "how do I make my agent do more steps" and start asking "where is the split, where is the merge." That second question is the one that actually scales.

08. Route the Edge at Runtime With a Conditional

Not every graph is fixed. Sometimes which edge you take depends on what a node found. A router node inspects a result and decides which downstream path fires: classify the ticket, then branch to the right handler. Check the diff size, then either do a quick review or spin up a full audit.

In a workflow this is just a JavaScript if or switch on a node's validated output, because your control flow lives in code.

json // Router node: an agent classifies, code picks the edge. const { severity } = await agent(Classify this diff's risk:\n${diff}, { schema: { type: 'object', properties: { severity: { type: 'string', enum: ['low', 'high'] } }, required: ['severity'], }, });

let review; if (severity === 'high') { // heavy path: full parallel audit review = await parallel(FILES.map((f) => () => agent (Audit ${f}))); } else { // light path: one quick pass review = await agent(Quick review of ${diff}); }

This is where determinism turns into a feature instead of a limitation. The router's decision can be Claude powered, a subagent classifies it. The routing is code Claude wrote, so it runs the same way every single time for the same classification.

You get Claude's judgment at the node and a script's reliability at the edge. There is no emergent "Claude decided to skip the audit today" surprise, because skipping would have to be written into the graph, and it isn't.

09. Put a Verifier on the Edge

The real leverage of a graph is not that you get more agents. It is the structure you can wrap around them to produce confidence.

A verifier node sits on the edge, before a result is allowed downstream, and its only job is to try to kill the finding. If it survives, it passes. If it doesn't, it never reaches your answer.

Three patterns worth keeping in your hands:

  • Adversarial verify. For each finding, spawn N independent skeptics prompted to refute it, and keep it only if a majority survive.

  • Perspective diverse verify. Give each verifier its own lens, correctness, security, does it reproduce, because diversity catches failure modes that N identical checks never will.

  • Judge panel. Generate N attempts from different angles, score them with parallel judges, synthesize from the winner while grafting in the best pieces of the runners up.

This is exactly the pattern that let a real team port the Bun runtime with adversarial code review baked straight into the loop.

10. Isolate Nodes So One Failure Stays Local

In a chain, a failure cascades. C dies, D never runs, the whole thing halts. In a graph, failure should be contained to its node.

Part of that is already true: a thunk that throws inside parallel() resolves to null, so eight good agents still return while the one bad one drops out. Your .filter(Boolean) is the containment. Design every fan in to tolerate missing inputs instead of assuming a full set.

The subtler failure is nodes stepping on each other. When agents write files in parallel, they collide.

The fix is isolation: worktree. Each agent runs in its own git worktree, does its work in a sandbox, and merges cleanly at the end. Reach for it only when nodes genuinely write in parallel. It is the seatbelt for one specific topology, not a default tax on every run.

11. Add a Cycle, But Make It Converge

Sometimes you have no idea how big the job is until you are inside it. Discovery of unknown size. A bug sweep where finding one bug reveals three more. That calls for a cycle, a controlled edge back to an earlier node.

The danger is obvious. A cycle that never converges is an infinite loop that spawns agents until your budget is gone.

Article image

The pattern that converges is loop until dry: keep spawning finders until K consecutive rounds surface nothing new, then stop. And here is the detail that makes or breaks it, the one almost everybody gets wrong the first time: what you dedupe against.

Dedupe against everything seen, not just against confirmed results. Otherwise rejected findings come back every round, the loop never runs dry, and you have built a machine that pays to rediscover the same dead ends forever.

json const seen = new Set(); const confirmed = []; 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, closer to dry dry = 0; fresh.forEach((b) => seen.add(key(b))); // dedupe vs SEEN, not confirmed

// diverse lens verify: every fresh finding earns its place const judged = await parallel(fresh.map((b) => () => parallel(['correctness', 'security', 'repro'].map ((lens) => () => agent(Judge &quot;${b.desc}&quot; via ${lens}. Real?, { schema: VERDICT }))) .then((v) => ({ b, real: v.filter(Boolean).filter ((x) => x.real).length >= 2 })) ));

confirmed.push(...judged.filter((v) => v.real).map((v) => v.b)); }

12. Tier the Models Across Your Nodes

Not every node needs your best model. A graph makes this obvious in a way a single agent never does: some nodes are bounded and repetitive, extract this field, classify this ticket. Others carry the actual judgment, synthesize the report, adjudicate the finding.

Run the boring nodes on a cheaper model and spend your expensive tokens where judgment actually lives.

In a workflow, every subagent Claude spawns inherits your session model unless the script overrides it, so by default a big run bills entirely at your session tier. The model option on a single agent() call tells Claude to route just that node somewhere else.

Check /model before a large run, then have Claude route the fan out's repetitive nodes down to a cheaper model while keeping the merge node up top. This is the lever that turns a token hungry graph into an economical one without touching its shape at all.

13. Topology Is Your Cost and Latency

The shape of the graph is not cosmetic. It is the single biggest lever you have on wall clock time. The choice that trips up basically everyone is parallel() versus pipeline().

A parallel() barrier makes everything wait for the slowest node before the next stage can start. A pipeline() streams each item through all the stages independently, with no barrier, so item A can be sitting in stage 3 while item B is still in stage 1. Fast items finish early instead of idling behind slow ones.

Default to pipeline(). Reach for a barrier only when a stage truly needs every prior result at once: a cross set dedupe, an early exit on the total, a prompt that compares one finding against all the others.

"It's cleaner code" and "the stages feel separate" are not reasons. Barrier latency is real, measurable, wasted time. Separate is not the same thing as synchronized.

Shape

Use it when

What it costs you

Chain

Each step really reads the last step's output

Slowest possible run, one stall kills everything

parallel() fan out

N independent jobs, and the next stage needs them all

Everyone waits for the slowest node

pipeline()

N independent jobs flowing through the same stages

Almost nothing, this is your default

Diamond

Breadth first, then one merged answer

One barrier, at the merge, where it is earned

Conditional

The path depends on what a node found

A classification call before the branch

Cycle

You don't know how big the job is

Runaway spend if it never converges

14. Let Claude Draw the Graph

The final move is to stop drawing graphs by hand for jobs you cannot plan in advance.

With dynamic workflows you describe the objective and Claude writes the orchestration script itself: decomposing the task, choosing the fan out, spawning a coordinated fleet of subagents, and synthesizing the result. You end up with a graph tailored to this run instead of a fixed one you hoped would fit.

There are three ways in.

Say the word workflow in your prompt and Claude writes one for the task.

Run a saved or bundled one. /deep-research is a real graph shipping in production right now: scope, parallel search, fetch, adversarial verify, synthesize. That is the exact skeleton from this article.

Turn on ultracode and Claude plans a workflow for every substantial task in the session. When a run comes out good, press s to save its script into .claude/workflows/. Now it is version controlled, re-runnable by name, and a graph anyone who clones the repo can launch.

plaintext > Run a workflow to audit every route under src/routes/ for missing auth. Spawn one agent per route file, then verify each finding before reporting.

• Claude wrote an orchestration script · launching in background... /workflows > auth-audit · running ✓ Scope 1/1 2.1k tok · 4s ✓ Fan-out 18/18 one agent per route file ○ Verify 11/18 3-vote skeptics per finding... ○ Synthesize 0/1 waiting on verify session stays responsive, keep working while the fleet runs

Six Graphs Worth Building This Week

  • A security sweep across every route. One subagent per route file, each hunting missing auth checks, then a verifier pass that confirms every finding before it reaches the report. Breadth no single context could hold.

  • A cited report with /deep-research. Claude splits your question into distinct angles, runs the searches in parallel, dedupes the sources, then adversarially verifies every claim with three vote skeptics before it writes a word.

  • A module ported file by file. The Bun ceiling, scaled down to your repo. Claude fans translation out across files, runs the test suite as a gate on each one, and loops the failures back, with adversarial review catching what a single pass would have shipped broken.

  • An adversarial review of a diff. Claude routes on diff size: a small change gets one quick pass, a large one triggers a full parallel audit with reviewers on distinct lenses, then a judge panel synthesizes the verdict.

  • An ecosystem scan on a schedule. Save it once, re-run it forever. Claude checks many sources in parallel, releases, blogs, discussion, ranks by impact at a barrier, and writes the digest. Version controlled in .claude/workflows/, launchable by name.

  • A discovery job of unknown size. You have no idea how many bugs are in there. Claude runs finders in parallel, dedupes each new find against everything seen, verifies the survivors, and keeps looping until two rounds turn up nothing new.

Why This Actually Matters

Every one of these steps points at the same underlying thing. Your agent's ceiling is almost never the model. It is the shape of the work you handed it.

A chain forces one context to hold everything, one failure to halt everything, and every fast step to wait behind the slowest one. A graph spreads the context across a fleet, contains failure at the node, and buys you structure you can wrap confidence around: schemas at the nodes, verifiers on the edges, routing that runs the same way every time.

And the orchestration layer is code. That is the part people underestimate. Coordination between eighteen agents costs zero model tokens, because a script is not a conversation.

What Actually Matters Here

If you only take three things from this: cut the arrows that carry no data, default to pipeline() instead of a barrier, and dedupe against everything you have seen rather than everything you confirmed.

Those three alone will make your agents faster, cheaper, and far harder to break than adding another step to the queue ever will.

The linear agent was never the ceiling. It was just the first shape everybody reaches for, because it matches how we type. One line, one head, one thing at a time.

A prompter asks a question. An architect draws a graph.

So which is your agent right now, a line or a graph?

If you made it this far, there is a lot more to this topic than fourteen steps and a few code blocks.

@seeconvm is where I keep breaking it down.

More coming soon.

查看原文 ↗