Five LEGO bricks — chaining, routing, parallelization, orchestrator-workers, evaluator-optimizer — that snap together into every production LLM pipeline, with the cost and failure mode of each baked in.
A workflow is an LLM pipeline whose control flow you can draw as a flowchart before you run it. Anthropic's "Building Effective Agents" distilled the design space down to five composable patterns: prompt chaining (sequential steps with gates between them), routing (classify, then dispatch to a specialist), parallelization (fan out into independent sections or repeated votes), orchestrator-workers (a planner LLM decomposes a task at runtime and farms out subtasks), and evaluator-optimizer (generate, critique, refine in a loop). These are not competing frameworks — they are LEGO bricks. A real product is usually a chain whose third step is a router that hands off to a worker pool that internally runs an evaluator loop. The whole point of learning the five is to reach for the smallest pattern that solves the problem, because each step up the ladder buys flexibility by spending tokens, latency, and your ability to debug a failure at 3am.
The words first.
Step by step.
Remember this: start with the smallest pattern that works and step up the ladder only when the task forces you to.
The cleanest mental test Anthropic gives is: can you draw the flowchart before execution? If yes, you have a workflow — LLM calls and tools orchestrated through predefined code paths. If no — if the path depends on what the model discovers as it runs — you have an agent. This isn't a binary, it's a dial of autonomy. The five patterns live on the low-to-mid part of that dial. They matter because every step toward autonomy trades away three things interviewers will press you on: cost predictability (a workflow has a fixed token budget; an agent can spike 4–15x on exploration loops), auditability (every workflow path is testable pre-deploy, which SOC 2 / GDPR / financial regulators love), and debuggability (a workflow failure has a known path; an agent failure requires reconstructing a trajectory). The senior instinct is to push as far down the spectrum as the task allows.
Sequential steps with checkpoints. Use when a task splits into fixed, ordered subtasks (e.g. outline → draft → polish).
A chain decomposes a task into a fixed sequence of LLM calls, each consuming the previous output. The canonical example: write an outline → check the outline meets criteria → write the document from the outline. You use it when the task is genuinely a pipeline of well-defined, verifiable sub-steps, and where doing it in one giant prompt would overload the model and tank accuracy. Decomposition trades a little latency (more round-trips) for a lot of accuracy, because each call has a narrower, cleaner job.
The load-bearing word is gate. Between steps you insert a programmatic check — schema validation, a regex, a relevance test, or a small cheap LLM call — that either passes the output forward or short-circuits the chain. A gate that catches a malformed outline before you spend tokens writing 2,000 words from it is the entire ROI of the pattern. Without gates, a chain just compounds the first step's error through every subsequent step. Cost and complexity are both low; this is your default for multi-stage reasoning.
Routing puts a classifier at the front door. One upstream call (or a fine-tuned small model, or even a regex tier) labels the input — pricing, refund, returns — and code dispatches it to the handler purpose-built for that class. The win is that each specialist handler gets a tight, focused prompt instead of one mega-prompt trying to cover every case, which raises accuracy and lets you route easy classes to a cheap model (Haiku) and hard classes to a frontier model. The upstream classification is a single cheap call, so cost is low.
Routing is also where relevance classifiers earn their keep: a binary "is this even in scope?" check rejects out-of-scope inputs before any expensive tool fires, cutting both token spend and error propagation. The failure mode to name in an interview: a misroute sends the input to the wrong specialist, and the specialist confidently answers the wrong question. So you measure routing accuracy as its own metric, and you give handlers a cheap escape hatch ("this doesn't look like a refund — re-route") rather than letting them hallucinate.
Setup: 10,000 support tickets/day. One mega-prompt on a frontier model gets 82% right and costs (say) 1.0x per ticket. A router adds one cheap classification call, then sends each ticket to a focused specialist.
End-to-end accuracy = 0.95 * 0.92 + 0.05 * 0.20 = 0.874 + 0.010 = 87.4%, up from 82%.
Cost: the classifier is a tiny call on a cheap model — call it 0.05x. Two of your three specialists run on Haiku at 0.3x, one on a frontier model at 1.0x. Blended specialist cost ≈ 0.05 + (0.7 * 0.3 + 0.3 * 1.0) = 0.05 + 0.51 = 0.56x per ticket.
What it did to the data: +5.4 accuracy points at roughly half the per-ticket cost — because most traffic was easy and the router stopped paying frontier prices for it. The 1% absolute loss from mis-routes is the price you pay; that's the number you'd attack next by improving the classifier.
Parallelization has two distinct shapes, and naming both signals depth.
Sectioning breaks a task into independent subtasks that run concurrently, then aggregates. Reviewing a PR? Run a security-review prompt, a style prompt, and a test-coverage prompt in parallel, then merge. Latency drops to roughly the slowest branch instead of the sum of all branches; token cost stays about the same (you were going to do the work anyway) but wall-clock improves dramatically.
Voting runs the same task multiple times and aggregates — majority vote, or "flag if any of 5 runs says unsafe." This buys confidence, not speed, and it costs Nx the tokens for N votes. You use voting where a false negative is expensive (content moderation, "is this code change risky?") and where the model's errors are uncorrelated enough that voting actually denoises them. The honest caveat: if the model is systematically wrong on a class of input, five votes give you the same wrong answer five times — voting fixes variance, not bias.
Here the structure becomes dynamic. A central orchestrator LLM reads the input and decides at runtime how to decompose it — how many subtasks, of what shape — then delegates each to a worker LLM and synthesizes the results. The defining property: you can't predict the subtask count or structure upfront, so you can't pre-wire it as a sectioning workflow. Classic use: "research this topic," where the orchestrator decides which sub-questions to investigate based on what the topic turns out to be.
This is the most powerful workflow pattern and the one Anthropic explicitly warns most production systems should not need — they advise staying at parallelization or below unless the task genuinely demands runtime decomposition. The cost is real: 3–10x more tokens per task than a simple chain, plus three new failure modes you must name — the orchestrator mis-decomposes (splits the task badly), a worker fails or drifts, or the synthesis step hallucinates a coherent answer from incomplete worker outputs. Because the decomposition is non-deterministic, two runs on the same input can diverge, which is why durable checkpointing (covered in the next lesson) becomes important here.
A generator LLM produces a draft; an evaluator LLM grades it against explicit success criteria; if it fails, the generator revises using the critique; repeat until pass or a max-iteration cap. This works precisely when two conditions hold: you can articulate clear evaluation criteria, and iteration measurably improves the output. Legal-document drafting, code review, and translation polishing are the sweet spot — domains where "is this good?" has a checkable answer and a second pass genuinely helps.
The cost is variable and unbounded unless you cap it — loop depth depends on how fast the generator converges, so you always set a max-iterations limit (and ideally an early-stop when the evaluator score plateaus). The subtle failure mode: if the evaluator and generator share the same blind spot, the loop converges to confidently-wrong, because the grader can't see the error either. Strong answers note that the evaluator should use a different prompt framing — or even a different model — than the generator, so their errors aren't perfectly correlated.
The five patterns are small enough that you should be able to write any of them from memory. Here are three of the trickier ones — routing, voting, and an evaluator loop — in production-shaped Python against the Claude API, using structured outputs so the router's decision and the evaluator's verdict are guaranteed-valid JSON, not regex-scraped prose.
import os
import asyncio
import json
from collections import Counter
from anthropic import Anthropic, AsyncAnthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
aclient = AsyncAnthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
# --- Pattern 2: ROUTING ------------------------------------------------------
# A routing classifier returns a constrained label via tool-use, so the route
# is type-safe — never a hallucinated category. tool_choice forces the call.
ROUTE_TOOL = {
"name": "route",
"description": "Classify a support ticket to the correct specialist.",
"input_schema": {
"type": "object",
"properties": {
"category": {"type": "string", "enum": ["pricing", "refund", "returns", "other"]},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
},
"required": ["category", "confidence"],
},
}
# Map each class to (model, system_prompt). Easy classes -> cheap Haiku.
HANDLERS = {
"pricing": ("claude-haiku-4-5", "You are a pricing specialist. Be exact about plans and tiers."),
"refund": ("claude-sonnet-4-5", "You are a refund specialist. Quote the refund policy precisely."),
"returns": ("claude-haiku-4-5", "You are a returns specialist. Give clear step-by-step instructions."),
"other": ("claude-sonnet-4-5", "You are a general support agent."),
}
def route(ticket: str) -> dict:
"""Upstream classify call. One cheap model call; tool_choice forces JSON."""
msg = client.messages.create(
model="claude-haiku-4-5",
max_tokens=256,
tools=[ROUTE_TOOL],
tool_choice={"type": "tool", "name": "route"}, # force the schema
messages=[{"role": "user", "content": ticket}],
)
decision = next(b.input for b in msg.content if b.type == "tool_use")
# A relevance gate: low confidence -> human handoff, not a guessed answer.
if decision["confidence"] < 0.55:
decision["category"] = "other"
return decision
def handle(ticket: str) -> str:
decision = route(ticket)
model, system = HANDLERS[decision["category"]]
reply = client.messages.create(
model=model, max_tokens=512, system=system,
messages=[{"role": "user", "content": ticket}],
)
return reply.content[0].text
# --- Pattern 3: PARALLELIZATION (voting) -------------------------------------
async def _one_vote(code_diff: str) -> str:
msg = await aclient.messages.create(
model="claude-haiku-4-5", max_tokens=8, temperature=1.0, # variance is the point
system="Reply with exactly one word: SAFE or RISKY.",
messages=[{"role": "user", "content": code_diff}],
)
return msg.content[0].text.strip().upper()
async def vote_is_risky(code_diff: str, n: int = 5) -> bool:
"""Run the SAME judgment n times in parallel; majority wins. Costs n calls."""
votes = await asyncio.gather(*[_one_vote(code_diff) for _ in range(n)])
tally = Counter(votes)
# Conservative: any 'RISKY' plurality blocks. Tune the threshold to your risk.
return tally["RISKY"] >= (n // 2 + 1)
# --- Pattern 5: EVALUATOR-OPTIMIZER ------------------------------------------
GRADE_TOOL = {
"name": "grade",
"description": "Grade a draft against the rubric.",
"input_schema": {
"type": "object",
"properties": {
"passes": {"type": "boolean"},
"score": {"type": "integer", "minimum": 0, "maximum": 10},
"critique": {"type": "string"},
},
"required": ["passes", "score", "critique"],
},
}
def refine(task: str, rubric: str, max_iters: int = 3) -> str:
"""Generate -> evaluate -> refine. ALWAYS cap iterations: loop cost is unbounded."""
draft = client.messages.create(
model="claude-sonnet-4-5", max_tokens=1024,
messages=[{"role": "user", "content": task}],
).content[0].text
for _ in range(max_iters):
grade = client.messages.create(
model="claude-sonnet-4-5", max_tokens=512,
tools=[GRADE_TOOL], tool_choice={"type": "tool", "name": "grade"},
# Different framing than the generator -> less correlated blind spots.
system=f"You are a strict reviewer. Rubric:\n{rubric}",
messages=[{"role": "user", "content": draft}],
)
verdict = next(b.input for b in grade.content if b.type == "tool_use")
if verdict["passes"]:
return draft
# Feed the critique back into a targeted revision.
draft = client.messages.create(
model="claude-sonnet-4-5", max_tokens=1024,
messages=[{"role": "user", "content":
f"Task: {task}\n\nYour draft:\n{draft}\n\nFix this critique:\n{verdict['critique']}"}],
).content[0].text
return draft # ran out of iterations: return best effort, flag for reviewThree things make this production-shaped rather than toy. First, the router uses tool-use with tool_choice forcing so the category is one of four valid enums — Claude restricts token generation to the schema grammar, giving ~99.9%+ valid JSON and eliminating the "model returned Pricing." parsing bug class. Second, the router has a confidence gate: low-confidence tickets fall back to human/general handling instead of being confidently mis-routed. Third, the evaluator loop has a hard max_iters cap and returns a flagged best-effort on timeout — never an infinite loop. Voting uses temperature=1.0 on purpose: votes only denoise if the runs actually vary. Model IDs here are illustrative; check current names in the Claude API reference before shipping.
| Pattern | Relative token cost | Latency profile | Best when | Primary failure mode |
|---|---|---|---|---|
| Prompt chaining | Low (fixed steps) | Sum of steps (serial) | Task is a fixed, verifiable pipeline | Errors compound through steps without gates |
| Routing | Low (one classify call) | Classify + one handler | Distinct input classes need distinct handling | Mis-route → confident wrong answer |
| Parallelization — sectioning | ~Same as serial | ≈ slowest branch | Independent subtasks; latency matters | Aggregation logic drops/double-counts a branch |
| Parallelization — voting | N× (N votes) | ≈ one call (parallel) | Need confidence; uncorrelated errors | Systematic bias survives the vote |
| Orchestrator-workers | 3–10× | Variable, unpredictable | Subtasks unknown until runtime | Mis-decompose / worker drift / synthesis hallucination |
| Evaluator-optimizer | Variable (capped) | Iters × round-trip | Clear rubric + iteration helps | Correlated evaluator/generator blind spot |
The single most important production fact is compounding error. A workflow of k sequential LLM steps, each succeeding with probability p, has end-to-end success of p^k. At p = 0.99 and k = 10, that's 0.99^10 ≈ 0.904 — a 10-step pipeline at 99% per-step reliability fails roughly one run in ten. This is the reason interviewers prefer workflows over agents for high-volume repeatable processes: fewer steps and deterministic paths keep k small and each p near 1. The levers to raise end-to-end success are: cut k (fewer steps), raise each p (gates, structured outputs, voting on the riskiest step), and make failures recoverable (durable checkpointing so a failed step retries instead of re-running the whole chain).
What changes at scale (1000s of runs/day): you can no longer eyeball failures, so observability becomes mandatory — OpenTelemetry spans for every LLM call, tool invocation, and state transition, with structured, queryable logs (model ID, temperature, token counts, the route taken). Industry data shows ~89% of orgs have agent observability and 62% have step-level tracing, but only ~52% close the loop with systematic evaluation — meaning most teams see failures but don't route them back into improvement. The senior move is annotation queues: production traces flagged as wrong get routed to domain experts whose judgments become evaluation datasets. Cost-wise, the discipline is pre-computing a token budget per run and routing easy traffic to cheap models (Haiku) — workflows let you do this because the path is known; agents fight you on it because the path isn't.
classify(x) → handlers[label](x). It earns its keep when distinct input classes need distinctly different handling, because each specialist gets a focused prompt (higher accuracy) and you can route easy classes to a cheaper model. The cost is one extra classification call, and the failure mode is a mis-route, which I'd guard with a confidence gate that falls back to a general handler.0.97^7 ≈ 0.808 — about 81%, which is unshippable. Three levers: (1) cut steps — fold trivial steps into adjacent ones so k drops, since the exponent dominates; (2) raise per-step p — add a gate (schema validation / structured outputs) after the riskiest steps, and vote on the single least-reliable step to denoise it toward ~99.5%; (3) make it recoverable — durable checkpointing so a transient step failure retries that step instead of failing the whole run. To clear 99% end-to-end across 7 steps you need each step at roughly 0.99^(1/7) ≈ 0.9986, so realistically you combine cutting k to ~4–5 and hardening the top two failure points rather than chasing 99.86% everywhere.max_iters and an early-stop when the evaluator's score plateaus, returning a flagged best-effort otherwise. Second, the output stalls because the evaluator and generator share a blind spot — if both are the same model with the same framing, the grader can't see the error the writer made. Fix it by giving the evaluator a different prompt framing (a strict-reviewer persona, an explicit rubric) or a different model, so their errors are less correlated and the critique actually catches something.max_iters and an early-stop, and make the evaluator's framing differ from the generator's to break correlated blind spots.tool_choice forcing for any routing or grading decision — ~99.9%+ valid JSON beats regex every time.Flashcard. Five patterns, increasing autonomy: chain (fixed steps + gates) → route (classify → specialize) → parallelize (section for speed, vote for confidence) → orchestrator-workers (runtime decomposition, 3–10x cost) → evaluator-optimizer (generate-critique-refine, always capped). Pick the smallest that works. End-to-end success of
ksteps at per-step reliabilitypisp^k.
Next: Durable execution & state — what happens when step 6 of 10 crashes, and how checkpoint-replay (Temporal, LangGraph) turns a fragile chain into one that survives restarts. Also see /agents for where the autonomy dial goes past these five, and /evals for turning those annotation-queue traces into regression suites.