Checkpoints and Rollback
Persist each graph-state transition. When a worker crashes, its lease expires and another worker resumes from the latest checkpoint. Cloudflare Durable Objects can hold that state for hours or weeks, and propose-then-commit (Lesson 15) supplies a rollback plan per action. Post-action verification closes the loop. EU AI Act Article 14 requires effective human oversight for high-risk systems, which in practice means checkpoints an auditor can query, rollbacks you have rehearsed, and an audit trail that survives a deploy. The sharp failure mode: without idempotency keys and precondition checks, a retry after a transient failure can double-execute an approved action, and post-action verification is what catches it.
Learning Objectives
- Explain why an idempotency key alone cannot stop an overdraft, and add the precondition check that closes the gap.
- Choose in-band, compensating, or out-of-band rollback for a given action, and state when an action with no rollback needs stronger human review.
- Design post-action verification that re-reads the target state instead of trusting a 200 response.
- Trace the double-execute failure through a crash and resume, and order the status writes to avoid it.
- Run `main.py` and point out where its "committed"-before-execute ordering leaves a durability gap.
The Problem
Durable execution (Lesson 12) makes a crashed agent resumable. Propose-then-commit (Lesson 15) makes an approved action auditable. This lesson joins them and asks the awkward question: what happens when an approved action executes partway, crashes, and resumes? When does the rollback run, and against what state?
Real systems wire this up differently:
- LangGraph checkpoints every graph-state transition to PostgreSQL. On worker crash, the lease releases and another worker resumes at the latest checkpoint. Workflows pause on
interrupt(), which itself persists. - Cloudflare Durable Objects hold per-key state across hours or weeks. Co-locate the computation with the storage for the approved action.
- Microsoft Agent Framework exposes
Checkpointprimitives in the workflow API, and replay plus idempotency covers retries.
The combination that works in each case has four parts: an idempotency key (prevents double-execute), a precondition check (state is still what we approved against), a post-action verify (the side effect happened), and rollback when verify fails.
The Concept
Persist every transition
A graph-state transition is any step that moves the workflow from one named state to another. Naive implementations persist only at specific commit points. Production implementations persist each transition. A few extra writes cost little next to what you get back: replay can land anywhere, and lease recovery is precise.
Lease recovery
When a worker crashes, the workflow is not lost. The lease (a short-lived claim that this worker is executing this run) expires, and another worker picks up the latest checkpoint and resumes. This is what lets production systems survive rolling deploys without losing in-flight work.
Idempotency plus preconditions
Idempotency alone is not enough. Say a workflow is approved to "transfer $100 from A to B when balance > $1000." It commits, crashes mid-execution, and resumes. If the resume only checks the idempotency key, the transfer runs once, which is correct. Now suppose A's balance drops to $500 through a different workflow between the crash and the resume. The idempotency check still passes, but the precondition does not, and without a precondition check you ship an overdraft.
Consequential actions need both:
- Idempotency key: prevents double-execute.
- Precondition check: confirms the state is still consistent with what was approved.
Post-action verification
"The tool returned 200" is not verification. Real verification re-reads the target state and confirms the side effect happened. Patterns:
- Database update:
UPDATE ... RETURNING *, then assert the returned row matches intended state. - Email send: check the sent folder for the message ID after submission.
- File write: read the file back and hash it.
- API call: follow-up
GETon the target resource.
If verify fails, the workflow is in a known-bad state and rollback engages.
Rollback plans
Each consequential action in propose-then-commit (Lesson 15) carries a rollback plan. There are three types:
- In-band rollback: reverse the side effect directly (
DELETEafterINSERT, a correction email after a send). - Compensating transaction: a new action that neutralizes the original (the standard SAGA pattern).
- Out-of-band rollback: alert a human, pause the workflow, and leave the bad state for investigation.
If an action cannot be undone, the proposal has to say so. Actions with no rollback need stronger HITL at commit time (Lesson 15 challenge-and-response).
EU AI Act Article 14, operationally
Article 14 requires "effective human oversight" for high-risk systems. Implementers read that as:
- Checkpoints an auditor can query.
- Rollbacks that have been rehearsed, meaning tested end-to-end at least once.
- An audit trail that survives a deploy, so the checkpoint backend cannot be ephemeral.
- Failed verifications that raise an alert instead of landing silently in a log.
A workflow that crashes mid-commit, resumes, and completes the side effect with no verify and rollback path does not pass this test.
The sharp failure mode: the double-execute
The most common production incident in this space goes like this:
- Action approved, idempotency key k.
- Commit starts, executes, returns 200.
- Workflow crashes before persisting the "committed" status.
- Workflow resumes, sees "approved but not committed", and re-executes.
- The side effect fires twice.
The mitigation: persist an "in-flight" intent before execution, execute with an idempotency key, and mark "committed" only after post-action verification succeeds. If the action fires and the status write fails, you know to verify and re-fire if needed. If the status write succeeds and the action fails, you verify and fire once via the recovery path.
Use It
code/main.py implements a checkpointed workflow with idempotency, preconditions, verify, and rollback. The driver simulates four scenarios: clean run, retry after crash (idempotency catches), precondition fail (workflow aborts without firing), verify fail (rollback fires).
Ship It
outputs/skill-rollback-rehearsal.md designs a rollback-rehearsal test for a proposed workflow and audits the checkpoint backend for audit-trail persistence.
I'd run main.py first and read the four scenario outputs before the prose. Scenario 2 crashes after the transfer and then retries the same txid. The retry returns idempotent-skip, and balance_A only drops by 100 (1400 to 1300), so tx-002 ran once, not twice. Scenario 4 compares the balances before and after a forced verify failure, and they come back equal.
The part I'd read slowly is the order of writes in run_transfer. The code saves committed to the checkpoint before it calls persist_transfer, and only promotes the record to verified after the read-back. That is not the mitigation described above, which marks "committed" after verification. Here committed is doing the job of the in-flight intent. It works, but there is a gap, and the comment in the code says so: a crash between the save and persist_transfer leaves a committed record for a transfer that never ran, and the retry skips it. Closing that gap means carrying the idempotency key into the destination write or checking the destination before trusting the marker.
Two smaller things I'd notice. The precondition checks the balance that would remain after the transfer against min_balance, which is a different gate from the "balance > $1000" wording in the example. It also runs only after the terminal-state lookup, so a retry of a finished txid never reaches it. And Checkpoint.save writes a temp file, fsyncs, then renames, so a crash mid-write leaves the previous record intact. That is a small version of the durable backend the Article 14 reading above calls for.
For Exercise 2 I'd expect the duplicate to show up once the committed save moves past the injected crash (after the verify step). The crash then lands before any record exists, the retry sees status new, the precondition still passes (1300 - 100 >= 200), and the transfer fires a second time.
Exercises
-
Run
code/main.py. Verify the four scenarios. For the crash-during-commit case, confirm the action fires once across retries. -
Modify the "mark as done first, then do it" pattern so the status write fires after the action. Rerun the crash scenario. Measure how many duplicate actions fire.
-
Design a rollback plan for a specific production action (e.g., "post to a Slack channel"). Classify as in-band, compensating, or out-of-band. Justify the choice.
-
Take one workflow you know. Identify every state transition. Mark each with a durability requirement (persist / do not persist). Count the ones you are currently not persisting.
-
Rehearsed-rollback test: design an end-to-end test that runs a real workflow, crashes it, and confirms the rollback path fires. What does the test assert?
Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Checkpoint | "Save point" | Every graph-state transition persists to a durable store |
| Lease | "Worker claim" | Short-lived claim that a worker is executing a run; expires on crash |
| Precondition | "State gate" | Assertion that the state is still consistent with the approved action |
| Post-action verify | "Re-read check" | Confirm the side effect actually happened in the target system |
| In-band rollback | "Direct undo" | Reverse the side effect with the inverse operation |
| Compensating transaction | "SAGA undo" | A new action that neutralizes the original |
| Mark-as-done-first | "Status write order" | Persist the committed status before returning from commit |
| Article 14 | "EU AI Act human oversight" | Operational: queryable checkpoints, rehearsed rollbacks, auditable trail |
Further Reading
- Microsoft Agent Framework: Checkpointing and HITL. Checkpoint primitives and lease recovery.
- Cloudflare Agents: Human in the loop. Durable Objects as a state substrate.
- EU AI Act, Article 14: Human oversight. The regulatory baseline.
- Anthropic: Measuring agent autonomy in practice. Reliability framing for long-horizon workflows.
- Anthropic: Claude Code Agent SDK, agent loop. Workflow shape for Claude Code Routines.
Code
- main.py (python, 192 lines)