Agent Pipelines
Four shapes for composing agent steps, and when each one is the right one
Four shapes
Most multi-step agent work is one of these. Picking the wrong one is where systems get slow, expensive, or unreliable.
1. Chain. Each step feeds the next
extract → normalise → validate → write
Use when each step genuinely depends on the last. Simple, debuggable, no coordination.
The failure mode is error propagation: a bad extraction produces a confidently normalised, validated, written wrong answer. Put the cheapest possible check between steps.
data = extract(doc)
assert data.get("total") is not None, "extraction produced no total"
clean = normalise(data)
2. Fan-out / fan-in, independent work, then synthesis
┌── analyse security ──┐
plan ───┼── analyse perf ──┼── synthesise
└── analyse tests ──┘
Use when subtasks are genuinely independent. The synthesis step is the one that needs capability, run the branches on a cheap tier and the join on a good one.
The failure mode is contradictory branches. Two agents reach opposite conclusions from different slices. The synthesiser must be told to surface disagreement rather than average it:
If the reports conflict, say so explicitly and state which one has stronger evidence. Do not merge contradictory claims into a single confident answer.
3. Loop with a critic, generate, check, revise
generate → validate ──fail──> revise ──┐
│ │
pass └──> (max 3 attempts)
↓
done
The most reliable shape for anything that must be correct. The critic should be a program, not a model, wherever possible. A test suite, a linter, a schema validator. A deterministic critic cannot be talked around.
for attempt in range(3):
code = generate(spec, feedback=last_error)
ok, last_error = run_tests(code)
if ok:
return code
raise PipelineError(f"failed after 3 attempts: {last_error}")
Always bound the loop. An unbounded revise cycle is how a $0.40 task becomes a $40 one.
4. Router, classify first, then dispatch
┌── simple → fast tier
request ── classify ── standard → balanced tier
└── hard → frontier tier
The highest-leverage cost pattern there is. A fast-tier classifier costs almost nothing and routes most traffic away from the expensive tier.
tier = classify(request) # haiku: ~$0.0002 model = TIERS[tier] return run(model, request)
The failure mode is misrouting downward. A hard request sent to the fast tier returns a confident wrong answer. Build in escalation:
result = run(TIERS[tier], request)
if result.low_confidence or validation_failed(result):
result = run(TIERS["hard"], request) # escalate, don't retry the same tier
Choosing
| If… | Use |
|---|---|
| Steps depend on each other | Chain |
| Steps are independent | Fan-out / fan-in |
| Correctness is checkable by a program | Loop with a critic |
| Requests vary a lot in difficulty | Router |
| Two of the above apply | Compose them, router in front of a critic loop is very common |
Rules that apply to all four
Bound everything. Max attempts, max tokens, max wall-clock. An agent pipeline without limits will eventually find a way to spend all your money on one request.
Log the boundaries. Store the input and output of every step. When the pipeline produces something wrong, you need to know which step went wrong, and reconstructing that from the final output is impossible.
Fail loudly. A step that cannot do its job should stop the pipeline, not pass through a plausible default. Silent degradation in an agent pipeline produces output that looks fine and is not.
Make the critic deterministic where you can. A test suite, a schema, an exit code. Model-as-critic is a fallback, not a first choice.
See also: Single Agent or Subagents? · Chain Commands with Agents · Build a Golden Task Set