"I called the tool. Then I crashed. Then I woke up and called the tool again. The customer now owns two refrigerators. The retrospective was tense."
An Agent Without an Idempotency Key
A multi-agent workflow that survives crashes, never double-acts on a retry, and can pause for a human and resume hours later is not a prompt; it is a distributed, stateful, fault-tolerant program, and the engine that runs it is a distributed scheduler of agent steps and tool calls. The earlier sections of this chapter built the pieces: agents as components (32.1), tool calls (32.2), parallel workflows (32.4), messaging protocols (32.6), and shared state (32.7). This section is where those pieces are run for real. The frameworks that get the attention (LangGraph, AutoGen, CrewAI, the OpenAI Agents SDK) define how you express a workflow; the property that separates a demo from production is what happens to the workflow's state when a machine dies. The answer, durable checkpointing of every step plus idempotent execution of every side effect, is the same fault-tolerance machinery this book has used since elastic training, now wrapped around LLM and tool calls instead of gradients.
By this point in the chapter an agent workflow is a graph of steps: a planner decides what to do, executors call tools and other agents, results flow back through shared memory, and a control loop decides whether to continue, branch, or stop. On a laptop, in a notebook, this runs as one Python process and the whole thing lives in memory. That picture breaks the moment the workflow matters. A real agentic task may run for minutes or hours, call dozens of paid LLM and tool endpoints, wait on a human approval, and span more requests than any one process should hold open. The process will be redeployed, preempted, or simply crash partway through. The question that defines this section is the one a notebook never has to ask: when the process holding a half-finished, twelve-step agent workflow dies after step seven, what happens to steps one through seven, and does step seven's tool call fire a second time when we recover?
1. The Frameworks and the Execution Models They Impose Beginner
The orchestration frameworks in wide use in 2025 differ less in capability than in the execution model they hand you, and that model shapes everything downstream. LangGraph models a multi-agent application as an explicit graph, a state machine whose nodes are agent or tool steps and whose edges are the control flow, with the shared state passed from node to node as a typed object. The graph is the unit you reason about, persist, and resume. AutoGen takes the conversational view: agents are participants in a structured conversation, and orchestration is the protocol that decides who speaks next, a model that maps cleanly onto the messaging of Section 32.6. CrewAI organizes work as a role-based crew, where each agent owns a role (researcher, writer, reviewer) and a process (sequential or hierarchical) routes tasks among them. The OpenAI Agents SDK, the productionized descendant of the Swarm experiment, keeps the surface deliberately small: agents, tools, and handoffs, where one agent transfers control to another with a lightweight function call.
None of these, on its own, tells you what happens to a half-finished run when the host dies, and that is the gap the next layer fills. A separate lineage, durable execution engines in the style of Temporal, treats the workflow itself as a durable, replayable program: the engine records every step's input and result in a history log, and on failure it reconstructs the workflow's exact state by replaying that history, so a function that ran for three hours and crashed at the end resumes from where it stopped rather than from the beginning. Increasingly the two layers compose, a graph or crew framework expresses the agent logic while a durable engine underneath provides the survival guarantees, and learning to see which layer owns which guarantee is the skill this section builds.
Choosing LangGraph over CrewAI over the Agents SDK is mostly a choice of how you want to express agent control flow: a graph, a crew, or a chain of handoffs. That choice is real but secondary. The property that decides whether your workflow is production-grade is orthogonal to it: does the engine persist the workflow's state durably after each step, and does it make every side-effecting tool call idempotent so recovery cannot double-act? A beautiful agent graph with no checkpointing is a toy; an ugly chain of handoffs running on a durable engine is a system. When you evaluate an orchestration stack, look past the authoring API and ask where the state lives when the process is gone.
2. The Hard Requirements That Separate a Toy From Production Intermediate
Four requirements turn a working demo into a workflow you can run with real money and real users attached. The first is durability and checkpointing: the workflow's state, which steps have completed, what each produced, what the shared memory holds, must be written to durable storage after each step so that a multi-step, long-running run survives a crash and can resume. This is exactly the checkpointing discipline of elastic training in Section 18.2, where a job that loses a worker reloads the last checkpoint instead of restarting from epoch zero. The difference is only what gets checkpointed: there, optimizer state and model shards; here, the agent's plan, intermediate results, and conversation history.
The second is exactly-once or, more precisely, idempotent execution of side effects. A retry is the engine's main recovery tool, and a retry is safe only if re-running a step that already completed does not act twice. Calling an LLM twice wastes money; calling a payment, email, or order-placement tool twice causes real harm. The standard fix is an idempotency key: a deterministic identifier for the intended effect (workflow id plus step name) that the tool, or a ledger in front of it, uses to recognize a duplicate and return the original result rather than acting again. The third requirement is human-in-the-loop pause and resume: a workflow must be able to suspend at an approval point, persist its full state, release its compute, and resume hours later when a human responds, which is impossible without the durability of the first requirement. The fourth is concurrency control over shared state, the problem of Section 32.7: when parallel agents read and write the same memory, the engine must serialize or version those writes so the state stays consistent, the same coordination problem distributed systems have always had.
Who: A platform engineer at a travel-booking company operating a customer-support agent.
Situation: A multi-step agent handled refund requests: verify the booking, compute the eligible amount, issue the refund through a payments API, then email the customer, a run that often paused for a human agent to approve refunds above a threshold.
Problem: Runs that paused for approval held a live process for up to an hour, and a routine deployment that recycled those processes lost the in-flight state. Worse, a naive restart re-issued refunds that had already been paid.
Dilemma: Keep the simple in-memory orchestrator and forbid deployments during business hours, an operational straitjacket, or move to a durable execution engine and rewrite the workflow against its checkpointing and idempotency model, a larger upfront change.
Decision: They moved the workflow onto a durable engine, checkpointing state after every step and wrapping the refund call with an idempotency key derived from the booking id.
How: The approval pause became a durable wait: the engine persisted the run, freed the worker, and rehydrated the run when the human responded. The refund tool consulted an idempotency ledger before charging.
Result: Deployments could roll any time; interrupted runs resumed at the exact step they left off; and replays of the refund step after a crash returned the original receipt instead of paying twice. Duplicate-refund incidents went to zero.
Lesson: Durability and idempotency are not features you add later. They are the difference between an agent you can deploy and one you must babysit.
3. The Orchestrator Is a Distributed Scheduler Intermediate
Strip away the LLM vocabulary and an orchestration engine is doing something this book has built several times: scheduling a directed acyclic graph of dependent tasks across machines, respecting dependencies, running independent steps in parallel, and recovering failed ones. That is the same problem a data pipeline scheduler solves in Section 26.2, where a workflow tool drives ML training stages as a DAG. The agent orchestrator differs only in what the nodes do: instead of a Spark job or a training step, a node issues an LLM call, a tool invocation, or a handoff to another agent. The scheduling logic, topological ordering, fan-out and fan-in for parallel agents, retry with backoff on a failed node, is recognizably the same machinery.
Seen this way, an agent workflow of $n$ steps where step $i$ has per-attempt failure probability $p_i$ has an expected number of executed attempts, under independent retries, of
$$\mathbb{E}[\text{attempts}] = \sum_{i=1}^{n} \frac{1}{1 - p_i},$$which grows without bound as any single step's reliability approaches zero, the formal reason a long agent chain with even modestly flaky tools needs durable retries to terminate at all. The orchestrator runs on the same infrastructure the rest of Part V and Part VII describe: it dispatches LLM calls to the serving fleet of Chapter 24 and runs its own worker processes on the cluster scheduler of Chapter 33 (cluster infrastructure and scheduling). The orchestrator is, in effect, a workload that schedules other workloads, an application-level scheduler sitting on top of the cluster-level one.
The durable checkpoint is one of this book's recurring primitives. It appears as periodic model snapshots that let a training job survive a lost worker (Section 18.2), and it underpins the recovery story of every long-running distributed job. Here it returns one level up the stack: the thing being checkpointed is no longer a tensor but an agent's plan, intermediate results, and conversation history. The recovery contract is identical, write durable state at a barrier, reload it after a crash, resume rather than restart, which is why an agent orchestration engine and an elastic training controller are, structurally, the same kind of system wearing different costumes.
4. Statefulness and the Deployment Surface Advanced
A stateless web request is the easy case: it arrives, it is served by whichever replica the load balancer picks, and it leaves no trace, so a fleet of identical replicas behind a balancer scales it trivially. An agent session is the opposite. It is a long-lived, stateful entity that accumulates plan, memory, and conversation history over many steps and many minutes, and the next step must reach the state the previous step produced. This is the same statefulness that complicates inference serving in Section 23.1, where the KV cache makes a generation request sticky to the replica that holds it, raised to the level of an entire multi-step workflow.
That statefulness reshapes deployment. You cannot treat agent workers as interchangeable stateless replicas, because a running session is bound to state that must outlive any single worker. The durable engine resolves the tension by separating the two: the workflow's state lives in durable storage, and the worker processes that advance it are stateless and replaceable, pulling the next ready step, executing it, and writing the result back. Any worker can pick up any step because the authoritative state is never in the worker; it is in the store. This is the same move that turns stateful training into elastic training, externalize the state, make the compute fungible, and it is what lets an orchestration engine scale workers up and down, survive preemption, and roll deployments without losing a single in-flight agent run.
A durable engine genuinely does not care how long a step waits. A workflow can call a "wait for human approval" step, persist itself, release every scrap of compute, and sit inert in durable storage for a week. When the approval finally arrives, a fresh worker rehydrates the run and continues as if no time had passed. The agent has no sense of the gap; from inside the workflow, the line after the pause runs immediately after the line before it. It is the closest thing in distributed systems to suspended animation, and the only cost while it sleeps is a few kilobytes in a database.
5. A Durable Orchestration Engine in Miniature Intermediate
The whole idea fits in one short, dependency-free program. The engine below runs a four-step agent workflow (plan, search, charge, email), checkpoints the workflow state to disk after each completed step, simulates a crash right after the paid charge step, and then starts a fresh process that resumes from the checkpoint. The single side-effecting tool, charge_card, carries an idempotency key and consults a ledger, so even though recovery brings the run back to life, the card is charged exactly once across both attempts. Note the two durability details that make this real: the checkpoint is written with an fsync then an atomic rename, so a crash can never leave a torn, half-written checkpoint, and the in-memory call counter is deliberately reset between attempts to prove that the no-double-charge guarantee comes from the durable ledger, not from a variable that a real crash would have wiped.
import json, os, hashlib
CKPT = "_ckpt_328.json"
LEDGER = "_ledger_328.json" # records which idempotency keys already executed
def load_json(path, default):
if os.path.exists(path):
with open(path) as f:
return json.load(f)
return default
def atomic_write(path, obj):
tmp = path + ".tmp"
with open(tmp, "w") as f:
json.dump(obj, f)
f.flush(); os.fsync(f.fileno()) # durable: survive a crash after return
os.replace(tmp, path) # atomic rename, no torn checkpoint
charge_count = {"n": 0} # stands in for a real external effect (a payment API)
def charge_card(amount, idem_key):
"""A tool with a real side effect. The idempotency key makes a retry a no-op."""
ledger = load_json(LEDGER, {})
if idem_key in ledger:
return ledger[idem_key] # already done: return the stored result
charge_count["n"] += 1 # the effect fires EXACTLY once per key
receipt = f"receipt-{hashlib.sha1(idem_key.encode()).hexdigest()[:8]}"
ledger[idem_key] = receipt
atomic_write(LEDGER, ledger)
return receipt
# the workflow: an ordered list of agent steps
def step_plan(state): state["plan"] = ["search", "charge", "email"]; return state
def step_search(state): state["results"] = ["doc-A", "doc-B"]; return state
def step_charge(state):
state["receipt"] = charge_card(42, idem_key=f"{state['run_id']}:charge")
return state
def step_email(state): state["sent"] = True; return state
STEPS = [("plan", step_plan), ("search", step_search),
("charge", step_charge), ("email", step_email)]
def run(run_id, crash_after=None):
state = load_json(CKPT, {"run_id": run_id, "done": []})
print(f" resume point: completed steps so far = {state['done']}")
for name, fn in STEPS:
if name in state["done"]:
continue # idempotent skip of finished work
state = fn(state)
state["done"].append(name)
atomic_write(CKPT, state) # durable barrier after every step
print(f" ran step '{name}' -> charge_card calls so far = {charge_count['n']}")
if crash_after == name:
print(f" *** simulated crash right after '{name}' (process dies) ***")
return None # state is already on disk
return state
for p in (CKPT, LEDGER):
if os.path.exists(p): os.remove(p)
print("ATTEMPT 1 (fresh start, will crash after the 'charge' step):")
run("run-7", crash_after="charge")
charge_count["n"] = 0 # new process after a crash: in-memory counter resets to 0
print("\nATTEMPT 2 (new process, resumes from the durable checkpoint):")
final = run("run-7")
print("\nFINAL STATE:", json.dumps(final, sort_keys=True))
print("times the card was actually charged across BOTH attempts:",
len(load_json(LEDGER, {})))
charge_card tool runs exactly once across the crash and the resume.ATTEMPT 1 (fresh start, will crash after the 'charge' step):
resume point: completed steps so far = []
ran step 'plan' -> charge_card calls so far = 0
ran step 'search' -> charge_card calls so far = 0
ran step 'charge' -> charge_card calls so far = 1
*** simulated crash right after 'charge' (process dies) ***
ATTEMPT 2 (new process, resumes from the durable checkpoint):
resume point: completed steps so far = ['plan', 'search', 'charge']
ran step 'email' -> charge_card calls so far = 0
FINAL STATE: {"done": ["plan", "search", "charge", "email"], "plan": ["search", "charge", "email"], "receipt": "receipt-63da962e", "results": ["doc-A", "doc-B"], "run_id": "run-7", "sent": true}
times the card was actually charged across BOTH attempts: 1
plan, search, and charge already done and runs only email. Its charge_card counter is zero because the ledger short-circuits the replayed effect, and the closing line confirms the card was charged exactly once across both runs. Crash, resume, no double-act.The result is the entire production story in miniature. The first process did real work, recorded it durably, and died. The second process, with a fresh and empty in-memory counter, picked up exactly where the first left off and finished the job without re-charging the customer. Everything a Temporal-style engine adds on top of this (distributed workers, a replicated history log, scalable timers for the human-in-the-loop waits, retry policies) is engineering around these same two invariants: persist state at every barrier, and make every side effect idempotent.
The forty lines of Code 32.8.1 are what a production engine gives you behind one decorator or one compiled-graph object. In LangGraph you build the graph and attach a checkpointer; the engine then persists state after every node and resumes a thread by its id, so crash-and-resume is configuration rather than code:
# LangGraph: durable state via a checkpointer, no hand-rolled checkpoint logic
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver
graph = StateGraph(AgentState)
graph.add_node("plan", step_plan); graph.add_node("charge", step_charge)
graph.add_edge(START, "plan"); graph.add_edge("plan", "charge"); graph.add_edge("charge", END)
app = graph.compile(checkpointer=SqliteSaver.from_conn_string("ckpt.db"))
# Each invocation under a thread_id checkpoints every step; a re-run resumes it.
app.invoke(inputs, config={"configurable": {"thread_id": "run-7"}})
checkpointer= argument. AutoGen offers comparable state save and load on its teams, and a Temporal worker promotes any workflow function to a durable, replayable one. The library owns the history log, the atomic state writes, the durable timers, and the retry policy; you own the agent logic and the idempotency keys on your tools.6. Choosing an Engine Beginner
The choice follows the requirements, not the marketing. If your workflows are short-lived, mostly in-memory, and tolerant of a restart from the top, a lightweight framework (the Agents SDK, a plain LangGraph run without a persistent checkpointer) keeps things simple and fast to build. As soon as a workflow runs long enough that a crash midway is expensive, pauses for human approval, or calls side-effecting tools where a double-act causes real harm, you need durable execution, and the decision becomes whether to use a graph framework's built-in checkpointer (LangGraph with a database-backed saver) or to run the agent logic on a dedicated durable engine (Temporal-style) that was built for exactly-once long-running workflows. Three questions settle most cases: how long does a run live and can it survive a deploy; what is the blast radius of a duplicated tool call; and does the workflow ever wait on something external, a human or a slow API, for longer than a process should stay alive. The more of these bind, the further you move from a clever script and toward a durable engine, the same progression from convenient-but-fragile to durable-but-deliberate that this book has traced in training and serving.
The convergence of agent frameworks and durable execution is one of the most active systems areas of this period. LangGraph (LangChain, 2024 onward) made persistent checkpointing and human-in-the-loop interrupts first-class, reframing an agent as a resumable state machine rather than a script. Temporal published patterns and an SDK posture for running AI agents as durable workflows, arguing that the exactly-once, replay-based execution model it built for microservice orchestration is precisely what long-running agents need, and competing durable-execution platforms (Restate, DBOS, Inngest) advanced similar claims with different tradeoffs in latency and state model. In parallel, Microsoft's AutoGen and the Semantic Kernel agent runtime, the OpenAI Agents SDK (the productionized successor to the Swarm handoff experiment), and CrewAI converged on a shared vocabulary of agents, tools, handoffs, and persisted sessions. The open research questions are sharp: what is the right consistency model for shared agent memory under concurrent writes (tying back to Section 32.7), how to make LLM and tool calls cheaply idempotent without a hand-written key for every tool, and how to checkpoint an agent's full context (including a large KV cache) cheaply enough to pause and resume at scale.
For each of the following, say whether a lightweight in-memory framework suffices or a durable execution engine is required, and name the specific requirement from Section 2 that forces the answer: (a) a research agent that summarizes ten web pages in one twenty-second run and is fine to restart from scratch on failure; (b) a procurement agent that places supplier orders and pauses for a manager's approval that can take a day; (c) a data-cleaning crew of three agents that only read and write a shared scratchpad with no external side effects but runs for two hours. Explain why distributing the workflow across more workers does not, by itself, give any of them crash recovery.
Start from Code 32.8.1. First, remove the idempotency ledger from charge_card (charge on every call) and change the simulated crash to happen before the checkpoint of the charge step is written rather than after, then run two attempts and report how many times the card is charged. Next, restore the ledger and confirm the count returns to one. Finally, add a second side-effecting tool, send_email, that is also idempotent, make the crash occur right after charge but before email, and verify across the crash that the email is sent exactly once. Explain precisely why the order of "do the effect" versus "write the checkpoint" matters, and which ordering the idempotency key makes safe.
Using the expected-attempts model $\mathbb{E}[\text{attempts}] = \sum_{i=1}^{n} 1/(1-p_i)$ from Section 3, consider a workflow of $n = 12$ steps where each step independently fails with probability $p = 0.1$ per attempt. Compute the expected total number of step executions, and the probability that the full twelve-step chain completes on a single pass with no retries at all. Then argue, from these two numbers, why a long agent workflow with flaky tools is effectively impossible to run reliably without durable retries, and how durable checkpointing changes the cost of a retry from "redo the whole chain" to "redo one step." How would the picture change if step reliability were $p = 0.4$ instead?