The question this answers
The process died mid-workflow. What do I need to have written down to continue safely?
With a step log that records intent before an effect and outcome after it: a resumed workflow performs each already-succeeded step zero further times, resolves each unresolved step by consulting the tool rather than guessing, and continues from the first genuinely incomplete step. It does not guarantee that no effect was duplicated before the log existed, and it does not guarantee that a compensating action undoes an effect — only that a compensating action is attempted.
Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.
A resuming process knows exactly what is in the step log, and nothing else. It does not know what the previous process was thinking, what was in its context, or what it was about to do. That is why the log has to record intentions and not only results: an intention is the only trace a dead process leaves of an action whose outcome it never learned.
A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.
Ask the right question
"How do we make the agent not crash" is the wrong question. Processes are evicted, containers are rescheduled, deploys roll, model calls hang past the wall-clock limit, and a workflow that spans a human approval will certainly outlive the process that started it. Crashing is normal.
The right question is: what is the smallest durable record from which this workflow can be resumed without repeating a side effect? That question has a precise answer, and it is much less than people expect. You do not need the conversation. You do not need the model’s reasoning. You need the sequence of steps, their identities, their statuses and their results — enough to rebuild a context and to know what has already happened in the world.
This is [[checkpoint-and-log]] applied to a workflow whose plan is generated at runtime. The classical version checkpoints state and logs the operations since the checkpoint; here the operations *are* the state, because the effects are external and cannot be re-derived.
The step log, and why the context window is not it
The temptation is to persist the conversation: serialise messages[] after every turn and reload it on resume. It looks like a checkpoint and it is not one, for two reasons that matter.
First, it does not record intentions. If the process died between emitting a tool call and receiving its result, the serialised conversation contains a call with no result — which is exactly the ambiguous state, and the reload gives it back to the model, which will call again. Persisting the conversation faithfully preserves the bug.
Second, it is not addressable. You cannot ask a serialised conversation "did step 4 execute?" without parsing prose. The step log is a table with statuses you can query, sweep, alert on and reconcile against the tool services. That queryability is most of its value in operation.
The right relationship is: the step log is the state; the context is derived from it. On resume you rebuild the context by replaying the log — which also means you can rebuild it differently, summarising old steps or dropping irrelevant ones, without losing any information the workflow depends on.
1type StepStatus =2 | 'intended' // we are about to act, or acted and never learned the outcome3 | 'succeeded'4 | 'failed' // the tool told us it did not happen5 | 'compensated' // a forward correcting action was applied6 | 'abandoned' // escalated; no further automatic action7 8interface StepRecord {9 workflowId: string10 index: number // position, not time — this is the identity11 tool: string12 args: unknown13 idempotencyKey: string // (workflowId, index, tool) — see agent-idempotency14 status: StepStatus15 result?: unknown16 reversible: boolean // decided at design time, not by the model17 compensation?: string // the forward action that corrects it, if any18}19 20// resume =21// read all steps for the workflow22// for each 'intended' step: ask the tool by key what happened23// rebuild context from succeeded steps24// continue from the first step that is neither succeeded nor compensatedFour ways to resolve an unresolved step, in order of preference
A step in intended is the interesting case: the effect may or may not have happened. There are exactly four things you can do about it, and choosing consciously per tool is the design work.
Query. If the tool supports a lookup by idempotency key or by a client-supplied reference, ask it. This is the only option that resolves the ambiguity rather than routing around it, and tools should be built or wrapped to support it. Where it exists, use it.
Re-drive idempotently. If the tool deduplicates on the key, call it again: a duplicate collapses into the original and you get the stored result. This is the workhorse and the reason [[agent-idempotency]] comes first in the module.
Compensate. If the effect may have happened and cannot be repeated safely, apply the correcting forward action — refund the charge, cancel the ticket, send a correction. This may be applied unnecessarily if the effect never occurred, so the compensation itself must tolerate that (cancel on a nonexistent booking should be a no-op, not an error).
Escalate. Mark the step abandoned and put it in a human queue. This is not a failure of the design; for genuinely unkeyable, genuinely irreversible effects it is the correct answer, and building the queue is cheaper than pretending the other three options apply.
| Tool property | Resolution | Residual risk |
|---|---|---|
| Supports lookup by keyprotocol | Query, then continue or re-drive | None beyond the lookup being wrong |
| Deduplicates on key, no lookupassumption | Re-drive with the same key | A key store expiry turns the re-drive into a duplicate |
| Effectful, keyless, reversibleassumption | Compensate, then re-drive | Compensation applied when nothing happened |
| Effectful, keyless, irreversibleprotocol | Escalate to a human | Latency, and a queue somebody must staff |
An agent workflow is a saga
Once a workflow performs effects across several services, it is a distributed transaction that nobody can roll back, which is precisely the situation [[sagas]] exists for. The mapping is exact: steps are the saga’s local transactions, and each step that must be undoable needs a compensating action.
And the compensation is a new forward action, not an undo — [[compensation-is-not-rollback]] is worth reading before designing any of this. There is no unsend for an email; there is a correction email, or nothing. There is no un-charge; there is a refund, which is a different transaction with its own record, its own fees and its own failure modes. Writing "compensation: reverse the charge" in a design document without naming the actual forward action is how compensations turn out not to exist at implementation time.
Saga design gives one structural instruction that applies directly and is the highest-leverage thing in this lesson: order the steps so the irreversible ones come last. In saga vocabulary this is the pivot — everything before it is compensatable, everything after is retriable-until-success. An agent that drafts, validates, records and then sends has one irreversible step at the end and a trivial recovery story. An agent that sends first and then records has an unrecoverable step at the beginning and no good options.
The plan being generated at runtime does not exempt you from this. It means the *tool definitions* carry the reversibility, and the orchestrator enforces ordering constraints on the plan — refusing, for instance, to execute an irreversible tool while reversible steps remain outstanding, or requiring approval at the pivot.
Checkpoint granularity, and when to just start over
Checkpointing is not free: every durable write is latency, cost and code. Too fine and every model turn becomes a transaction; too coarse and a crash discards expensive work.
The rule that resolves it: checkpoint at every side-effect boundary, and nowhere else for correctness. The intent-and-outcome pair around each effectful call is mandatory. Everything else — caching model outputs, storing intermediate reasoning, snapshotting a summarised context — is a *cost* optimisation, and should be built and reasoned about separately, because confusing the two leads to systems that persist a great deal and still cannot resume.
And the honest alternative: for a fully read-only workflow, do not resume at all — restart. If no step has an external effect, the entire recovery design collapses into "run it again", which is cheaper to build, cheaper to operate and impossible to get subtly wrong. The cost is repeated model spend, which is often less than the engineering. The moment a single effectful tool enters the workflow, that option closes — which is a good reason to keep the effectful part of a workflow small and at the end.
One more practical constraint: resumption must be bounded. A workflow that resumes, crashes, resumes and crashes forever is a poison message, and the answer is the ordinary one — an attempt counter, and a dead-letter destination after it is exceeded. [[poison-messages]] and [[dead-letter-queues]] apply unchanged, and a workflow with no such bound will eventually consume a queue.
Key points
- Processes will die mid-workflow; the design question is what durable record allows a safe continuation.
- The step log — identity, status, result, per step — is the state. The context window is derived from it, not the other way round.
- Serialising the conversation is not a checkpoint: it does not record intentions and cannot be queried.
- An
intendedrecord is what makes an unknown outcome recoverable; without it, "never called" and "called and lost" are indistinguishable. - Four ways to resolve an unresolved step: query the tool, re-drive idempotently, compensate, or escalate. Choose per tool at design time.
- A workflow with side effects is a saga: no rollback, only compensating forward actions.
- Order steps so the irreversible ones come last — the single highest-leverage structural choice.
- Checkpoint at every side-effect boundary; everything else is cost optimisation. A read-only workflow should restart, not resume.
The chain, answered
Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.
- • Create a durable workflow record when the task begins, before any work.
- • For each step: derive an idempotency key from workflow and step identity, write
intendedwith the arguments, then call the tool. - • On a result, write
succeededwith the result; on a definite failure from the tool, writefailed. - • On resume, load all step records for the workflow.
- • For each
intendedstep, resolve it by the resolution chosen for that tool: query, re-drive, compensate or escalate. - • Rebuild the model context by replaying succeeded steps, summarising freely — the log, not the context, is authoritative.
- • Continue from the first step that is neither succeeded nor compensated, incrementing an attempt counter.
- • If the attempt counter exceeds its bound, move the workflow to a dead-letter state for human inspection rather than resuming again.
- • The process dies between the intent write and the call, or between the call and the outcome write.
- • The step log write itself fails, so an effect happens with no record at all — the one case recovery cannot help with.
- • The tool has no lookup by key, so an
intendedstep cannot be resolved except by compensating or escalating. - • The tool’s idempotency key expires before the workflow resumes, so a re-drive duplicates the effect.
- • A compensation fails, leaving the workflow in a state that is neither completed nor corrected.
- • Two processes resume the same workflow concurrently because ownership was not fenced —
[[multi-agent-coordination]]. - • The workflow resumes indefinitely on a step that always fails, consuming budget without progressing.
- • Duplicate effect after resume: the customer is charged twice, hours apart, because the re-drive happened after the tool’s key retention expired. The operator sees two charges with the same idempotency key and no error in either service.
- • Workflow stuck in an unresolved step: a step sits in
intendedfor days because nobody built the resolution path for that tool. The observable is a step-age metric climbing with zero errors, and it is invisible unless that metric exists. - • Resume loop: a workflow crashes at the same step every time and is resumed indefinitely. The operator sees rising token spend, a flat completion rate, and one workflow id dominating the trace volume.
- • Half-compensated workflow: the charge was refunded but the confirmation email had already gone out and no correction was sent. The customer sees contradictory messages and the system reports the workflow as recovered.
- • Lost work on restart: an agent that persisted only the conversation reloads at step 7 with a tool call and no result, calls the tool again, and repeats every effect from that step onward.
- • Concurrent resume: two processes recover the same workflow and both continue from step 7, doubling every remaining effect. Each trace looks perfectly healthy on its own.
- • Resumption requires exclusive ownership of the workflow, or two recoveries will run concurrently — the step log makes recovery *possible*, ownership makes it *safe*.
- • Each unresolved step requires coordination with the tool that owns the effect, which is why a lookup endpoint is worth more than any amount of orchestrator-side cleverness.
- • Compensation coordinates across services and can itself fail, so compensations need their own idempotency keys and their own retry treatment.
- • A dead-letter transition is a coordination point with a human, and should carry enough context for that human to decide — which is a different and larger payload than the workflow needs internally.
- • Effects already performed remain performed; the log records what is known and marks what is not.
- • A workflow that cannot be resolved automatically stops rather than guessing, which is the correct behaviour and must be visible rather than silent.
- • Compensations restore business meaning, not prior state — the refund exists as a second transaction and both appear in the ledger.
- • Read-only progress is cheap to lose and should not be protected at the cost of complicating the effectful path.
- • Detect: alert on steps in
intendedbeyond a threshold, on workflow age, and on resume attempt counts above one. - • Contain: fence the workflow so only one process resumes it, and cap attempts so a poisoned workflow stops instead of looping.
- • Recover: resolve unresolved steps by the per-tool policy, rebuild the context from the log, and continue from the first incomplete step.
- • Reconcile: compare the step log against each tool service’s effect records on a schedule — the only way to find effects that happened without a record.
- • Verify: assert the workflow’s business outcome, not its status field. A workflow marked complete with a missing effect is exactly the failure this design exists to prevent.
- • Count and age of steps in
intended, which is the direct measure of unresolved ambiguity. - • Resume rate and attempts per workflow; anything above one attempt is normal, a rising distribution is not.
- • Compensation rate and compensation failure rate, tracked separately — a failing compensation is a silent, compounding problem.
- • Dead-lettered workflows with their reason, which is the queue that tells you which tool needs a lookup endpoint.
- • Cost per completed workflow, which distinguishes "resuming successfully" from "looping expensively".
- • Long-running workflows that will certainly outlive a process — anything with human approval, long tool calls, or many steps.
- • Any workflow with external side effects, where repeating a step has a real-world cost.
- • Environments with routine eviction: spot instances, autoscaled pods, serverless with duration caps, rolling deploys.
- • Workflows where partial progress is expensive to recreate, so restarting from zero is not economically acceptable.
- • Short, read-only workflows, where restart is strictly simpler and cheaper than resume.
- • Prototypes, where the machinery slows iteration and no real effects exist to protect.
- • When it is built without idempotency underneath — resumption without keys is a mechanism for repeating effects on a schedule.
- • When it is built without ownership — resumption without fencing means two processes recovering the same workflow, which is worse than not recovering at all.
- • Restart the whole workflow, for read-only work. Simpler, cheaper to build, impossible to get subtly wrong.
- • A durable workflow engine that provides the step log, timers, retries and resumption as infrastructure — the option to prefer whenever it fits.
- • Shrink the workflow: several short workflows chained by durable events have a much smaller recovery surface than one long one.
- • Move the effectful steps out of the agent entirely — the agent produces a plan, a deterministic executor performs it — so recovery is an ordinary job problem.
- • Require human confirmation at the pivot, which converts an automatic recovery decision into a review and often costs less than the alternative.
The process died at step 7. What did you write down?
| # | Tool | Class | Compensation | After resume |
|---|---|---|---|---|
| 1 | fetch_customer | read | — | re-run, harmless No record exists, so the resumed run starts from step 1 and does this again. |
| 2 | validate_address | read | — | re-run, harmless No record exists, so the resumed run starts from step 1 and does this again. |
| 3 | reserve_inventory | keyable | release_reservation | DONE AGAIN No record exists, so the resumed run starts from step 1 and does this again. |
| 4 | create_order | keyable | cancel_order | DONE AGAIN No record exists, so the resumed run starts from step 1 and does this again. |
| 5 | charge_card | keyable | refund — a new transaction, not a rollback | DONE AGAIN No record exists, so the resumed run starts from step 1 and does this again. |
| 6 | send_receipt_email | unkeyableirreversible | none — an apology email is not an undo | DONE AGAIN No record exists, so the resumed run starts from step 1 and does this again. |
| 7 | notify_warehouse | keyable | cancel_pick | unresolved Nothing was written down. The resumed run reaches this step again by replaying everything before it. |
| 8 | print_shipping_label | keyableirreversible | void_label, if the carrier allows it | not started |
| 9 | update_crm | idempotent | overwrite | not started |
| 10 | post_to_ledger | keyableirreversible | a compensating journal entry | not started |
| 11 | send_shipping_email | unkeyableirreversible | none | not started |
| 12 | mark_complete | idempotent | — | not started |
What people believe, and what is true
We persist the conversation, so we can resume.
A serialised conversation containing a call with no result reproduces the ambiguity rather than resolving it, and cannot be queried to ask whether a step executed. It is a cache, not a checkpoint.
On resume we can roll back the incomplete work.
There is no rollback across service boundaries. There are compensating forward actions, each of which is a new operation with its own failure modes — and for some effects there is no compensation at all.
Checkpoint after every model turn to be safe.
Model turns are not the boundary that matters; side effects are. Checkpointing every turn adds cost without adding recoverability, and can still miss the one write that mattered.
The workflow resumed successfully, so we are fine.
Resumption restores the process, not necessarily the business outcome. Verify the effects, not the status field — a workflow can resume cleanly around an effect that silently happened twice.
Retrying the workflow forever is safe because steps are idempotent.
Idempotent steps make repetition safe, not free. An unbounded resume loop consumes budget, occupies workers, and hides a permanently failing step. Bound the attempts and dead-letter the remainder.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
Write each step down before you do it and again after: identity, status, result. On resume, resolve anything left as "intended" by asking the tool, then continue. The conversation is not the checkpoint; the step log is.
Practical
Build the step table with intended / succeeded / failed / compensated / abandoned, keyed by (workflow, index, tool). Decide per tool which of the four resolutions applies, and check key retention against your longest pause. Order the plan so irreversible steps come last, bound resume attempts, and dead-letter the rest. Then monitor step age and resume attempts, because a stuck or looping workflow produces no errors.
Advanced
The structure is a saga whose plan is discovered at runtime, and that difference is the only genuinely new thing here. A classical saga knows its steps and compensations up front, so the compensation chain can be verified statically. An agent workflow does not, so the verification has to move into the tool definitions — reversibility and compensation are properties declared per tool, and the orchestrator enforces the pivot ordering over whatever plan the model produces. That is the practical form of the domain’s central rule for this module: the model proposes and deterministic code disposes. It also explains why "let the agent decide when to call the irreversible tool" is a design smell — it hands the ordering constraint that makes recovery tractable to the one component that cannot be held to it.
Apply it
- 🔧 Design the step record for one real workflow, including which tools are reversible and what each compensation actually is as a forward action.
- 🔧 Kill the process between the intent write and the tool call, and demonstrate that the resumed workflow does not duplicate the effect.
- ⚡ A workflow pauses overnight for approval. The tool’s idempotency keys expire after one hour. Describe what happens on resume and how you would fix it.
- ⚡ A compensation fails after its effect succeeded. Describe the resulting state and how an operator would find it.
- 💬 The process dies at step 7 of 12. What do you need on disk to continue safely?
- 💬 Why write the intent before making the call rather than after?
- 💬 A step is in
intendedand the tool has no lookup endpoint. What are your options? - 💬 Why is ordering the irreversible step last the highest-leverage change you can make?