Everyone building multi-agent systems in 2026 is still writing straight lines. Step one, then step two, then step three - each one waiting on the last. Here's why that's slow, and how to fix it.

The problem nobody checks
You built a multi-step agent. It works. It's also slow.
You assume the model is the bottleneck. It isn't.
The bottleneck is the shape you drew. A chain - step 1 waits for step 2, step 2 waits for step 3 - forces sequential execution even when half those steps have nothing to do with each other.
"Summarize this document, then check the weather" is two independent jobs wearing a trench coat as one workflow. The weather task doesn't need the summary. It never did. But if you wrote it as a chain, it waits anyway.
That wasted wait, multiplied across dozens of steps, is where most of your runtime disappears.
Chapter 1 - Loops vs graphs
A loop is one unit of self-improvement:
1try something → check the result → adjust → try again
That's the atom. One agent, one metric, cycling until it converges.
Loops have a known failure mode: they optimize exactly what you measure and nothing else. A support bot tuned to close tickets fast will close tickets fast - while satisfaction quietly craters. The loop can't see outside its own metric. That's Goodhart's Law showing up in your agent architecture.
A graph fixes this by design. Instead of one loop chasing one number, you build a network of loops that watch and correct each other. Node A's output feeds Node B. Node C runs independently and checks both. No single metric drives the whole system - the structure does.

For agent systems this means one concrete shift: stop writing one agent that does everything top to bottom. Design the shape of the work first - what has to happen before what, what can run at the same time, what actually needs to wait.
Chapter 2 - Nodes, edges, and the test that separates them
A graph has exactly two components:
Node - one unit of work. One agent, one job, one input, one output.
Edge - a real dependency. Node B's input requires Node A's output.
The mistake almost everyone makes: treating "and then" as an edge by default.
1"Read this codebase and then write the changelog"2"Fetch the pricing page and then summarize competitor features"
Ask one question for every "and then" in your workflow:
Does the next step actually read the previous step's output?
If yes → real edge. Keep the sequential order. If no → no edge. The wait is wasted. Run them in parallel.
If no data crosses the boundary between two tasks, they're independent — and every independent pair you're running sequentially is runtime you're throwing away for free.
Here's the test applied in code:
1from dataclasses import dataclass23@dataclass4class TaskNode:5 id: str6 prompt: str7 depends_on: list[str] # IDs of nodes this one actually needs89def has_real_edge(node_a: TaskNode, node_b: TaskNode) -> bool:10 """11 The core graph engineering test:12 does node_b's prompt actually require node_a's output?13 """14 return node_a.id in node_b.depends_on1516# Example: most "chains" collapse into 2-3 real dependency groups17nodes = [18 TaskNode("audit_routes", "List all API route files", []),19 TaskNode("check_auth", "Check auth middleware coverage", []),20 TaskNode("fetch_weather", "Get today's weather", []),21 TaskNode("summarize", "Summarize route + auth findings",22 depends_on=["audit_routes", "check_auth"]),23]2425# audit_routes, check_auth, fetch_weather have NO edges between them26# They run in parallel. Only "summarize" has real edges -- it waits.
Your current "do A, then B, then C" agent is technically already a graph. It's just the worst possible one - a single chain where if C stalls, nothing downstream ever runs.
Chapter 3 - Building your first graph

