# 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.

```json
{
  "$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.json` covers 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`

```json
{ "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.

```json
{
  "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_by` and `ran_on` are 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 under `SKIP_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`](/tasks#fan-out-v091) 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.

```json
{
  "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](/specification#governance) auditable rather than merely configured.

```json
{
  "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](#fan-out-attempts)), 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](/versioning-and-stability#conformance). It requires every attempt to record `performed_by`, `ran_on`, and its produced assets.
- **Cost attribution** sums `cost` across attempts, grouped by `ran_on` or by context.
- **AI-disclosure certification** reads the definition for declarations and the run for what actually executed. A `generative` task 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.
