11 KiB
Persisted Run/Resume Contract
Date: 2026-06-03
Status: contract clarification; current V1 mostly implemented
Related:
- Durable workflow runs and resume design
- Store transaction and locking boundary
- Durable run operations
- WorkflowOperationContext audit
Purpose
This spec sharpens the public and internal contract for persisted deployment runs. The older durable-run design describes the broad model and implemented V1. This document defines the invariants that future MCP, CLI, and HTTP frontends must preserve when starting, inspecting, tracing, and resuming stored runs.
The main rule: persisted resume is a human-in-the-loop interrupt mechanism, not a generic retry/recovery mechanism for dead external sources.
Contract Summary
run_deploymentcreates a durable run only after deployment validation passes.- Every started run receives a stable
run_id. - Stopped runs are persisted when execution returns
completed,failed, orinterrupted. resume_runonly resumes runs whose latest stored status isinterrupted.- Resume validates the pinned execution environment before mutating execution state.
- If pinned dependency validation fails, resume returns
blockedreadiness and does not consume the resume payload or write a new execution checkpoint. - If a live tool/source fails during execution, the run fails. It is not paused.
- Trace entries are never returned wholesale by default; callers must request a bounded range.
Data Model Contract
WorkflowRunRecord
WorkflowRunRecord is the durable summary for one started execution attempt.
Required invariants:
idis a safe run id matchingRUN_ID_PATTERN.statusis one ofinterrupted,completed, orfailed.resume_readinessis:readyfor interrupted runs that are currently resumable.blockedfor interrupted runs whose pinned dependency environment is currently invalid.not_applicablefor completed and failed runs.
environmentpins the exact deployment, root artifact, and child artifacts captured at run start.latest_checkpoint_idpoints to the latest stored execution checkpoint.diagnosticsstores control-plane diagnostics, especially blocked-resume dependency diagnostics.created_atis stable for the run id.updated_atchanges when the summary changes.
RunCheckpoint
RunCheckpoint is the stored execution state at a public stopped boundary.
Required invariants:
run_idmatches the owningWorkflowRunRecord.id.sequencestarts at1and increases monotonically per run.reasonmatches the stopped runtime status that caused checkpoint creation.stateis a validatedPersistedRunState, not an untyped dict.- V1 writes checkpoints only when public run operations return stopped states.
PinnedRunEnvironment
The pinned environment must be sufficient to resume without re-reading mutable deployment or artifact definitions:
deployment: exact deployment binding snapshot used at start.root_artifact: exact root artifact snapshot used at start.child_artifacts: exact saved child artifact snapshots used at start.
Changing or deleting a deployment after a run starts must not silently redirect or erase that run's resume environment.
Operation Contract
run_deployment
Input:
- deployment id
- workflow input
- optional trace range
Behavior:
- Load deployment and artifact from the configured artifact store.
- Resolve saved child artifact tree.
- Validate root and child dependency bindings against current available sources.
- If validation has blocking diagnostics:
- return
status="unrunnable" - return
run_id=None - do not create a run record
- do not write a checkpoint
- return
- If validation passes:
- create pinned environment
- execute workflow through
WorkflowRuntimeRunner - persist stopped run and checkpoint
- return compact run payload
Current implementation:
WorkflowRunApi.run_deployment()follows this contract.persist_stopped_run()rejects active runtime statuses.
inspect_run
Input:
- run id
Behavior:
- Load
WorkflowRunRecord. - Load latest checkpoint.
- Decode checkpoint state into
RunState. - Return compact summary:
- status
- run id
- resume readiness
- interrupt payload when present
- outcome/error/output when present
- diagnostics
- trace count
- Do not return trace entries.
Current implementation:
WorkflowRunApi.inspect_run()follows this contract.
read_run_trace
Input:
- run id
- trace range with
start >= 0andlimit > 0
Behavior:
- Validate trace range before store lookup.
- Load run and latest checkpoint.
- Return compact run summary plus the bounded trace slice.
- Return trace metadata:
trace_starttrace_limittrace_truncatedtrace_count
Current implementation:
WorkflowRunApi.read_run_trace()follows this contract.
list_runs
Returns paged compact summaries for stopped durable runs. The list payload contains run id, deployment id, artifact id/version, status, resume readiness, diagnostic count, and timestamps. It never returns trace entries, checkpoint state, runtime output, or pinned environment bodies.
resume_run
Input:
- run id
- resume payload
- resume outcome, default
submitted - optional trace range
Behavior:
- Load run and latest checkpoint.
- Reject if stored run status is not
interrupted. - Decode checkpoint state into
RunState. - Validate pinned environment against current available sources.
- If validation has blocking diagnostics:
- keep run status
interrupted - set
resume_readiness="blocked" - save updated run summary diagnostics
- do not write a new execution checkpoint
- do not apply resume payload
- keep run status
- If validation passes:
- restore saved child artifact tree from pinned environment
- resume workflow through
WorkflowRuntimeRunner - persist next stopped run/checkpoint with same
run_id - return compact run payload
Current implementation:
WorkflowRunApi.resume_run()follows this contract for stored interrupted runs and blocked dependency validation.- Same-process callers are serialized per
run_idaround the restore, dependency validation, runtime resume, and checkpoint write sequence. restore_interrupted_run()rejects non-interrupted statuses.mark_resume_blocked()updates run summary without writing a checkpoint.
Status Semantics
| Condition | Public result |
|---|---|
| Deployment dependencies invalid before start | status="unrunnable", no run id |
| Workflow reaches explicit interrupt | status="interrupted", resume_readiness="ready" |
| Interrupted run has broken pinned dependency before resume | status="interrupted", resume_readiness="blocked" |
| Workflow completes | status="completed", resume_readiness="not_applicable" |
| Workflow/runtime/tool fails during execution | status="failed", resume_readiness="not_applicable" |
unrunnable is not a stored run status. It is a pre-start response.
External Source Failure Rule
External source failure during execution is not a resumable pause.
Rationale:
- A tool may have performed a side effect before disconnecting or failing to return a response.
- Resuming from that point without explicit workflow semantics could duplicate external side effects.
- Retry/timeout fields exist in models but are not yet an implemented runtime policy.
Future retry support must explicitly define idempotency, unknown-side-effect behavior, and checkpoint boundaries.
Store Contract
RunStore must provide:
- save/get/list run records
- save/get/list checkpoints
- latest-checkpoint lookup
- safe run id validation
FileRunStore currently stores:
<store-root>/runs/<run-id>/run.json
<store-root>/runs/<run-id>/checkpoints/000001.json
<store-root>/runs/<run-id>/checkpoints/000002.json
Current limits:
WorkflowRunApi.resume_run()provides a process-local per-run critical section for one API/server process.FileRunStorelocks individual file writes only; it does not provide compare-and-swap or cross-process transactions.- The file store is appropriate for local/dev/single-process use.
- A multi-worker or cloud deployment still needs SQLite/Postgres or another transactional store before claiming strong concurrent resume safety.
Frontend Contract
MCP, CLI, and future HTTP surfaces should preserve the same operation semantics:
- Start:
run_deployment - List compact summaries:
list_runs - Inspect:
inspect_run - Debug trace:
read_run_trace - Continue explicit interrupt:
resume_run
Frontend-specific names may differ, but they must not change:
- status meanings
- trace range requirement
- blocked resume behavior
- run id stability
- pinned environment semantics
- no-implicit-pause rule for dead tools/sources
Current Gaps / Next Implementation Work
The core V1 behavior exists. Remaining implementation work should focus on hardening and frontend durability:
-
Required stores for durable API
- Implemented for process-local frontends through
wf_api.durable_context.require_workflow_stores()andwf_api.durable_context.durable_workflow_api(). WorkflowOperationContextstill allows optional stores for MCP test and compatibility paths.
- Implemented for process-local frontends through
-
Run listing and checkpoint listing
RunStorecan list runs, and the public workflow API exposes compact paged run listing. Checkpoint listing remains intentionally private for now.
-
Transactional backend
FileRunStoreis fine for local process use.- Multi-process/cloud use needs SQLite/Postgres or another transactional store to avoid lost writes and weak concurrent resume behavior.
-
Resume concurrency guard
- Implemented for same-process API callers through a per-
run_idasync critical section inWorkflowRunApi.resume_run(). - Cross-process protection remains part of the transactional backend gap.
- Implemented for same-process API callers through a per-
-
Protocol-native long-running progress
- MCP tasks/progress or an HTTP streaming/event surface should report active long-running runs without bloating stopped-run responses.
-
Retry/timeout policy
- Do not activate existing retry/timeout fields casually.
- Specify idempotency and unknown-side-effect semantics first.
Implementation Order
Recommended order after this contract:
- Contract regression tests now cover:
- non-interrupted
resume_runrejection - deleted deployment does not erase existing run inspection
- trace range validates before store lookup
- blocked resume writes no checkpoint (
tests/wf_mcp/test_saved_subgraphs.py)
- non-interrupted
- Add a required-store context/factory for durable API surfaces.
- Specify and implement a transactional run store backend when a cloud/API deployment is real.
- Add paged run listing/checkpoint listing only after the storage boundary is stable.
Non-Goals
- Do not redesign
RunState. - Do not checkpoint every node.
- Do not add automatic retry.
- Do not treat source death as an interrupt.
- Do not require MCP clients to reload dynamic tools to run workflows.
- Do not make HTTP/API design depend on
WfMcpService.