Persisted formats — V1 reference
This page describes the filesystem run store. Mongo/cloud uses equivalent documents and collections rather than reproducing the directory layout. The current Go structs and constants under pkg/store/ are the authoritative schema; readers must tolerate additive fields and event types.
Filesystem layout
<store-root>/runs/<run_id>/
run.json
events.jsonl
run.log
user_messages.jsonl
.pid
artifacts/<node_id>/<version>.json
interactions/<interaction_id>.json
attachments/<name>/<original_filename>
attachments/<name>/meta.json
plans/<sequence>.json
tools/<tool_use_id>/input
tools/<tool_use_id>/outputEntries are created only when the feature is used. Files and directories are private by default (0600 / 0700). Large tool inputs and outputs are kept in tools/; their events carry previews and references instead of forcing the whole body into events.jsonl.
Detached-runner PID
With ITERION_RUNS_DETACHED=1, a server-launched run executes in a managed iterion run --background process. .pid contains one decimal PID and is removed on normal exit. On restart, the server uses the PID plus run locking and event freshness to reattach or reconcile an orphan. Absence is valid for in-process and historical runs.
run.json (format_version: 1)
run.json contains the run's identity, source, lifecycle, effective launch metadata, worktree/finalization state, cloud ownership/queue metadata, attachments, artifact index, and checkpoint. A deliberately abbreviated example is:
{
"format_version": 1,
"id": "01938f4c-78b3-7d2e-bc44-5e6a7b8c9d0e",
"name": "kind-otter",
"workflow_name": "review",
"workflow_hash": "<sha256>",
"file_path": "/repo/review.bot",
"bundle_path": "",
"preset": "strict",
"status": "running",
"inputs": { "scope": "pkg/runtime" },
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:01:00Z",
"finished_at": null,
"error": "",
"checkpoint": { "node_id": "inspect" },
"artifact_index": { "inspect": 2 }
}The complete shape is store.Run. omitempty fields from local/cloud, worktree, webhook, secret, attachment, budget, and Studio features can legitimately be absent.
failure_code (ADR-095) is the machine-readable classification of error on a failure status (failed / failed_resumable / cancelled), written atomically with the status and cleared by every transition to a non-failure status. Absent/empty means UNKNOWN (legacy rows, unclassified writers) — never "no failure". The vocabulary is open-world: readers must accept codes they do not know (see store.FailureCode) — and it is open in practice, not just in principle: a workflow's own fail <name>: declaration supplies the code and the error text (DSL), so a deployment sees whatever vocabulary its bots define. The engine's constants remain the fallback: an untyped -> fail still writes FAIL_NODE / "workflow reached fail node". A fail node that declares resumable: true writes its code on failed_resumable instead of failed; the code says WHY, never whether the run may continue.
outcome_seq counts the run's terminal EPISODES: it increments on every TRANSITION into finished / failed / failed_resumable / cancelled (never on a same-status rewrite — a drain's re-flip or the publisher's resume rollback must not invent an episode). (run_id, outcome_seq) is the stable per-episode key an outcome consumer claims on; the event-derived id cannot serve (second-truncated timestamps collide, and every save refreshes updated_at). continuation_state says who owns the run's future after a park: redelivery_pending (the RUNNER promoted it at an actual queue Nak), retry_armed (a quota-window retry exists — promotion happens when ScheduleRunRetry arms, demotion to final when it abandons), or final (nobody — acting is the consumer's decision). Empty = UNKNOWN, which a consumer must never read as final. Engine writers state only the CAUSE (failure_code); continuation is runner/server-side knowledge. Both fields are store-owned bookkeeping: full-document saves can neither rewind the counter nor resurrect a stale continuation. Deploy-order constraint: an old binary's SaveRun drops the fields entirely (rewinding the episode key for its consumers), so a release adding an outcome_seq CONSUMER must ship only after the fleet fully carries the writer.
nodes_served maps each LLM node's id to the last (backend, model) that served it (model is the provider-reported effective model; declared_model is what the node asked for). It is the run-record half of making a finished run self-describing without replaying events.jsonl. Empty for legacy runs and for workflows that never delegated. The event stream is the full history (delegate_started carries declared_model; delegate_finished / delegate_error add effective_model / context_window / max_output_tokens; model_drift fires when the two model fields name different models).
fingerprint on each entry is the backend's provider-routing label for that session — anthropic-oauth, anthropic-direct, anthropic-env, or facade:<base url>. An Anthropic-shaped facade answers a claude-* id with whatever it aliases it to, so the two model fields agree and model_drift stays silent; the fingerprint is the only evidence, and model_served_via_facade is the event half of it. It names the route that SERVED on a success and the one ATTEMPTED on a failure that still reported a model — the same reading as model beside it, and last-write-wins with it. Empty means the backend reported none (claw and the CLI-agent backends report no fingerprint), i.e. "route unknown", never "not a facade". The base URL is stripped of userinfo/query/fragment before it is recorded — it is operator input, so it can embed a credential, and this record is readable by anyone with run-read access.
Run statuses
| Status | Meaning | Resume posture |
|---|---|---|
queued | Cloud message accepted but not yet claimed by a runner. | Internal queue state. |
running | An engine owns or is expected to own the run. | Not normally resumable; stale local runs can be reconciled. |
paused_waiting_human | A durable interaction is waiting for answers. | Resume with answers. |
paused_operator | Soft operator or daily-spend-cap pause with no pending human form; also the successful post-rewind state. | Runtime-restorable checkpoint; a rewound run still requires an explicit resume. See resume. |
finished | Reached done. | Terminal. |
failed | Intentional fail or failure without resumable persisted state. | Terminal (no auto-resume), but the checkpoint is preserved so an explicit rewind can recover it; runs failed before that preservation have none and stay unrecoverable. |
failed_resumable | Failure with a restart checkpoint, or an entry restart marker. | Resume without answers. |
cancelled | User interruption with state preserved when possible. | Resume without answers. |
failed_resumable and cancelled are terminal for polling purposes even though an explicit resume can transition them back to running.
Checkpoint
The checkpoint embedded in run.json is the source of truth for resume. events.jsonl is an audit/observation stream and is never replayed to rebuild execution state. A checkpoint may be present while a run is still running because it is saved best-effort after successful node boundaries. A status transition never destroys it (ADR-095): only DeleteRun and the rewind machinery remove one, and resumability is decided by Status alone — a terminal run keeps its checkpoint (iterion fork reads a finished parent's).
{
"node_id": "review",
"interaction_id": "run_123_review",
"outputs": { "inspect": { "summary": "..." } },
"loop_counters": { "retry": 2 },
"round_robin_counters": { "alternate": 1 },
"loop_previous_output": {},
"loop_current_output": {},
"artifact_versions": { "inspect": 3 },
"artifacts": {}, // inspection equals outputs.inspect, so its body is omitted
"artifact_owners": { "inspection": "inspect" },
"artifacts_known": true,
"artifact_revisions": {
"inspection": { "node_id": "inspect", "version": 2 }
},
"artifact_revisions_known": true,
"selected_incoming": {
"gate": [{ "from": "validate", "to": "gate" }]
},
"vars": { "scope": "pkg/runtime" },
"interaction_questions": { "approved": "Ship?" },
"backend_session_id": "session-id",
"backend_session_fingerprint": "anthropic-oauth",
"backend_name": "claude_code",
"backend_conversation": null,
"backend_pending_tool_use_id": "",
"node_sessions": {
"writer-or-named-session-slot": {
"backend": "claude_code",
"session_id": "sess-…",
"fingerprint": "…",
"state_ref": "ulid"
}
},
"backend_session_state_ref": "",
"node_attempts": { "review": { "RATE_LIMITED": 1 } },
"recovery_pause": true,
"recovery_code": "AUTH_FAILED",
"budget_tokens_used": 42000,
"budget_cost_usd": 1.25,
"budget_iterations_used": 7,
"budget_elapsed_ns": 90000000000,
"cost_usd_total": 1.25
}node_sessions keys default to the node id. A session: persist node may declare session_slot: <name> so serial nodes share one durable conversation; the value shape and backend-session blob storage are unchanged. Historical claw envelopes are normalized when loaded: tool results without a preceding call, calls without a later result, and provider-specific reasoning blocks are removed block-by-block, with empty messages discarded. This compatibility repair does not rewrite the stored blob in place; the next successful session checkpoint persists the normalized conversation.
The loop snapshots preserve loop.<name>.previous_output; backend fields preserve mid-agent interaction; recovery counters keep retry ceilings honest; budget fields prevent resume from granting a fresh allowance. The union of artifact_owners and artifacts is the authoritative logical publish-name snapshot. artifact_owners is the complete catalog: when a logical value is identical to its producer's outputs entry, the body is omitted from artifacts and reconstructed from that owner on resume. This keeps large published outputs from appearing twice in Mongo's size-limited run document. Historical aliases whose value differs from the producer's newest output stay behind their immutable revision with value_from_revision: true; unverified revisions and ownerless values remain explicit in artifacts. artifact_owners also keeps invalidation ownership when report mode cannot verify a blob; artifact_revisions independently records physical provenance. The corresponding *_known markers distinguish an intentionally empty current snapshot from a checkpoint written by an older binary, which is rebuilt conservatively from outputs without inventing provenance. Parallel branch checkpoints keep their artifacts bodies expanded for V1 mixed-version compatibility. Older runners do not understand artifact_owners, so compacting a completed branch would make its published values disappear when that runner resumes it. An artifact revision marked "unverified": true retains only its producer binding after report mode could not read the physical body. It is excluded from newly written dependency contracts until a later resume verifies it. backend_session_fingerprint is the provider fingerprint of backend_session_id, checkpointed beside it because the id alone is not usable: a session: fork resume drops a session whose parent provider it cannot identify (cross-provider thinking blocks 400). Absent on checkpoints written before the field existed, which reads as "unknown" — the same conservative outcome as before. recovery_pause / recovery_code mark a pause the recovery dispatcher wrote for a node whose execution failed (AUTH_FAILED, BUDGET_EXCEEDED, …): the node still owes its work, so the answer that resumes the run is an acknowledgement, never the node's output, and resume re-executes the node (the interaction is written with kind: "recovery"). Both are cleared with the pause pointer once a resume claims the run. selected_incoming is the set of incoming edges routing actually fired into each node for its current visit, so a resume of that node applies the same with-mappings (issue #484). Missing fields on historical checkpoints take their zero-value compatibility behavior — for selected_incoming that means the pre-#484 fallback of merging every incoming edge whose source has produced output.
Bot code provenance and delegated workers
New runs may carry bot_origin, the host-stamped identity of the code that supplied their workflow. It is deliberately separate from source (the ticket/schedule that triggered execution) and bot_source_tenant (the stored bot resolution tier). The record can include a project/repository identity, commit, tree hash, package and repository-relative workflow path. Local runs may additionally retain the resolved repository root; assistant context never exposes that host path.
A worker launched from a failed run carries delegation with source_run_id, kind, episode_fingerprint and attempt. The tuple is the auditable link and the admission key. Iterion chooses a deterministic run id per attempt, so concurrent replicas racing the same failure episode converge on the run store's unique-create operation; a terminal worker permits a later attempt, while a non-terminal worker blocks another.
Installed bundles persist their origin beside, not inside, the copied bundle under .botz/.origins/<name>.json. Keeping the sidecar outside the bundle means provenance does not perturb its logical content hash.
Assistant run watches
Assistant watches are deliberately outside run.json: they connect two runs and have their own lifecycle. Local mode atomically replaces <store>/assistant-run-watches.json, containing watches and episodes maps. Cloud mode uses the assistant_run_watches and assistant_run_watch_episodes Mongo collections.
A watch is active, resolved or stopped. An episode is independently pending, processing, done or blocked; processing carries a bounded lease so a crashed claimant can be retried. (watch_id, outcome_event_id) is unique, as is one active (tenant_id, owner_id, target_run_id) watch. These separate enums are intentional: an assistant being temporarily busy must not turn its durable watch into a terminal state. The watch target is a tree root. tree_tracking_started_at is the one-time descendant replay floor, and observations stores a monotonic event cursor per root/descendant. Each episode keeps target_run_id for routing and observed_run_id for the concrete run that produced the outcome. Legacy watches retain last_observed_event_seq as the root cursor and migrate lazily. The default watched kind is run.failed; host-created delegation watches can also select run.finished and run.cancelled to deliver a worker's terminal result before resolving the watch.
Assistant missions
Assistant missions are a separate durable authority ledger. Local mode stores <store>/assistant-missions.json behind an OS file lock and atomic rename; cloud mode uses the assistant_missions Mongo collection. Both enforce a permanent unique (tenant_id, operator_id, invocation_key) binding and at most one non-terminal mission per (tenant_id, target_run_id).
Each version-1 mission persists immutable target/watch/assistant/project bindings and a canonical policy (actions, original ttl_seconds, absolute expires_at, max_actions, contract_version). Mutable state uses a revision plus a leased owner/epoch fence. It also carries the artifact-version frontier and action receipts with prepared, issued, succeeded, rejected, or uncertain state. issued is written before target mutation and charged exactly once; an unknown outcome is reconciled from the target event journal and is never blindly retried.
Queue schema v17 adds ResumeSpec.expected_status and receipt_id. The publisher consumes the exact expected status in its resumable→queued CAS; the runner retains the receipt and stamps it on run_resumed. run_rewound carries the corresponding receipt directly. Rolling deployments therefore reject an old runner instead of executing an untraceable mission action.
events.jsonl
Each line is one store.Event, ordered by a monotonic per-run seq:
{
"seq": 12,
"timestamp": "2026-01-01T00:00:30Z",
"type": "node_finished",
"run_id": "run_123",
"branch_id": "",
"node_id": "inspect",
"data": {},
"log_offset": 1842,
"active_ms": 23000
}The current persisted event vocabulary is grouped below. Payload keys are event-specific and additive; consult comments beside the constants and the emitter when a consumer needs an exact payload contract.
| Family | Persisted event types |
|---|---|
| Run lifecycle/control | run_started, run_paused, human_input_requested, human_answers_recorded, interaction_answered, run_resumed, run_auto_resumed, run_retry_scheduled, run_retry_skipped, run_workspace_reset, run_workspace_bank_restored, run_redelivery_deferred, run_delivery_exhausted, run_steered, run_health, run_finished, run_failed, run_cancelled, run_interrupted |
run_started carries the run's execution provenance: engine_version / engine_commit (the build executing the workflow), workflow_hash (the source revision), and launched_by_version — present only when the build that compiled the IR differs from the one running it, which in cloud is two deployments that move independently. The same pair is on the run document as iterion_version (the launcher) and runner_version (the executor); a runner that finds them different logs a WARN and keeps going, because skew is normal for the length of every rolling deploy. | Graph/budget/artifacts | branch_started, branch_finished, branch_abandoned, node_started, node_recovery, node_verified_action, node_finished, edge_selected, join_ready, budget_warning, budget_exceeded, budget_exit_grace, artifact_written, plan_written | | LLM, delegation, and tools | llm_request, llm_prompt, llm_retry, llm_step_finished, assistant_text, llm_compacted, tool_started, tool_called, tool_error, delegate_started, delegate_finished, delegate_error, delegate_retry, delegate_stall, model_fallback, model_drift, model_served_via_facade | | Review gate | review_turn, review_verdict, review_merged | | Sandbox/network | sandbox_skipped, sandbox_started, sandbox_claw_routed_via_runner, sandbox_host_state_mounted, sandbox_user_remap, sandbox_uid_mismatch_warning, sandbox_devbox_provisioned, sandbox_workspace_export_failed, network_blocked, sandbox_build_started, sandbox_build_finished, sandbox_build_failed | | Browser/preview | preview_url_available, browser_screenshot, browser_session_started, browser_session_ended | | Operator messages | user_message_queued, user_message_delivered, user_message_consumed, user_message_cancelled | | Worktree | worktree_branch_failed |
alert is deliberately not persisted: it is an ephemeral broker event. run_health is its persisted, replayable counterpart. A single torn JSONL tail line is tolerated; widespread corruption returns the typed ErrEventsCorrupted rather than presenting a partial audit as complete.
budget_exit_grace is the audit record of a deliberate overspend: the node ran on a cap that was already spent, inside the bounded exit grace, so the run could reach a terminal node and deliver work it had paid for. Its data carries {dimension, used, limit} — the axis that overran and its own used/limit pair, never another axis's — with the graced node in the event's own node_id. On the sequential path it is deduplicated per (node_id, dimension); a fan-out emits one per branch boundary instead, because branches run concurrently and one event per boundary is the honest audit. iterion report renders it, so an operator never has to open the raw stream to learn that a run spent past what it declared. See dsl.md.
Artifacts
artifacts/<node_id>/<version>.json stores published node outputs:
{
"run_id": "run_123",
"node_id": "inspect",
"version": 0,
"data": { "summary": "..." },
"labels": ["review", "runtime"],
"written_at": "2026-01-01T00:00:30Z"
}Versions are zero-based and increment on publication. artifact_index in the run record accelerates latest-version lookup; older records fall back to a directory scan.
Deployment-report output contract
Reserved output keys any workflow can emit to declare a delivery. The run-view reducer (pkg/runview/snapshot.go, recordDeployment) folds them out of node_finished into RunHeader.deployment, and the studio renders them as the run header's deployment row. The seam is the field names — no bot name, node name or manifest flag is involved, so any bot that reports a deployment lights it up.
Two groups, recognised independently so a bot may emit them from one node or split them across a deploying agent and a deterministic traceability gate (the app-dev shape):
| group | recognised by | fields |
|---|---|---|
| delivery | deployed_url present | deployed bool · healthy bool · deployed_url string · image_ref string · commit string · notes string |
| traceability | verifiable present and at least one of pushed / image_from_repo / built_from_head | verifiable bool · pushed bool · image_from_repo bool · built_from_head bool · commit string · trace_log string |
Last-write-wins per group: a redeploy loop re-reports both, and the final attempt is the run's outcome. A node output carrying neither key contributes nothing, so a run that deploys nothing carries no deployment at all.
verifiable is the meta-fact and it is load-bearing. false means the gate could not establish the three traceability facts (git unreachable, gate miswired) — an environment fault, not a verdict against the deploy, and the studio renders it as its own state rather than as a failure. The three booleans below it carry no information when it is false.
The traceability group exists because liveness is necessary and not sufficient: an app served from a ConfigMap on a stock base image answers 200 and reports every liveness field honestly while nothing was pushed and nothing is reproducible. A delivery must also be traceable — commits reachable from a remote branch, and the running image published under the repo's own registry path, naming the deployed commit.
Interactions
interactions/<interaction_id>.json records the durable question/answer exchange. Review gates additionally retain the ordered companion↔human turns:
{
"id": "run_123_review",
"run_id": "run_123",
"node_id": "review",
"requested_at": "2026-01-01T00:01:00Z",
"answered_at": null,
"questions": { "approved": "Ship?" },
"answers": {},
"turns": [
{ "role": "companion", "content": "Run the smoke test.", "at": "..." },
{ "role": "human", "content": "Passed.", "at": "..." }
],
"tenant_id": ""
}The checkpoint embeds pending questions as a resilience fallback if the separate interaction record is lost.
Attachments, plans, tool blobs, and messages
- Attachment bytes live under
attachments/<name>/; metadata is mirrored in the run record and a sidecar so the filesystem can be re-indexed. Cloud uses object storage with an opaquestorage_ref. plans/<sequence>.jsoncontains deduplicated snapshots of an agent's living todo list, ordered by zero-padded sequence.tools/<tool_use_id>/{input,output}contains large exact tool bodies.user_messages.jsonlis the durable operator-message inbox; companion events expose queue/delivery/consumption transitions in the run timeline.
Compatibility rules
- Missing or zero
format_versionis treated as V1-compatible. - New struct fields are additive and normally optional; do not reject unknown JSON fields in external readers.
- Event consumers must ignore event types and payload keys they do not know.
Event.Dataremains schemaless by design.- Filesystem paths are an implementation of store interfaces, not a portable cloud storage contract.
Mongo saves during a rolling upgrade
Mongo SaveRun preserves additive BSON fields absent from the writer's Go schema, including fields nested in checkpoints and retained branch records. A rename by an older replica therefore keeps the newer replica's execution state and its BSON value types. The replacement still uses the loaded run version for compare-and-swap: a concurrent update produces ErrRunConflict, and a stored schema version newer than the writer supports refuses the save.
Known fields remain owned by the caller. Clearing a checkpoint removes that whole subtree; deleting a branch or an output map entry removes it and its extensions. Lists of records retain extensions only for entries with the same complete known value, including when reordered. A changed record is a replacement, since an older writer cannot infer the meaning of its opaque extensions. Arbitrary input/output payloads are replaced as supplied.
This preservation takes effect only once every writer has this safeguard; binaries predating it can still truncate newer fields. It does not make an older runner capable of executing newer checkpoint semantics or relax the deployment ordering required for new consumers.
