ADR-008: Caller-Supplied Checkpoint Function for Workflow Resume¶
Date: 2026-06-18 Status: Accepted Deciders: ExecutionKit core team Technical Story: Workflow runs can be interrupted by process crashes, container evictions, or explicit cancellation. Users needed a way to resume from the last completed batch without re-executing costly LLM steps.
Context and Problem Statement¶
Workflow executes steps in dependency-ordered batches. A run involving many
LLM-backed steps can take minutes and consume significant token budget. Without
a resume mechanism, any interruption restarts the entire workflow from scratch.
The design needed to satisfy three constraints simultaneously. First, the library imposes zero runtime dependencies — no embedded database, file system abstraction, or serialisation library was acceptable. Second, the storage medium is caller-defined: some callers write checkpoints to JSON files, others to a database row, others to in-memory state during tests. Third, the checkpoint representation must be serialisation-friendly so any backend can round-trip it without coupling to ExecutionKit internals.
Decision Drivers¶
- Zero runtime dependencies — no persistence mechanism may be bundled.
- Callers choose their own storage backend; the library must not restrict that choice.
- Checkpoint data must be expressible in plain Python so JSON, pickle, and database backends all work without adaptation.
- Resume must be unambiguous: a step is skipped if and only if its output is already recorded in the checkpoint.
- The public API surface must remain small enough to test exhaustively with synchronous in-memory callbacks.
Considered Options¶
- Option A: Caller-supplied
checkpoint_fncalled after each batch; plain-Pythonto_dict/from_dictonWorkflowCheckpoint - Option B: Built-in file-based persistence with a configurable path
- Option C: No checkpointing support
Decision Outcome¶
Chosen option: Option A (caller-supplied checkpoint function), because it keeps persistence entirely outside the library while giving callers full control over when, where, and how checkpoints are stored.
After each batch of completed steps, Workflow.run calls
checkpoint_fn(WorkflowCheckpoint(...)) if one was provided. The caller
persists the checkpoint however it chooses. On the next run, the caller passes
resume_from=WorkflowCheckpoint.from_dict(saved_data). Steps whose names
already appear in checkpoint.outputs are skipped; accumulated outputs and
token usage are restored verbatim.
WorkflowCheckpoint.to_dict() produces a plain-Python dict with only
int, str, and nested dict values — no ExecutionKit types. from_dict
reconstructs the dataclass from that same dict. The caller is responsible for
any additional serialisation (e.g., json.dumps).
Positive Consequences¶
- Zero persistence dependency — the library ships nothing that writes to disk or a database.
- Any storage backend works: JSON file, SQLite row, Redis key, in-memory dict.
WorkflowCheckpointis a frozen dataclass; callers can safely hold references across async boundaries without defensive copies.- Testing is straightforward — pass a list-appending lambda as
checkpoint_fnand inspect the recorded checkpoints.
Negative Consequences¶
- The caller must implement persistence boilerplate. There is no built-in helper for common cases like writing to a JSON file.
- If
checkpoint_fnitself raises, the exception propagates out ofWorkflow.run. Callers must guard against storage failures inside the callback if they want the workflow to continue despite a failed checkpoint.
Pros and Cons of the Options¶
Option A: Caller-supplied checkpoint_fn¶
- Good: No storage dependency enters the library.
- Good: Works with any serialisation backend without adaptation.
- Good: Easily testable with a synchronous in-memory callback.
- Bad: Callers must write persistence boilerplate themselves.
Option B: Built-in file-based persistence¶
- Good: Reduces boilerplate for the common case of single-process runs.
- Bad: Introduces an implicit dependency on the file system — incompatible with serverless environments, containers with read-only filesystems, or distributed workers.
- Bad: File paths and naming conventions become part of the public API, creating migration burdens.
Option C: No checkpointing support¶
- Good: No added complexity.
- Bad: Every interruption restarts the entire workflow, wasting LLM budget and user time on long-running pipelines.