Agents vs workflows
Most problems people reach for an agent to solve are better solved by a workflow — a fixed path of LLM calls you compose and control. This is the core of Anthropic's published "Building Effective Agents" guidance: start with the simplest thing that works, add autonomy only when the task genuinely needs it. This lesson turns that guidance into a decision you can make, with a runnable sketch of each of the five workflow patterns and the one true agent pattern.
Learning objectives
- State Anthropic's workflow-vs-agent distinction and why workflow-first is the default.
- Recognize each of the five workflow patterns in the wild and know when each applies.
- Model each pattern's control flow in plain Python — prompt chaining, routing, parallelization, orchestrator-workers, evaluator-optimizer.
- Explain the cost / latency / reliability reasons autonomy is expensive.
- Decide — as a lead — when a true autonomous agent is justified, and defend it.
1 · The word "agent" hides a decision essential
"We should build an agent for this" is where a lot of LLM projects go wrong. In Anthropic's published engineering writing (the Building Effective Agents post), the useful split is between workflows — systems where you code the path and the LLM fills in the steps — and agents — systems where the LLM decides the path at runtime, directing its own tool use in a loop. Both are "agentic systems"; only one is an "agent." The decision between them is the first and most consequential design choice you make.
Per Anthropic's current guidance (verify in their docs, as wording evolves), the headline advice is deliberately unglamorous: find the simplest solution possible, and only increase complexity when it demonstrably improves outcomes. A single well-prompted call beats a workflow when it suffices; a workflow beats an agent when the steps are knowable. You climb that ladder only as far as the problem forces you.
2 · The decision, in one picture essential
The fork is about how well you can specify the task. If you can write down the steps up-front, you want a workflow: predictable, cheap, testable. If the task is open-ended — the steps depend on what the model discovers as it goes — you may need an agent. Everything else is choosing which workflow pattern fits.
This one picture is the whole lesson. Follow the arrows left to right; the middle box is the only decision you actually make.
- Task — start with what you need done, before naming any technology.
- Steps knowable? — the fork. Ask: can I write down the steps before I run? That single question decides everything downstream.
- Workflow (green) — if yes, you code the path and let the model fill in each step. Predictable, cheap, and testable one step at a time.
- Agent (amber) — if no, the path truly depends on what the model discovers, so the model drives the loop. More power, but more cost and less predictability.
In short: If you can enumerate the steps, it's a workflow — reach for the patterns in §4–§8. Only the genuinely un-scriptable case earns the agent in §9.
Read it left-to-right. The middle node is the only real question: can you enumerate the steps before you run? If yes, pick a workflow pattern (§4–§8) — you keep control, the system is debuggable, and each step is independently testable. If no — the path truly varies with the input and can't be scripted — that's the narrow case where an agent (§9) earns its cost.
3 · Why autonomy is expensive intermediate
Why is "workflow first" the default rather than a stylistic preference? Because an autonomous agent pays real, compounding costs a fixed workflow avoids. A workflow makes a known number of calls on a known path; an agent loops an unknown number of times, each turn re-sending a growing context.
| Axis | Workflow | Autonomous agent |
|---|---|---|
| Cost | bounded — you know the call count | unbounded — loops until done or capped |
| Latency | predictable, often parallelizable | serial round-trips, grows with steps |
| Reliability | each step testable in isolation | errors compound across the loop |
| Debuggability | fixed path — inspect any step | path varies per run — hard to reproduce |
| When it wins | steps are knowable | task is genuinely open-ended |
cost_model.pydef workflow_cost(steps, cost_per_call):
"""A workflow makes a FIXED number of calls."""
return steps * cost_per_call
def agent_cost(avg_loops, cost_per_call, context_growth=1.0):
"""An agent loops an unknown number of times; each turn re-sends a
growing context, so later calls cost more."""
total = 0.0
for turn in range(avg_loops):
total += cost_per_call * (context_growth ** turn)
return total
wf = workflow_cost(steps=3, cost_per_call=0.01)
ag = agent_cost(avg_loops=8, cost_per_call=0.01, context_growth=1.2)
print(f"workflow (3 fixed calls): ${wf:.3f}")
print(f"agent (8 growing loops): ${ag:.3f}")
print(f"agent is ~{ag/wf:.1f}x the workflow here")
workflow (3 fixed calls): $0.030
agent (8 growing loops): $0.165
agent is ~5.5x the workflow here
4 · Pattern 1 — Prompt chaining intermediate
When to use: the task splits cleanly into fixed, ordered subtasks, each easier and more reliable than doing it all in one shot — and you can check the work between steps. Anthropic's canonical example: draft, then translate; or outline, then write. Each step's output is the next step's input.
Spot it in the wild: any "first do X, then do Y with the result" pipeline — generate-then-format, extract-then-summarize. If you find yourself asking one prompt to do three things, that's usually a chain wanting to be split — with an optional gate between steps that bails early if a step fails a check.
chaining.pydef step(name, text):
"""Offline stand-in for an LLM call: deterministic, no network."""
return f"[{name}] {text}"
def gate_ok(text):
"""A cheap check between steps (e.g. 'did the outline have >=1 section?')."""
return "outline" in text
def chain(topic):
outline = step("outline", topic)
if not gate_ok(outline): # gate: stop early if step 1 failed
return "ABORT: outline failed its check"
draft = step("draft", outline) # each step feeds the next
polished = step("polish", draft)
return polished
print(chain("a blog post about caching"))
[polish] [draft] [outline] a blog post about caching
Prompt chaining splits one big ask into fixed, ordered steps, where each step's output feeds the next — and an optional gate between steps can stop early if a step fails a cheap check. This models that control flow with a fake step() so it runs offline.
step(name, text)is a stand-in for an LLM call — deterministic, no network — so you can see the shape of the pattern without an API key.outline = step("outline", topic)is step one. Its result becomes the input to step two — that hand-off is what makes it a chain.if not gate_ok(outline):is the gate — a cheap check between steps. If step one's output fails, wereturnan abort instead of wasting the later calls. Gates are what make chains reliable.- The remaining
draftthenpolishsteps each consume the previous output, so the final string nests all three markers.
What the output means: The gate passes (the outline contains "outline"), so all three steps run and you get [polish] [draft] [outline] a blog post about caching — the steps wrapped in order.
Try this: Change gate_ok to look for a word the outline won't contain and re-run — you'll get the ABORT path, proving the gate can stop a chain early.
5 · Pattern 2 — Routing intermediate
When to use: inputs fall into distinct categories that are each handled better by a specialized prompt (or a different-sized model), and you can classify the input reliably. Anthropic's example: route customer queries to the right specialized handler; send easy cases to a small cheap model and hard ones to a big one.
Spot it in the wild: a classify-then-dispatch shape. The win is separation of concerns — each handler stays simple because it only sees its own kind of input — plus a cost lever: most traffic is easy and can go to a cheaper model.
routing.pydef classify(query):
"""Offline classifier — a real one would be a cheap model call."""
q = query.lower()
if "refund" in q or "charge" in q: return "billing"
if "error" in q or "crash" in q: return "technical"
return "general"
def handle_billing(q): return f"BILLING desk: {q}"
def handle_technical(q): return f"TECH desk: {q}"
def handle_general(q): return f"GENERAL desk: {q}"
ROUTES = {"billing": handle_billing, "technical": handle_technical,
"general": handle_general}
def route(query):
kind = classify(query) # cheap step decides the path
return ROUTES[kind](query) # specialized handler does the work
for q in ["I want a refund", "the app crashed", "what are your hours"]:
print(route(q))
BILLING desk: I want a refund
TECH desk: the app crashed
GENERAL desk: what are your hours
Routing is classify-then-dispatch: a cheap step decides which category an input belongs to, then a specialized handler does the work. Each handler stays simple because it only ever sees its own kind of input.
classify(query)is the cheap routing step — here plain keyword checks, but in production a small, fast model. It returns a category name, not an answer.handle_billing,handle_technical,handle_generalare the specialized handlers — one per category, each free to use its own prompt or even a different-sized model.ROUTESmaps each category name to its handler, sokind = classify(query)thenROUTES[kind](query)is the entire dispatch: decide the path, then walk it.- The loop runs three different queries and each lands at the right desk — separation of concerns, plus the cost lever of sending easy traffic to a cheaper model.
What the output means: Three lines, each query routed to its matching desk: refund → BILLING, crash → TECH, hours → GENERAL.
Try this: Add a fourth category (say "sales") with its own keyword, handler, and ROUTES entry — the dispatch code doesn't change, only the routing table.
6 · Pattern 3 — Parallelization (sectioning & voting) advanced
When to use: the work can be split into independent pieces that run at once. Anthropic describes two flavors. Sectioning: break a task into subtasks that don't depend on each other and run them in parallel (e.g. one call per document, or one guardrail check running alongside the main answer). Voting: run the same task several times and aggregate — majority vote or best-of — to raise reliability on a hard judgment.
Spot it in the wild: a fan-out/fan-in shape. Sectioning cuts latency (pieces run concurrently); voting trades extra cost for confidence. Both need an aggregation step — the design choice is how you combine the partial results.
parallel.pyimport hashlib
from collections import Counter
def step(worker, item):
"""Offline stand-in for a per-piece model call."""
return f"{worker}:{item}"
def sectioning(docs):
"""Independent subtasks -> run each, then combine (fan-out / fan-in)."""
partials = [step("summ", d) for d in docs] # would be concurrent
return " | ".join(partials) # aggregation step
def one_run(task, i):
"""Deterministic stand-in for one model verdict (stable across runs)."""
h = int(hashlib.md5(f"{task}:{i}".encode()).hexdigest(), 16)
return "flag" if h % 4 == 0 else "safe"
def voting(task, runs=5):
"""Same task many times -> majority vote for reliability."""
votes = [one_run(task, i) for i in range(runs)]
winner, n = Counter(votes).most_common(1)[0]
return f"verdict={winner} ({n}/{runs} agreed)"
print(sectioning(["doc1", "doc2", "doc3"]))
print(voting("is this comment abusive?"))
summ:doc1 | summ:doc2 | summ:doc3
verdict=safe (4/5 agreed)
Parallelization has two flavors. Sectioning splits work into independent pieces that run at once and then combines them. Voting runs the same task several times and takes the majority — trading extra cost for confidence on a hard call.
sectioning(docs)runs onestep()per document (these would run concurrently in real code) and then aggregates with" | ".join— the fan-out / fan-in shape.one_run(task, i)is a deterministic stand-in for one model verdict — it hashes the task so the demo prints the same result every time (a real run would be a live model call).voting(task, runs=5)collects five verdicts and usesCounter(...).most_common(1)to pick the majority — the aggregation step for voting.- The design choice in both flavors is how you combine the partial results: concatenate for sectioning, majority-vote (or best-of) for voting.
What the output means: The sectioning line joins the three per-doc summaries; the voting line reports verdict=safe (4/5 agreed) — four of five runs agreed on "safe".
Try this: Raise runs to 9 and see how the agreement count changes. More runs cost more but make the majority more stable — that's the cost/confidence trade.
7 · Pattern 4 — Orchestrator-workers advanced
When to use: you can't enumerate the subtasks up-front — the number and shape of the pieces depend on the input — but a central LLM can decompose the problem, and the pieces are still done by directed worker calls. Anthropic's example: a coding change that touches an unknown set of files, where an orchestrator decides which files and dispatches a worker per file, then synthesizes the results.
Spot it in the wild vs. parallelization: in sectioning you fix the split in code; in orchestrator-workers the model decides the split at runtime. It's the bridge toward agency — dynamic decomposition — but the workers are still bounded, directed calls, not a free-running loop. That's what keeps it a workflow.
orchestrator.pydef step(role, payload):
"""Offline stand-in for a model call."""
return f"[{role}] {payload}"
def orchestrator(task):
"""A central step decides the subtasks at RUNTIME (count varies)."""
if "refactor" in task:
subtasks = ["rename symbols", "update imports", "fix tests"]
else:
subtasks = ["do it"]
return subtasks
def worker(subtask):
return step("worker", subtask)
def run(task):
subtasks = orchestrator(task) # model picks the pieces
results = [worker(s) for s in subtasks] # a directed call each
return step("synthesize", " + ".join(results)) # combine
print(run("refactor the auth module"))
[synthesize] [worker] rename symbols + [worker] update imports + [worker] fix tests
Orchestrator-workers is the pattern for when you can't list the subtasks up front — their number and shape depend on the input. A central step decides the split at runtime; each piece is still a directed worker call, then a synthesize step combines them.
orchestrator(task)is the central decision: at runtime it chooses the subtasks. A "refactor" produces three; anything else produces one. That dynamic count is what separates this from fixed sectioning.worker(subtask)handles one piece — a bounded, directed call, not a free-running loop. The workers stay simple; the orchestrator holds the plan.subtasks = orchestrator(task)then a worker per subtask thenstep("synthesize", ...)is the full flow: decide the pieces, do each, combine.- It's the bridge toward agency — the model decides the decomposition — but because the workers are bounded calls with no self-directed loop, it stays a workflow.
What the output means: The orchestrator picks three subtasks for the refactor, a worker runs each, and the synthesize step joins them into one result string.
Try this: Pass a non-refactor task ("add a comment") and watch the orchestrator choose the single-subtask path — the number of workers changed with the input, at runtime.
8 · Pattern 5 — Evaluator-optimizer professional
When to use: there are clear evaluation criteria and iteration measurably improves the output — and, crucially, a critic can articulate why a draft falls short. Anthropic's examples: literary translation, or deep search where a second pass catches what the first missed. One call generates; another evaluates against the criteria and hands back feedback; the generator revises; repeat until it passes or you hit a cap.
Spot it in the wild: a generate → critique → revise loop with a bounded number of rounds. It looks agent-ish, but the control flow is fixed: two roles, a scoring function, and a hard iteration cap. Don't confuse it with a full agent — the loop is your loop, not the model's.
evaluator.pydef generate(prompt, feedback=""):
"""Offline stand-in — appends any feedback to simulate improvement."""
base = len(prompt)
return {"text": prompt + feedback, "quality": base + len(feedback)}
def evaluate(draft, target=30):
"""Score against clear criteria; return pass + actionable feedback."""
if draft["quality"] >= target:
return True, ""
return False, " [add detail]"
def optimize(prompt, max_rounds=5):
feedback = ""
for r in range(max_rounds): # HARD cap -> always terminates
draft = generate(prompt, feedback)
ok, feedback = evaluate(draft)
if ok:
return f"passed on round {r+1}: {draft['text']!r}"
return "hit round cap without passing"
print(optimize("translate this line"))
passed on round 2: 'translate this line [add detail]'
Evaluator-optimizer is a generate → critique → revise loop against clear criteria — but it's your loop, with a hard round cap, so it always terminates. That cap is exactly what keeps it a workflow and not an agent.
generate(prompt, feedback)produces a draft; here it simply appends any feedback so its "quality" score rises each round — a stand-in for a model improving on a critique.evaluate(draft, target=30)scores against clear criteria and returns(passed, feedback)— actionable feedback the next generate can act on.for r in range(max_rounds):is the loop, andrangegives it a hard cap — aftermax_roundsit stops whether or not it passed. No unbounded looping.- Each round feeds the previous round's feedback back in, so quality climbs until it clears the target and we
returnthe passing draft.
What the output means: Round 1 scores 18 (below 30); round 2 adds feedback to reach 31 and passes, so it prints passed on round 2 with the revised text.
Try this: Lower target to 10 and it passes on round 1; raise it above what feedback can reach and it hits the round cap — the cap is the safety rail.
9 · The autonomous agent — and when it's justified professional
When to use: the task is open-ended, you can't predict the number or order of steps, and the model needs to act on feedback from a real environment (tools, files, shell) over many turns. Anthropic's framing: agents are for problems where you can't hard-code the path, and where the value justifies higher cost, higher latency, and lower predictability. The model plans, acts, observes the result, and decides the next action — in a loop — until it judges the task done.
Anthropic's published "should this be an agent?" check has four gates: complexity (is it genuinely hard to script?), value (does the autonomy pay for its cost?), model capability (is the model actually good at this task?), and cost of error (are mistakes recoverable, or gated by a human?). If any gate is "no," compose the workflow patterns above instead.
agent.pydef model_decides(observations):
"""Offline stand-in for the LLM choosing its OWN next action.
Returns ('act', tool) to keep going, or ('done', answer) to stop."""
if len(observations) < 3: # pretend it needs 3 lookups
return ("act", f"lookup_{len(observations)}")
return ("done", f"answer from {observations}")
def agent(goal, max_steps=10):
"""The MODEL owns the control flow — path varies per run."""
observations = []
for step in range(max_steps): # cap = the only safety rail
action, payload = model_decides(observations)
if action == "done":
return f"[{step} steps] {payload}"
observations.append(payload) # act -> observe -> loop
return "stopped: hit step cap"
print(agent("investigate the alert"))
[3 steps] answer from ['lookup_0', 'lookup_1', 'lookup_2']
max_steps cap trips. That's the reliability cost of autonomy, and why Anthropic's guidance is to reach for it last and gate irreversible actions (Chapter 4's safety rails).10 · Recognizing the patterns — a field guide tech-lead
A lead's real skill here is naming the pattern behind a vague request and pushing the design down the complexity ladder. Most "build an agent" asks decompose into one or two workflow patterns that are cheaper, faster, and testable. Use this as the translation table:
| You hear… | It's probably… | Because |
|---|---|---|
| "first do X then Y" | prompt chaining | fixed ordered steps with a check between |
| "handle each type differently" | routing | distinct categories, specialized handlers |
| "do all of these at once" / "be more sure" | parallelization | independent pieces, or voting for confidence |
| "break it up — but it depends on the input" | orchestrator-workers | the model decides the split at runtime |
| "keep improving it until it's good" | evaluator-optimizer | clear criteria + bounded iteration |
| "figure it out and just handle it" | agent (maybe) | open-ended — run the four-gate check first |
The tech-lead move is not to pick the most powerful pattern; it's to pick the simplest pattern that meets the bar, and to say no to premature autonomy. A composed set of workflows you can test beats an impressive agent you can't reproduce.
11 · Check yourself tech-lead
A teammate says: "We need an agent that summarizes each incoming document and then flags any that mention a competitor." Which pattern(s) does this actually call for, and is it an agent?
Show answer
Evaluator-optimizer has a loop, and an autonomous agent has a loop. Per Anthropic's framing, what actually distinguishes them?
Show answer
Exercise CS1.1 — Push a request down the ladder
Context: Every engineer has a "we should build an agent" idea in flight. The discipline is running the four-gate check on it honestly and pushing it down the complexity ladder before writing any code.
Your task: Take a real "build an agent" idea from your own work: run Anthropic's four-gate check, redesign it as a composition of the five workflow patterns if any gate is "no," and state the cheapest pattern that meets the bar.
Requirements:
- Answer each of the four gates — complexity, value, model capability, cost of error — for your idea
- If any gate is "no," redesign it as a composition of the five workflow patterns
- Sketch the control flow using the lesson's
step()stand-ins - State the cheapest pattern that still meets the bar
- If you keep the agent, justify which specific gate forced it
💡 Hint: A single "no" on any gate is enough to push the design back to composed workflows — find it before you commit.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: "We should build an agent for this" is where a lot of LLM projects go wrong. Per Anthropic's public "Building effective agents" guidance, autonomy is a cost you pay only when the path can't be predefined.
Your task: For each task, decide workflow (fixed code path) or agent (model directs its own steps): (a) translate a support email to French, (b) "fix this failing test in our repo."
Requirements:
- Classify (a) as a workflow — one call, fixed path, known inputs/outputs
- Explain that paying for autonomy on (a) only adds latency, cost, and failure surface
- Classify (b) as an agent — the steps (read code, run tests, edit, re-run) can't be predefined
- Note the agent case needs the model to decide based on tool results as it goes
- Ground the rule in Anthropic's public guidance: prefer the simplest thing that works
💡 Hint: Ask a single question of each task: can I write the steps down before I run?
Show solution
- (a) Translate an email: Workflow — one LLM call, fixed path, known inputs/outputs. No decisions to delegate, so paying for autonomy would only add latency, cost, and failure surface.
- (b) Fix a failing test: Agent — the number and nature of steps (read code, run tests, edit, re-run) can’t be predefined; the model must decide based on tool results.
Per Anthropic’s public “Building effective agents” guidance: prefer the simplest thing that works, and only add agentic autonomy when the task genuinely requires open-ended, model-directed steps.
Context: Most "build an agent" asks are really one or two named workflow patterns in disguise. Naming the pattern is what lets you reach for the cheapest thing that meets the bar.
Your task: Match each to one of the lesson's five patterns: (a) classify a ticket then send billing vs technical to different prompts; (b) draft a translation, then critique-and-improve it in a loop.
Requirements:
- Identify (a) as routing — a classifier directs input to one of several specialized prompts
- Explain why routing beats one mega-prompt trying to handle every type
- Identify (b) as evaluator-optimizer — one call generates, another evaluates, loop until it passes or caps
- State when evaluator-optimizer fits: clear criteria and iteration measurably helps
- Note that both are workflows — the control flow is fixed even though an LLM sits in each box
💡 Hint: The presence of an LLM in a step doesn't make the system an agent; who owns the control flow does.
Show solution
- (a) Classify then branch: Routing — a classifier directs the input to one of several specialized downstream prompts. Cheaper and more accurate than one mega-prompt trying to do all types.
- (b) Draft then critique-and-improve: Evaluator-optimizer — one call generates, another evaluates against criteria, and you loop until it passes or you hit a cap. Use it when quality has clear criteria and iteration measurably helps.
Both are workflows (fixed control flow), not agents — the path is predefined even though an LLM sits in each box.
Context: Some tasks can't have their subtasks enumerated up front — the number and shape depend on the input. Orchestrator-workers is the pattern that decomposes at runtime while keeping the workers bounded.
Your task: Design a "research a company" feature as orchestrator-workers: show what the orchestrator decides, what workers do, and the one guardrail that keeps cost bounded.
Requirements:
- The orchestrator decides the set of sub-tasks dynamically at runtime (e.g. funding, product, news)
- Workers each own one bounded sub-task and return results — not a free-running loop
- A synthesis step combines worker outputs and drops empties
- Explain why the dynamic split makes this orchestrator-workers, not plain parallelization
- Name the guardrail: cap the number of workers (and per-worker tool calls / token budget) the orchestrator may spawn
💡 Hint: The workers stay simple; the orchestrator holds the plan — and a cap on the fan-out is the core discipline.
Show solution
Orchestrator (LLM): given a company name, decides which sub-tasks are needed,
e.g. [funding history, product line, recent news, headcount].
| spawns (in parallel)
v
Worker 1: search + summarize funding
Worker 2: search + summarize product line
Worker 3: search + summarize recent news
| results returned
v
Orchestrator: synthesizes workers' outputs into one brief, drops empties.
Orchestrator decides the set of sub-tasks dynamically (that’s why it’s not plain parallelization); workers each own one bounded sub-task. Guardrail: cap the number of workers (and per-worker tool calls / token budget) the orchestrator may spawn, so a vague query can’t fan out into runaway cost. Bounding the loop is the core discipline of agentic systems.
Context: A PM wants a fully autonomous agent that "reads any user request and does whatever's needed across our internal tools." Full autonomy over internal tools is the most expensive, highest-blast-radius option.
Your task: Write the design review: when full autonomy is justified, and the cheaper alternative you'd propose first.
Requirements:
- State the stance: full autonomy is expensive, hard to test, and has a large blast radius
- Justify it only when the task space is genuinely open-ended, value per task is high, and you have observability + reversibility
- Propose the cheaper first design: routing + a small set of narrow workflows covering the top request types
- Instrument what falls through to "other" to find where an agent is actually warranted
- Gate any destructive tool call behind human approval even in the agent case
- Tie it to Anthropic's public guidance: add complexity only when simpler compositions demonstrably fall short
💡 Hint: Cover the 80% with cheap workflows first, then reserve autonomy for the instrumented long tail.
Show solution
Design-review stance: full autonomy over internal tools is the most expensive and highest-risk option — unpredictable cost, hard to test, and a large blast radius if a tool call goes wrong. Justify it only when: the task space is genuinely open-ended, the value per task is high, and you have observability + reversibility (dry-runs, human approval on risky actions, audit logs).
Cheaper first proposal: start with routing + a small set of narrow workflows covering the top few request types (they’re usually 80% of volume). Instrument what falls through to “other.” Only the genuinely unpredictable remainder justifies an agent, and even then, gate destructive tool calls behind human approval. This mirrors Anthropic’s public guidance: add complexity only when simpler compositions demonstrably fall short.
Context: A team needs a repeatable way to decide workflow-vs-agent for any new feature, not a one-off judgment call. The right artifact is a checklist biased toward simplicity that anyone can paste into a design doc.
Your task: Turn the lesson into a reusable checklist that decides workflow vs agent and which pattern, read top-to-bottom, first match wins.
Requirements:
- Lead with the decisive question: can you write the steps as fixed code today? If yes, workflow — stop
- Cover single-call (one transform), routing (known categories), and parallelization (independent known sub-tasks)
- Include evaluator-optimizer (quality improves with critique loops) with an iteration cap
- Include orchestrator-workers (the set of steps is data-dependent)
- End with agent only when the path is truly unpredictable AND value-per-task is high — with budget caps, approvals, audit
- Instruct the reader to take the first match; every rung down adds cost, latency, and failure surface
💡 Hint: Order the questions so the cheapest answer is reached first and autonomy is the last resort, not the default.
Show solution
| Question | If yes → |
|---|---|
| Can you write the steps as fixed code today? | Workflow. Stop here. |
| Is it one transform (translate/classify/extract)? | Single call. |
| Do inputs split into known categories? | Routing. |
| Are sub-tasks independent & known? | Parallelization. |
| Does quality improve with critique loops? | Evaluator-optimizer (with an iteration cap). |
| Is the set of steps itself data-dependent? | Orchestrator-workers. |
| Is the path truly unpredictable AND value-per-task high? | Agent — with budget caps, approvals, audit. |
Read top to bottom and take the first match. The checklist is biased toward simplicity on purpose: every rung down adds cost, latency, and failure surface.
Context: A representative team wants to "automate our invoice processing." The lead's job is to walk from that vague ask to a right-sized design — and to resist over-building where autonomy merely tempts.
Your task: Walk from the vague "automate invoice processing" ask to a right-sized design, showing where you resist over-building.
Requirements:
- Decompose the ask: extract fields, validate against a PO, flag exceptions, post approved ones
- Design extraction as a schema-constrained single call or small chain — predictable, so a workflow
- Use deterministic code for validation (numbers match the PO), not an LLM — cheaper and exact
- Route exceptions to a human queue with the model's explanation attached
- Resist the agent temptation (chasing missing PO numbers across systems) — cover known systems with a routed lookup first
- Conclude v1 is mostly workflow + one human-in-the-loop queue and no autonomous agent; add one only if the tail proves unpredictable
💡 Hint: Name each sub-step's cheapest sufficient pattern, and make the human queue the home for the messy remainder.
Show solution
- Decompose the vague ask. “Invoice processing” = extract fields → validate against a PO → flag exceptions → post approved ones. Three of four steps have fixed paths.
- Extraction: schema-constrained single call (or a small chain) — predictable, so a workflow.
- Validation: deterministic code (numbers match the PO), not an LLM — cheaper and exact.
- Exceptions: route mismatches to a human queue with the model’s explanation attached.
- Where autonomy tempts you: “let an agent chase down missing PO numbers across systems.” Resist first — most gaps hit a handful of known systems, so a small routed lookup covers them. Reserve an agent for the long tail, gated by approval before it acts.
Outcome: mostly workflow, one human-in-the-loop queue, and no autonomous agent in v1. Add the agent only if the exception tail proves genuinely unpredictable and worth the cost.
✓ Checkpoint — you can move on when you can…
- State the workflow-vs-agent distinction and why "workflow first" is Anthropic's default.
- Name all five workflow patterns and when each applies.
- Model each pattern's control flow in plain Python.
- Explain the cost / latency / reliability reasons autonomy is expensive.
- Run the four-gate check and decide — and defend — when a true agent is justified.