Requirements:
- Claude Code (recent version with Dynamic Workflows support).
- Max, Team, or Enterprise plan - workflows on by default. On Pro, enable manually.
Open a real repository. Not a toy example - the payoff only shows up at real scale.
The prompt that kicks off your first graph:
1Create a workflow to audit every route file in this codebase.23For each route file, check independently:4- authentication middleware present5- input validation on all params6- rate limiting configured7- error handling doesn't leak stack traces89Run these checks in parallel across all route files —10they don't depend on each other.1112After all files are checked, produce one consolidated13report grouped by severity: critical, warning, info.1415The consolidation step should wait for all checks to complete.16Everything before it should not.
Notice the structure embedded in the prompt itself: parallel work explicitly called out, the one real dependency (consolidation waiting on all checks) explicitly named. You're not hoping the agent infers the graph - you're describing it.
What happens under the hood - a simplified version of the orchestration:
1import asyncio2from anthropic import Anthropic34client = Anthropic()56async def audit_route_file(filepath: str) -> dict:7 """One node. Runs independently of every other route file."""8 response = await client.messages.create(9 model="claude-sonnet-5",10 max_tokens=1000,11 messages=[{12 "role": "user",13 "content": f"""Audit this route file for:14 - auth middleware, input validation,15 rate limiting, error handling1617 File: {filepath}1819 Return JSON: {{"file": "", "issues": [], "severity": ""}}"""20 }]21 )22 return {"file": filepath, "result": response.content[0].text}2324async def consolidate(results: list[dict]) -> str:25 """The one real edge -- waits for every audit node to finish."""26 response = await client.messages.create(27 model="claude-opus-4-8",28 max_tokens=2000,29 messages=[{30 "role": "user",31 "content": f"""Consolidate these {len(results)} route audits32 into one report grouped by severity:3334 {results}"""35 }]36 )37 return response.content[0].text3839async def run_graph(route_files: list[str]):40 # Fan out -- all independent nodes run concurrently41 audit_tasks = [audit_route_file(f) for f in route_files]42 results = await asyncio.gather(*audit_tasks)4344 # Fan in -- the one node with a real dependency45 report = await consolidate(results)46 return report4748# 40 route files, one prompt, one parallel pass49results = asyncio.run(run_graph([50 f"routes/{f}.py" for f in ["auth", "users", "billing", "orders"]51 # ...36 more
40 sequential API calls at ~8 seconds each is over 5 minutes. The same 40 calls fanned out in parallel: under 15 seconds, bounded by your slowest single file, not the sum of all of them.
Chapter 4 - Where graphs actually break
Graph engineering fails in three predictable places. Know them before you hit them.
Context collapse. Fan out 1,000 nodes and try to feed all 1,000 outputs into one consolidation step, and you'll blow past any context window before synthesis even starts. Fix: layer your fan-in. Group nodes into batches of 20-50, summarize each batch, then consolidate the summaries - not the raw outputs.
1async def layered_consolidate(results: list[dict], batch_size: int = 30):2 """Fan-in in layers -- never synthesize raw output at scale."""3 batches = [results[i:i+batch_size]4 for i in range(0, len(results), batch_size)]56 batch_summaries = await asyncio.gather(*[7 summarize_batch(batch) for batch in batches8 ])910 # Final consolidation works on summaries, not 1,000 raw results11 return await consolidate(batch_summaries)
False independence. You'll assume two nodes are independent because their prompts don't reference each other - but they both write to the same file, or hit the same rate-limited API. That's a hidden edge. Fix: audit for shared resources, not just shared data. Two nodes with a write conflict need an edge even with zero data dependency.
Silent node failure. In a chain, one failure stops everything - annoying but obvious. In a graph, one failed node among 200 can vanish into a report that looks complete. Fix: every fan-in step checks node count against expected count before synthesizing, and flags gaps explicitly instead of quietly working with partial data.
1async def safe_consolidate(results: list[dict], expected_count: int):2 if len(results) < expected_count:3 missing = expected_count - len(results)4 print(f"WARNING: {missing} nodes failed silently. "5 f"Report will be incomplete.")6 return await consolidate(results)
Chapter 5 - Scaling to a real fleet

Once the pattern works at 40 nodes, scaling to hundreds is a config change, not a redesign - provided you built the graph correctly from Chapter 2 onward.
The full production shape:
1 Orchestrator2 |3 +--------+-------+-------+--------+4 v v v v v5 Node 1 Node 2 Node 3 ... Node N6 (parallel, no edges between any of them)7 | | | |8 +--------+-------+-------+-------+9 v10 Batch Summary <- layered fan-in11 (groups of 30)12 v13 Final Report <- the one true edge
The orchestrator's only job: decompose the task into nodes, identify real edges, and dispatch. It does no work itself - it draws the graph.
1async def orchestrate(task: str, resources: list[str]):2 """3 The orchestrator node -- decomposes, doesn't execute.4 """5 plan = await client.messages.create(6 model="claude-opus-4-8",7 max_tokens=2000,8 messages=[{9 "role": "user",10 "content": f"""Task: {task}11 Resources available: {resources}1213 Decompose into a graph:14 - List each independent node (no shared edges)15 - List any real dependencies between nodes16 - Group nodes into fan-in batches if count > 501718 Return JSON with: nodes, edges, batch_groups"""19 }]20 )2122 graph = parse_plan(plan.content[0].text)2324 # Execute independent nodes in parallel25 node_results = await asyncio.gather(*[26 execute_node(n) for n in graph["nodes"] if not n["depends_on"]27 ])2829 # Then execute dependent nodes, respecting real edges only30 final = await execute_dependent_chain(graph["edges"], node_results)3132 return final
This is the actual shift graph engineering represents: you stop being the person who writes every step, and become the person who designs the dependency structure. The agents fill in the nodes. You own the edges.
What changes when you think in graphs instead of lines
A linear agent with 40 steps has 40 points of sequential failure and 40x the latency of its slowest single step.
A graph with the same 40 units of work has as many points of parallel failure as you have real dependencies - usually 3 to 5 in most workflows - and latency bounded by your slowest layer, not your total step count.
That's not a marginal speedup. It's the difference between a workflow that takes 5 minutes and one that takes 15 seconds, running the exact same underlying work.
The model was never the bottleneck. The line you drew was.
This is a technical breakdown of multi-agent orchestration patterns as of July 2026. Code examples are illustrative - adapt error handling, rate limiting, and retry logic to your production environment before deploying at scale.
Thank you for reading.





