Open Workflow GraphPre-release
Specification

Run records

The run document — how one execution of a definition is recorded, with per-attempt status, cost, and the actual infrastructure used.

View as markdownMachine-readable source for agents and scripted implementers

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

FieldTypeNotes
owg_versionstringRequired. The definition's own owg_version
kindstringRequired. "run" — what distinguishes a run from a definition
idstringRequired. Identifies this execution
definitionobjectRequired. Which definition, at which version
statusenumRequired. 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

FieldTypePurpose
definitionobject{ id, version, digest } — see below
statusenumRun-level outcome
started_at, finished_atstringRFC 3339
paramsobjectThe values the caller actually supplied
attemptsarrayPer-task execution records. See below
reroutesarrayGate re-routes that occurred
spendobject{ amount, currency } — total attributed cost
contextstring | objectThe context this run executed against
triggered_bystringParticipant id that started the run
metadataobjectFreeform

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"
    }
  ]
}
FieldTypeNotes
taskstringRequired. Task id in the definition
instanceinteger ≥ 0Which fan_out instance this attempt belongs to. Absent for a task with no fan_out. Addressable as $.tasks.<id>[<instance>]
attemptinteger ≥ 1Required. Addressable as $.task.attempt — for a fan-out task, per instance: instance 3's second attempt is still attempt: 2
statusenumRequired. pending | running | succeeded | failed | skipped
started_at, finished_atstringRFC 3339. Absent for skipped
performed_bystringThe participant that actually performed it
ran_onstringThe infrastructure it actually ran on
producedstring[]Assets actually produced
outputsobjectResolved output values
errorobject{ code, message } on failure
costobject{ amount, currency }
skip_reasonenumwhen_false | dependency_skipped | dependency_failed
metadataobjectFreeform

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 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"]
    }
  ]
}
FieldNotes
gateTask id of the gate that routed
to_taskWhere it routed to
countWhich re-route this was for that gate
atRFC 3339
carriedOutput 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 statusWhen
queuedNo attempt has started
runningAt least one attempt is running, or work remains eligible
succeededEvery task reached succeeded or skipped
failedAny task is failed and no path to completion remains
cancelledHalted 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 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.