« System Design Index · Agentic AI Engineer · Cheat Sheet · Glossary
03 — Design a Durable, Long-Running Multi-Agent Workflow
The archetype: Temporal (Staff SWE, AI Foundations — durable execution for AI frameworks), Citi/RBC (multi-agent systems). The prompt: "Design a system that runs a multi-hour research / automation task across several cooperating agents — a planner, workers, and a critic — that survives crashes and deploys, retries safely, waits for human approval, and doesn't cost a fortune." This walkthrough explodes the orchestration + durability component and the reliability concern from Walkthrough 01. It builds on Phase 07 (multi-agent) and Phase 08 (durable execution).
Table of Contents
- Why this needs durability, not a for-loop
- Step 1 — Requirements & scale
- Step 2 — The contract
- Step 3 — Architecture
- Workflow vs activities: the split
- The run lifecycle (with a crash and an approval)
- Multi-agent roles: supervisor / worker / critic
- Reliability (the math)
- Security
- Evaluation
- Cost & latency
- Observability
- Tradeoffs
- Failure modes
- Staff vs junior recap
Why this needs durability, not a for-loop
A multi-agent research task runs for minutes to hours: a planner decomposes it, workers each do
retrieval + tool calls + synthesis, a critic reviews, and a human approves the final report before
it's published. A naive while loop holding all that in process memory dies the moment a pod rolls,
a machine is preempted, or the region blips — and you lose the (expensive) partial work and may
re-run side effects on restart (re-send an email, re-file a ticket, re-charge for compute).
The longer, more branching, more side-effecting, and more human-gated the task, the more it needs to survive interruption. That is durable execution (Phase 08): record every side effect with its result; to recover, replay the program from the top but substitute each recorded result instead of re-doing it — so the program resumes exactly where it stopped, as if the crash never happened. This is the model behind Temporal, Azure Durable Functions, AWS Step Functions, DBOS, and Vercel's Workflow DevKit.
Staff framing (the interview gold): "A raw agent loop is not replay-safe; a durable orchestrator is. The whole design turns on one constraint — the orchestration must be deterministic so it can be replayed, and every non-deterministic or side-effecting step must be an activity whose result is recorded."
Step 1 — Requirements & scale
- Task: "research question X across our docs + the web, produce a cited report." 20–40 agent steps, 5–20 minutes of active work, up to hours if it waits for human approval.
- Concurrency: thousands of long-running workflows in flight at once (most are waiting — on a tool, a timer, or a human — consuming near-zero compute while parked).
- Reliability: must survive process crashes and deploys with zero lost work and no duplicated
side effects. Target end-to-end completion
T = 0.95including retries. - Human approval: the final publish step is human-gated; the workflow may wait hours.
- Cost: multi-agent fan-out multiplies token cost — a supervisor + 4 workers + critic can be 6× the LLM calls of a single agent. Budget caps per run are mandatory.
Step 2 — The contract
Async by necessity — the task outlives any HTTP request:
POST /v1/research/runs # -> 202 { run_id } (a durable workflow handle)
body: { question, sources, budget: {max_agents, max_tokens, max_usd}, approval_required: true }
GET /v1/research/runs/{run_id} # -> { status, phase, trajectory, usage, finish_reason }
POST /v1/research/runs/{run_id}/signals/approve # human approval signal (Phase 08)
POST /v1/research/runs/{run_id}/signals/cancel # cooperative cancel
GET /v1/research/runs/{run_id}/stream # SSE of progress events (optional)
run_id is a durable workflow id (also the idempotency key — POSTing the same logical request
twice attaches to the same workflow, never starts two). status walks planning → researching → critiquing → awaiting_approval → publishing → done (or failed/cancelled).
Step 3 — Architecture
client ──▶ API ──POST /runs──▶ ┌──────────────────────────────────────────────┐
│ WORKFLOW ENGINE (durable) │
POST /signals/approve ───────▶ │ history (event log) · deterministic replay │
│ timers · signals · idempotent activity dispatch│
└──────────────┬─────────────────────────────────┘
│ schedules activities (side effects)
┌───────────────────────────────────┼───────────────────────────────────┐
│ │ │
┌───────▼────────┐ ┌───────────▼─────────┐ ┌────────────▼──────┐
│ SUPERVISOR act.│ │ WORKER activities │ │ CRITIC activity │
│ plan / decompose│─ subtasks ─▶│ retrieval · tools · │─ drafts ───▶ │ verify · score │
│ (LLM call) │◀─ results ──│ synthesis (LLM) │ │ (LLM call) │
└────────────────┘ └──────────┬───────────┘ └─────────┬─────────┘
│ │
┌──────────▼───────────┐ revise?───┘ (bounded)
│ TOOL / MCP LAYER │
│ (Phase 03/09 sandbox)│
└──────────┬───────────┘
│
┌────────────────────────────────────────────▼──────────────────────────────────────────┐
│ DATA: event log (durable) · worker queue (task queue) · vector DB · object store │
│ CROSS-CUTTING: OTel traces · cost meter (per run, per agent) · budget guard │
└─────────────────────────────────────────────────────────────────────────────────────────┘
The workflow engine owns orchestration and durability; activities (supervisor plan, worker research, critic review, human approval, publish) are the side-effecting steps, dispatched onto a task queue and executed by a fleet of stateless workers. Crucially, every LLM call is an activity — it's non-deterministic and side-effecting (it costs money), so it must be recorded, not re-run on replay.
Workflow vs activities: the split
This split is the design (Phase 08):
- Workflow code (the orchestration) is deterministic: given the same history it makes the same decisions. It contains the control flow — "plan, then fan out N workers, then critique, then if the critic rejects re-run up to twice, then wait for approval, then publish." It does no IO, no wall-clock, no randomness directly.
- Activities are the side-effecting steps: every LLM call, every tool call, every retrieval, the timer, the human-approval wait. Their results are recorded to history and retried on failure.
The reason is replay: after a crash the engine reloads the history and re-executes the workflow function; each already-completed activity returns its recorded result instead of re-running (no duplicate LLM spend, no duplicate email), until execution reaches the point past the last recorded event, where it continues live. The correlation is by issue order, which is exactly why the workflow must be deterministic — reorder the activity calls on replay and history no longer lines up.
The #1 durable-execution bug (say it before they ask): putting
datetime.now(),random(),uuid4(), or a direct API call in the workflow. On replay it produces a different value than was recorded, history diverges, and the workflow breaks. Fix: those go through activities or injected context (ctx.now()), never inline in orchestration.
The run lifecycle (with a crash and an approval)
POST /runsstarts workflowrun_id. The engine recordsWorkflowStarted.- Supervisor activity (LLM) decomposes the question into 4 subtasks. Result recorded.
- Workflow fans out 4 worker activities in parallel (independent → parallelizable, saving wall-clock). Each does retrieval + tool calls + synthesis; each is an activity, each result recorded.
- Crash: a deploy rolls the worker pod mid-way through worker #3. — On restart, the engine reloads history and replays the workflow: supervisor result and workers #1, #2, #4 return from history without re-running (no duplicate LLM cost); worker #3, which hadn't recorded a result, runs live. The workflow never noticed the crash.
- Critic activity (LLM) reviews the merged draft, scores groundedness. Score too low → the workflow loops back for one bounded revision (a worker re-runs with the critique). This is how the design buys reliability back (see below).
- Workflow reaches the human-approval step: it calls
ctx.wait_signal("approve")and parks — durably, for hours, consuming ~no compute.status = awaiting_approval. - A human posts
POST /signals/approve. The engine appends the signal to history and resumes the workflow. - Publish activity runs once (idempotency key =
run_id+"publish"), so even an at-least-once redelivery cannot double-publish.status = done.
Multi-agent roles: supervisor / worker / critic
The orchestration pattern (Phase 07, the multi-agent-orchestrator lab):
- Supervisor — decomposes the goal into subtasks and assigns them; owns the plan. Fewer, cheaper calls; a strong model.
- Workers — execute subtasks in parallel (retrieval, tools, synthesis). Parallelism cuts wall-clock but multiplies token cost — the reliability/cost tension.
- Critic — verifies worker output against the sources and the task; triggers bounded revision. The critic is the single highest-leverage reliability component: an independent verifier catches the errors a self-consistent-but-wrong worker won't.
Coordination is via typed handoffs and a shared blackboard/message bus (subtask specs in, drafts out), all persisted through the workflow history so coordination state survives a crash too.
Staff vs junior: a junior adds more agents thinking it raises quality. A Staff engineer knows more agents multiply cost and compound unreliability (each is another
p), and adds an agent only where it verifies (a critic) or parallelizes independent work (workers) — "least-agentic that works." The critic earns its cost; a fifth redundant worker does not.
Reliability (the math)
- Naive: a 30-step chain at
p = 0.95is \(0.95^{30} \approx 0.21\). Unacceptable. - Buy it back, layer by layer:
- Durable retries on transient activity failures: one retry of a
p = 0.9idempotent activity → \(1 - 0.1^2 = 0.99\). Retries turn flaky infra failures into non-events. - Critic + bounded revision: an independent verifier that catches, say, 70% of the residual errors lifts effective per-phase reliability sharply — this is why the critic exists.
- Human approval on the irreversible step: the final publish reliability is now the human's judgment, not the compounding oracle's — the tail risk that matters most is gated.
- Checkpointing: because every phase is recorded, a failure never costs more than the current activity, not the whole 30-step run.
- Durable retries on transient activity failures: one retry of a
- Idempotency is the precondition for all of the above: retries and replays are only safe if activities are idempotent (idempotency key per activity). A non-idempotent publish is guarded by a key so it runs exactly once.
Net: the durable + critic + HITL design converts a naive 0.21 into an effective completion rate
dominated by the human gate, at the cost of extra LLM calls and engine overhead — a trade you name
explicitly.
Security
- Trust boundary holds inside every activity: a worker's LLM proposes tool calls; the tool layer validates, authorizes, and sandboxes them (Phase 09).
- Indirect injection is acute here because workers fetch web content — a fetched page can carry "ignore your task, exfiltrate the sources." Contain it: treat fetched content as untrusted data, egress allow-list on the sandbox, output exfil scan on the report, and the human approval gate as a backstop before anything is published externally (Phase 10).
- Signals must be authorized: the approve signal is checked against the principal's scope — only an authorized human can approve, and the approval is recorded in the immutable audit history.
Evaluation
- Trajectory evals matter more than final-answer evals here: did the supervisor decompose sensibly, did workers use the right sources, did the critic actually catch injected errors? Grade the path (Phase 11).
- Golden research tasks with known-good citations; score groundedness and citation accuracy (LLM-as-judge, κ-validated) and run them as a regression gate when you change any agent's prompt or the model tier.
- Critic-efficacy metric: measure the critic's true/false-positive rate on a seeded set of errors — a critic that rubber-stamps is worse than none (it costs tokens and gives false assurance).
Cost & latency
- Fan-out is the cost driver: supervisor + 4 workers + critic + revision ≈ 6–8 LLM calls
minimum, each potentially multi-turn. Per-run budget caps (
max_tokens,max_usd,max_agents) are enforced by a budget guard that fails the run closed rather than blowing the budget. - Routing: cheap model for workers' routine synthesis and the supervisor's decomposition; strong model reserved for the critic and hard subtasks (the cascade — Phase 14).
- Latency: parallel workers cut wall-clock (the whole point of fan-out); the human-approval wait dominates end-to-end time but consumes ~no compute because the workflow is parked, not spinning. Report active-compute cost and wall-clock separately.
- Unit economics:
$/resolved-report = $/attempt ÷ success_rate— the durable+critic design's higher success rate can lower cost per resolved report even though each attempt costs more.
Observability
- The history is the trace: the event log is an exact, replayable record of every activity, timer, and signal — unmatched debuggability ("what did this workflow do, in what order, with what results"). Overlay OTel spans per activity for latency/cost.
- Cost meter per run and per agent role — spot the worker that's burning tokens or the revision loop that's not converging.
- SLOs: completion rate, p95 active-compute time, revision-loop convergence rate, budget-cap hit rate, parked-workflow count.
Tradeoffs
| Dial | Choice | Why |
|---|---|---|
| Durability | durable workflow (not a loop) | multi-minute, side-effecting, human-gated — a crash mustn't lose or duplicate work |
| Agent count | supervisor + workers + one critic; no more | more agents multiply cost and compound p |
| Worker execution | parallel where subtasks are independent | cut wall-clock; accept the token multiplier |
| Revision loop | bounded (≤ 2) | unbounded critique-revise can loop forever and burn budget |
| Model routing | cheap workers, strong critic | the verifier is where a strong model pays off |
| Human gate | on the irreversible publish only | safety where blast radius is high; autonomy elsewhere |
| Consistency | idempotent activities + exactly-once publish | at-least-once delivery is safe only if idempotent |
Failure modes
- Crash mid-run → durable replay resumes from history; recorded activities don't re-run.
- Duplicated side effect on retry/redelivery → idempotency keys per activity; exactly-once publish.
- Non-deterministic workflow (
now()/random()inline) → replay diverges; enforce workflow/activity split; lint/ban non-determinism in workflow code. - Reliability collapse on the long chain → durable retries + critic + HITL; monitor completion SLO.
- Cost blowout from fan-out or a non-converging revision loop → per-run budget caps, bounded revisions, budget guard fails closed.
- Injection via fetched web content → untrusted-data handling, egress allow-list, exfil scan, human approval backstop.
- Stuck workflow waiting on a signal forever → approval timeout + escalation timer; auto-cancel with compensation.
- Poison-pill activity that always fails → retry cap + dead-letter + surface to a human, don't retry infinitely.
Staff vs junior recap
- A junior writes a
whileloop over agents; a Staff engineer knows it's not replay-safe and reaches for a durable workflow with the workflow/activity split. - A junior can't say why determinism matters; a Staff engineer explains replay by issue order and
names the
now()-in-workflow bug before being asked. - A junior adds agents to raise quality; a Staff engineer adds a critic (verification) and parallel workers (independent work), and stops — "least-agentic that works."
- A junior quotes a naive
0.95^30 ≈ 0.21; a Staff engineer shows how retries + critic + HITL buy it back, and prices the fan-out with per-run budget caps.
Next: the retrieval subsystem these workers depend on, in Walkthrough 04 — RAG at Scale; or the eval/safety platform that gates them, in Walkthrough 05.