Run records
The run document — how one execution of a definition is recorded, with per-attempt status, cost, and the actual infrastructure used.
A definition says what should happen. A run records what did. They are separate documents, and keeping them separate is what makes a definition reusable across shows: the definition holds no execution state, so nothing needs stripping out before it is used again.
A run document is what an implementation emits as work proceeds, and what an auditor reads afterwards.
{
"$schema": "https://openworkflowgraph.org/schemas/run/v0.92.json",
"owg_version": "0.92",
"kind": "run",
"id": "run-ep104-0007",
"definition": {
"id": "episodic-dailies",
"version": "3",
"digest": "sha256:71b0e4…"
},
"status": "succeeded",
"started_at": "2026-08-21T08:02:14Z",
"finished_at": "2026-08-21T08:41:55Z",
"params": { "source_url": "https://…/card_a001.mov", "add_captions": true },
"attempts": [ ],
"reroutes": [ ],
"spend": { "amount": 42.10, "currency": "USD" }
}The run schema is not yet published.
schemas/core/v0.92.jsoncovers the definition document; the run document is specified here but has no schema artifact to validate against yet. Emit the shape below and expect the published schema to match it — but do not treat a missing schema as permission to improvise the field names.
Required fields
| Field | Type | Notes |
|---|---|---|
owg_version | string | Required. The definition's own owg_version |
kind | string | Required. "run" — what distinguishes a run from a definition |
id | string | Required. Identifies this execution |
definition | object | Required. Which definition, at which version |
status | enum | Required. queued | running | succeeded | failed | cancelled |
kind is the discriminator. A definition document omits it (or sets "definition"), so a reader can tell the two apart without guessing from which fields are present.
All fields
| Field | Type | Purpose |
|---|---|---|
definition | object | { id, version, digest } — see below |
status | enum | Run-level outcome |
started_at, finished_at | string | RFC 3339 |
params | object | The values the caller actually supplied |
attempts | array | Per-task execution records. See below |
reroutes | array | Gate re-routes that occurred |
spend | object | { amount, currency } — total attributed cost |
context | string | object | The context this run executed against |
triggered_by | string | Participant id that started the run |
metadata | object | Freeform |
definition
{ "id": "episodic-dailies", "version": "3", "digest": "sha256:71b0e4…" }id and version identify the definition. digest is a content hash of the definition document, and it is the field that makes a run trustworthy: it proves which exact bytes were executed, so a definition edited after the fact cannot silently rewrite history.
Definitions are immutable once run. If a definition needs to change, publish a new version. Mutating a definition that a run references breaks the digest and destroys the audit trail — this is the single most important rule on this page.
Attempts
An attempt is one execution of one task. A task that failed twice and succeeded on the third try produces three attempt records, not one overwritten result.
{
"attempts": [
{
"task": "transcode",
"attempt": 1,
"status": "failed",
"started_at": "2026-08-21T08:02:20Z",
"finished_at": "2026-08-21T08:02:33Z",
"performed_by": "transcode_svc",
"ran_on": "cloud_platform",
"error": { "code": "RATE_LIMIT", "message": "Upstream throttled" },
"cost": { "amount": 0.00, "currency": "USD" }
},
{
"task": "transcode",
"attempt": 2,
"status": "succeeded",
"started_at": "2026-08-21T08:02:43Z",
"finished_at": "2026-08-21T08:05:11Z",
"performed_by": "transcode_svc",
"ran_on": "cloud_platform",
"produced": ["delivery_mp4"],
"outputs": { "output_key": "jobs/ep104/transcode/out.mp4" },
"cost": { "amount": 0.25, "currency": "USD" }
},
{
"task": "caption",
"attempt": 1,
"status": "skipped",
"skip_reason": "when_false"
}
]
}| Field | Type | Notes |
|---|---|---|
task | string | Required. Task id in the definition |
instance | integer ≥ 0 | Which fan_out instance this attempt belongs to. Absent for a task with no fan_out. Addressable as $.tasks.<id>[<instance>] |
attempt | integer ≥ 1 | Required. Addressable as $.task.attempt — for a fan-out task, per instance: instance 3's second attempt is still attempt: 2 |
status | enum | Required. pending | running | succeeded | failed | skipped |
started_at, finished_at | string | RFC 3339. Absent for skipped |
performed_by | string | The participant that actually performed it |
ran_on | string | The infrastructure it actually ran on |
produced | string[] | Assets actually produced |
outputs | object | Resolved output values |
error | object | { code, message } on failure |
cost | object | { amount, currency } |
skip_reason | enum | when_false | dependency_skipped | dependency_failed |
metadata | object | Freeform |
Two rules matter more than the rest:
- Record what actually happened, not what was planned.
performed_byandran_onare the actual participant and infrastructure, which may differ from the definition's expectation — an engine that placed work dynamically must say where it landed. Cost attribution depends on this being true rather than aspirational. - A run is append-only. Attempts are added, never edited. A record that gets rewritten stops being a record.
Skip propagation
skip_reason distinguishes the three ways a task can be skipped, which matters when reading a run after the fact:
when_false— its own condition evaluated false.dependency_skipped— something upstream was skipped.dependency_failed— something upstream failed underSKIP_DEPENDENTS.
Without the distinction, an optional branch that correctly did not run looks identical to a branch lost to an upstream failure.
Fan-out attempts
A task with fan_out produces one attempt sequence per instance, distinguished by instance rather than by a different task id — the definition still names one task; the run recorded several hundred executions of it.
{
"attempts": [
{ "task": "generate_all_idents", "instance": 0, "attempt": 1, "status": "succeeded",
"performed_by": "concept_bot", "ran_on": "gpu_cloud", "produced": ["ident_shot_001"] },
{ "task": "generate_all_idents", "instance": 1, "attempt": 1, "status": "failed",
"error": { "code": "RATE_LIMIT", "message": "Upstream throttled" } },
{ "task": "generate_all_idents", "instance": 1, "attempt": 2, "status": "succeeded",
"performed_by": "concept_bot", "ran_on": "gpu_cloud", "produced": ["ident_shot_002"] },
{ "task": "generate_all_idents", "instance": 2, "attempt": 1, "status": "failed",
"error": { "code": "MODEL_TIMEOUT", "message": "Generation exceeded 120s" } }
]
}The fan-out task's own status is derived from its instances against its declared tolerance, the same way the whole run's status is derived from its tasks (below). With tolerated_failure_percentage: 3 and 500 instances, 15 failed instances is still a succeeded task; 16 is failed — and the boundary is exactly the number the definition declared, not a judgment call made reading the run afterward. Retry (if retry is also declared) happens per instance before that instance counts as failed for tolerance purposes — instance 1 above only counts as a failure if its retries are exhausted.
A produced asset from a fan-out instance is a normal asset with normal provenance — performed_by and ran_on are per instance, so a run this size still answers "which infrastructure actually made this specific one" without treating the batch as one opaque unit.
Re-routes
Each gate re-route is recorded, which is what makes the ceilings auditable rather than merely configured.
{
"reroutes": [
{
"gate": "qc_review",
"to_task": "regrade",
"count": 2,
"at": "2026-08-21T08:22:09Z",
"carried": ["qc_notes"]
}
]
}| Field | Notes |
|---|---|
gate | Task id of the gate that routed |
to_task | Where it routed to |
count | Which re-route this was for that gate |
at | RFC 3339 |
carried | Output keys carried back as feedback |
An implementation must refuse a re-route that would exceed either the gate's max_reroutes or the run's governance.max_total_reroutes. The record is how you prove it did.
Status derivation
Run status is derived, not independently asserted:
| Run status | When |
|---|---|
queued | No attempt has started |
running | At least one attempt is running, or work remains eligible |
succeeded | Every task reached succeeded or skipped |
failed | Any task is failed and no path to completion remains |
cancelled | Halted by external instruction |
A run with a CONTINUE task that failed while every other task succeeded is failed, not succeeded. The failure was tolerated for scheduling purposes; it is still a failure, and a run record that hid it would be misleading.
A fan_out task is the one exception, and it is a narrow one: if its failed instances stay within its declared tolerated_failure_percentage or tolerated_failure_count, the task itself is succeeded — not because the failures are hidden (every failed instance still has its own attempt record, in full, per above), but because the definition declared in advance exactly how much failure still counts as success for that task. A run made up entirely of tasks in that state is legitimately succeeded. This is different from CONTINUE tolerating a failure for scheduling purposes only — fan_out's tolerance is a stated part of what the task means.
What a run enables
- L3 conformance is a property of a run, not a definition — see Conformance. It requires every attempt to record
performed_by,ran_on, and its produced assets. - Cost attribution sums
costacross attempts, grouped byran_onor by context. - AI-disclosure certification reads the definition for declarations and the run for what actually executed. A
generativetask that was skipped did not generate anything, and the manifest should say so. - The retry history, which is usually the interesting part of an incident review, survives because attempts accumulate.
Guidance
- Emit attempts as they complete, rather than assembling a run document at the end. A run that only exists on success cannot explain a failure.
- Store the digest, and verify it before trusting a run's claims about what it executed.
- Keep runs for as long as you keep the assets they produced. An asset whose run record has been pruned has lost the provenance the graph was built to hold.
- Never reuse a run id. A re-execution is a new run referencing the same definition.