Agentic Distributed Systems

Resuming a Workflow That Died Halfway Through

The process holding a twelve-step workflow is evicted at step seven. The question is not whether it crashed — it will — but what the smallest durable record is from which the work can continue without repeating a side effect. An agent workflow with side effects is a saga: there is no rollback, only compensation, and the recovery design has to be built on that.

▶ Run the lab

The question this answers

The question

The process died mid-workflow. What do I need to have written down to continue safely?

The guarantee — the property claimed, and its scope

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.

What a node knows — observation versus inference

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.

What guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
recoverycheckpointingsagasdurabilityresume

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 outcome
3 | 'succeeded'
4 | 'failed' // the tool told us it did not happen
5 | 'compensated' // a forward correcting action was applied
6 | 'abandoned' // escalated; no further automatic action
7
8interface StepRecord {
9 workflowId: string
10 index: number // position, not time — this is the identity
11 tool: string
12 args: unknown
13 idempotencyKey: string // (workflowId, index, tool) — see agent-idempotency
14 status: StepStatus
15 result?: unknown
16 reversible: boolean // decided at design time, not by the model
17 compensation?: string // the forward action that corrects it, if any
18}
19
20// resume =
21// read all steps for the workflow
22// for each 'intended' step: ask the tool by key what happened
23// rebuild context from succeeded steps
24// continue from the first step that is neither succeeded nor compensated
The minimum durable record

Four 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 propertyResolutionResidual risk
Supports lookup by keyprotocolQuery, then continue or re-driveNone beyond the lookup being wrong
Deduplicates on key, no lookupassumptionRe-drive with the same keyA key store expiry turns the re-drive into a duplicate
Effectful, keyless, reversibleassumptionCompensate, then re-driveCompensation applied when nothing happened
Effectful, keyless, irreversibleprotocolEscalate to a humanLatency, and a queue somebody must staff
Choosing a resolution per tool

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.

Crash at the pivot, and the two possible resumptionssimplified
Orchestrator is down over this spanOrchestratorStep logPayment servicecharge_card, key=wf:7:charge_card: deliveredcharge_card, key=wf:7:charge_cardresult: sent, never arrives — dropped in flightresultdropped — never arriveslookup by key: deliveredlookup by keystep 6 succeeded (reversible) (write) at t=1step 6 succeeded (reversible)step 7 intended: charge_card (write) at t=3step 7 intended: charge_cardcharge committed (write) at t=6charge committedprocess evicted (crash) at t=7process evictednew process reads log (recover) at t=12new process reads logquery by key → charge exists → mark succeeded (decide) at t=15query by key → charge exists → mark succeededt=1time →t=15
delivereddelayed (dashed, long)duplicated (×2)dropped — stops short, never arriveswritecrashrecoverdecide
The `intended` record written at t=3 is the entire reason recovery is possible. Without it, the new process reads a log ending at step 6 and re-runs step 7 — charging twice. The record does not tell you what happened; it tells you what to ask about.

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 intended record 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.

How it works
  • 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 intended with the arguments, then call the tool.
  • On a result, write succeeded with the result; on a definite failure from the tool, write failed.
  • On resume, load all step records for the workflow.
  • For each intended step, 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.
What can fail at the boundary
  • 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 intended step 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.
How it fails — what an operator sees
  • 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 intended for 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.
Where coordination is required
  • 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.
What still holds under failure
  • 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.
How it recovers
  • Detect: alert on steps in intended beyond 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.
How you would know
  • 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".
When it helps
  • 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.
When it hurts
  • 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.
Simpler alternatives
  • 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?

The process died at step 7. What did you write down?
Not whether it crashes — it will. The question is the smallest durable record from which the work continues without repeating a side effect.
simplifiedA twelve-step plan with one crash point. Real workflows also lose steps to plans that change on resume, since the plan is produced at runtime and the resumed run may generate a different one.
what is persisted
steps re-executed
6
duplicated side effects
5
unresolved at resume
1, with no record
irreversible steps already done
1
#ToolClassCompensationAfter resume
1fetch_customerreadre-run, harmless
No record exists, so the resumed run starts from step 1 and does this again.
2validate_addressreadre-run, harmless
No record exists, so the resumed run starts from step 1 and does this again.
3reserve_inventorykeyablerelease_reservationDONE AGAIN
No record exists, so the resumed run starts from step 1 and does this again.
4create_orderkeyablecancel_orderDONE AGAIN
No record exists, so the resumed run starts from step 1 and does this again.
5charge_cardkeyablerefund — a new transaction, not a rollbackDONE AGAIN
No record exists, so the resumed run starts from step 1 and does this again.
6send_receipt_emailunkeyableirreversiblenone — an apology email is not an undoDONE AGAIN
No record exists, so the resumed run starts from step 1 and does this again.
7notify_warehousekeyablecancel_pickunresolved
Nothing was written down. The resumed run reaches this step again by replaying everything before it.
8print_shipping_labelkeyableirreversiblevoid_label, if the carrier allows itnot started
9update_crmidempotentoverwritenot started
10post_to_ledgerkeyableirreversiblea compensating journal entrynot started
11send_shipping_emailunkeyableirreversiblenonenot started
12mark_completeidempotentnot started
Nothing was persisted, so resume means starting over. Every effectful step before the crash runs a second time — 4 of them here, including charges and emails that have no undo. This is the case people mean when they say an agent "lost its place". This is the domain's opening problem wearing different clothes: a timeout tells you nothing about whether the work happened. The step log does not remove the ambiguity; it preserves enough evidence that somebody can go and resolve it afterwards. One more thing this plan gets wrong: send_receipt_email is irreversible and 1 reversible effectful step come after it. Order the plan so irreversible steps come last, and enforce that ordering in the orchestrator — over whatever plan the model produced. Letting the planner decide when to call the irreversible tool hands the one constraint that makes recovery tractable to the one component that cannot be held to it.

What people believe, and what is true

Claim

We persist the conversation, so we can resume.

Reality

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.

Claim

On resume we can roll back the incomplete work.

Reality

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.

Claim

Checkpoint after every model turn to be safe.

Reality

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.

Claim

The workflow resumed successfully, so we are fine.

Reality

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.

Claim

Retrying the workflow forever is safe because steps are idempotent.

Reality

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

Build it, then break 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.
Reason about this
  • 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.
Interview questions
  • 💬 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 intended and the tool has no lookup endpoint. What are your options?
  • 💬 Why is ordering the irreversible step last the highest-leverage change you can make?