Week 1 · Foundations
Everything from Session 1 on one page: workflow vs agent, the five workflow patterns with code, how to gate and compose them, the three-gate decision map, the guardrails an agent needs, the cost and latency math, the observability minimum, and the antipatterns. Skim it before you reach for an agent.
A demo runs once, on a happy path, with you watching. Production runs a million times, on inputs you never imagined, while you sleep. Each failure mode has a defence, so name it before you ship.
Every token is a line item. A chatty agent loop burns 20× a single call. Price the worst case, not the happy one.
Guard: per-request token budget, cheap-model routing, caching, hard loop caps.
Each sequential hop waits on the model. A 5-step chain feels broken. You are graded at p95/p99, not the average.
Guard: parallelise independent hops, stream tokens, set timeouts, cache hot paths.
A probabilistic service in a deterministic-looking API. A wrong answer arrives with the same tone as a right one. Air Canada was held liable for one.
Guard: ground answers in retrieval, validate outputs, gate with a judge, cite sources.
No evals to catch it, no traces to debug it. You learn it broke from a viral screenshot (see: Chevy, DPD).
Guard: trace every step, run evals in CI, sample live traffic, alert on drift.
One question separates them: are the steps fixed in code, or does the model choose them at run time?
flowchart LR
subgraph WF["WORKFLOW · you drive"]
direction LR
A1["Step 1"] --> A2["Step 2"] --> A3["Output"]
end
subgraph AG["AGENT · the model drives"]
direction LR
M{"Model picks
next step + tool"}
M --> T["Tool"] --> O["Observe"] --> M
M -.-> DONE["Done"]
end
classDef wf fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
classDef ag fill:#EEE6FF,stroke:#1F2937,color:#0E1726;
class A1,A2,A3 wf;
class M,T,O,DONE ag;
| Dimension | Workflow | Agent |
|---|---|---|
| Control flow | fixed in code | chosen by the model at run time |
| Token cost | predictable | unbounded without a cap |
| Latency | you can bound it | grows with every turn |
| Debugging | replay the steps | reconstruct a trajectory |
| Testing | unit-testable | needs eval sets and trajectory checks |
| Reach for it | almost always | last resort, and only when gated |
Ordered simplest to most dynamic. Reach left first; every rung right is a cost you take only when forced. The line between workflow and agent is where the model, not you, starts picking the steps.
flowchart LR
A["1 call"] --> B["Chain"] --> C["Router"] --> D["Parallelize"] --> E["Orchestrator"] --> F["Agent"]
classDef s fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
classDef m fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
classDef a fill:#EEE6FF,stroke:#1F2937,color:#0E1726;
class A,B s; class C,D,E m; class F a;
| Rung | Who picks the step | You gain | You pay |
|---|---|---|---|
| 1 call | you (fixed) | simplest, cheapest, testable | no adaptivity |
| Chain | you (fixed order) | quality via gating between steps | more latency per hop |
| Router | you (branch set is fixed) | right-sized cost per request | a classifier to build and watch |
| Parallelize | you (branch set is fixed) | latency down, quality up via voting | N× the tokens |
| Orchestrator | the model (branches) | handles structure unknown up front | run-time nondeterminism |
| Agent | the model (everything) | open-ended, looping tasks | cost, latency, blast radius |
These cover most real work before you ever need an agent. Each is a handful of lines; the skill is knowing which one the task wants, and where it breaks.
Split a task into ordered steps and gate between them. Validate the outline before you spend tokens writing the essay.
Use when the task has clean, checkable sub-steps and each step's output is the next step's input.
Breaks when steps are actually independent (parallelise instead) or the shape is unknown up front (orchestrate).
Cost: N sequential calls; latency adds up, so keep the chain short.
# gate between steps: fail early and cheap
outline = llm("draft an outline", brief)
if not valid(outline): # deterministic check
return reject(outline)
draft = llm("write from outline", outline)
return llm("tighten and proof", draft)
Classify first, then dispatch: cheap model for easy, big model for hard, a dedicated flow for refunds. Saves money on day one.
Use when inputs fall into distinct kinds that each want different handling or a different model tier.
Breaks when the classifier is wrong: a misroute sends a hard case to the cheap path. Measure routing accuracy.
Cost: one small classifier call plus the chosen flow; usually a net saving.
# classify once, dispatch to the right-sized flow
kind = classify(request) # small, fast model
match kind:
case "faq": return haiku(request)
case "refund": return refund_flow(request)
case _: return opus(request)
Fan out independent work and aggregate. Two flavours: sectioning (split one job into parts) and voting (run the same job N times and take the consensus). Faster, and often better.
Use when subtasks don't depend on each other, or when independent votes raise reliability on a hard call.
Breaks when branches secretly depend on one another, or the merge/vote logic is naive and hides disagreement.
Cost: N× tokens, but roughly 1× latency because the calls run concurrently.
# fan out independent work, then aggregate
parts = gather(
llm("summarise", doc),
llm("extract risks", doc),
llm("list next actions", doc),
)
return merge(parts) # or majority vote
A planner splits work it cannot know up front, spawns workers per subtask, then synthesizes. The branches are decided at run time: the first real taste of agency.
Use when the number and shape of subtasks depend on the input (multi-file edits, research over an unknown set of sources).
Breaks when the plan runs away: no cap on subtasks, or the synthesis step drowns in worker output.
Cost: a planner call, N worker calls, a synthesis call; bound N.
# the planner decides the branches at run time
plan = orchestrator(task) # -> list of subtasks
assert len(plan) <= MAX_SUBTASKS
results = gather(*[worker(s) for s in plan])
return synthesise(task, results)
Generate, critique against a rubric, revise, loop until it passes. Quality without a human in every turn. Always cap the loop, or a $5 task becomes $50.
Use when there is a clear rubric and revision reliably improves the draft (translation, code that must pass tests, structured extraction).
Breaks when the evaluator can't tell better from worse, or the loop has no cap and thrashes forever.
Cost: 2 calls per round × rounds; the cap is the budget.
# generate, critique, revise; ALWAYS cap
draft = generate(task)
for _ in range(MAX_ROUNDS):
v = evaluate(draft, rubric)
if v.passed: break
draft = revise(draft, v.feedback)
return draft
A pattern is only as good as its gates. A gate is the check that decides whether output moves forward, gets revised, or gets rejected. Reach for the cheapest gate that actually catches the failure.
Schema, type, regex, range, required fields, does-it-parse. No model call. Catches malformed and out-of-bounds output instantly.
A model scores the output against a rubric: faithfulness, tone, completeness. Use for quality you can't express as a rule.
Check the answer against a source document or a golden set. The main defence against confidently-wrong output.
A person approves before the action commits. Reserve for irreversible or high-stakes steps; it costs latency and attention.
Real systems are not one pattern; they are trees of them. A router picks a branch, a branch is a chain, a chain fans out in parallel, the result passes an evaluator before it ships. Each node stays independently testable.
flowchart TB
IN(["request"]) --> R{"Router"}
R -- simple --> C1["1 call"]
R -- structured --> CH["Chain w/ gates"]
R -- research --> O["Orchestrator"]
O --> P[["Parallel workers"]]
C1 --> EV{"Evaluator"}
CH --> EV
P --> EV
EV -- fail --> CH
EV -- pass --> OUT(["response"])
classDef s fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
classDef m fill:#DCEBFE,stroke:#1F2937,color:#0E1726;
classDef a fill:#EEE6FF,stroke:#1F2937,color:#0E1726;
class C1,CH s; class R,O,P m; class EV,OUT a; class IN s;
Three gates. An agent has to clear all three. Most features stop at the first, and that is a good thing.
flowchart TB
START(["A task to build"]) --> Q1{"Steps known
in advance?"}
Q1 -- yes --> WF["Use a WORKFLOW"]
Q1 -- "no, open-ended" --> Q2{"Must it loop:
act, observe, decide?"}
Q2 -- no --> WF
Q2 -- yes --> Q3{"Worth the cost,
latency and risk?"}
Q3 -- "not yet" --> WF
Q3 -- yes --> AG["Use an AGENT
with guardrails + evals"]
classDef wf fill:#D6F5E3,stroke:#1F2937,color:#0E1726;
classDef ag fill:#EEE6FF,stroke:#1F2937,color:#0E1726;
class WF wf; class AG ag;
Can you write the steps in code ahead of time? If yes, a workflow wins. Agency only earns its keep when the path genuinely can't be scripted.
Does the task have to act, observe the result, and decide the next move on the fly? A one-shot or fixed chain that would do means you don't need an agent.
Is the value worth the extra tokens, latency, and blast radius, and can you afford the guardrails and evals it demands? If not yet, stay on a workflow.
An unguarded agent is the demo that dies in every one of the four ways at once. Before it touches production, put a ceiling on each axis it can run away on.
| Axis | Guardrail |
|---|---|
| Iterations | hard max-steps; break on no-progress |
| Budget | token/$ ceiling per request; kill on breach |
| Tools | allow-list, least privilege; no raw shell |
| Irreversible acts | human approval or dry-run first |
| Time | per-tool and per-request timeouts |
| I/O | structured tool schemas; validate every arg |
| Failure | typed fallback path; never a raw stack trace to the user |
# the model drives; bound every axis
for step in range(MAX_STEPS): # 1. iteration cap
if spend > BUDGET: break # 2. $ ceiling
act = model(state, TOOLS) # 3. allow-list
if act.risky:
require_human(act) # 4. approval
state = execute(act, timeout=30) # 5. sandbox+TO
if act.done: break
Order-of-magnitude, not benchmarks. The point is the shape: an agent's worst case is "it never stops", which is why every rung right on the ladder is a bill you sign up for.
| Shape | Model calls | Rel. tokens | Rel. p95 latency | Blast radius |
|---|---|---|---|---|
| Single call | 1 | 1× | 1× | tiny |
| 3-step chain | 3 | ~3× | ~3× | small, bounded |
| Router + flow | 2 | ~1–2× | ~1.5× | small |
| Parallel (×4) | 4 | ~4× | ~1× (concurrent) | medium cost |
| Agent (~10 turns) | 10–30+ | 10–30× | 10×+ | large, unbounded |
You can't debug what you can't replay, and you can't improve what you don't measure. Two flying-blind failure modes above have the same cure: traces and evals from day one.
One span per step and per tool call, with inputs, outputs, tokens, and latency. When it breaks, you replay the trajectory instead of guessing.
A fixed test set scored in CI on every change. Catches regressions before they ship, not after a screenshot.
Sample live traffic, score with a judge or human labels, and watch for quality drift as inputs shift under you.
Task success, faithfulness/groundedness, p95 latency, cost per request, and refusal/error rate. Alert on each.
One giant loop with 30 tools doing everything. Impossible to test, reason about, or bound.
Multi-agent choreography for a job a three-step chain would nail.
Shipped with no traces or evals, so you learn it is broken from users, not dashboards.
An agent where a router or chain would do. You bought nondeterminism you didn't need and now have to guard.
A generate-critique or agent loop with no cap. A $5 task quietly bills $50 the day inputs get weird.
One bad model call and the raw error reaches the user. No retry, no default, no graceful degrade.
| If the task is… | Reach for | Because |
|---|---|---|
| Ordered steps, each checkable | Chaining | gate between steps, fail early and cheap |
| Different inputs need different handling | Routing | right-size the model per request |
| Independent subtasks, known up front | Parallelization | latency down, quality up via voting |
| Subtasks unknowable until run time | Orchestrator | the planner decides the branches |
| Quality needs iteration to a bar | Evaluator–optimizer | critique-and-revise loop, capped |
| Open-ended, must loop, worth the risk | Agent | only after all three gates clear |