# Validation

> Every check the validator performs — structural, referential, graph, and expression — and the error codes it emits.

Validation is what makes an OWG document safe to execute regardless of who or what wrote it. A workflow authored by an agent gets exactly the same scrutiny as one authored by a supervisor, and that symmetry is the point: it is what allows agent-authored workflows to be trusted at all.

Validation runs in four passes, in order. **Each pass runs only if the previous one produced no errors**, because a document that fails structurally cannot be meaningfully checked for anything else.

| Pass | Checks | Why it cannot be earlier |
|---|---|---|
| 1. Structural | JSON Schema — types, enums, patterns, required fields, unknown keys | — |
| 2. Referential | Every id reference resolves | Needs well-formed entities |
| 3. Graph | Acyclicity, uniqueness, reachability | Needs resolvable references |
| 4. Expression | `when` conditions and `$.` references parse, resolve, and type-check | Needs a valid graph to resolve against |

## Running the validator

```bash
owg-validate workflow.owg.json
```

| Flag | Effect |
|---|---|
| `--json` | Machine-readable output |
| `--schema-only` | Stop after pass 1 |
| `--verbose` | Include documentation links per error |
| `--help` | Usage |

Success:

```
✓ Valid — "episodic-dailies" (v0.92): 7 tasks, 9 assets, 5 participants
```

Failure, human-readable — code, path, message:

```
✗ Invalid — 2 error(s):

  [OWG_UNKNOWN_DEPENDENCY] /tasks/3/depends_on/0
    Task "publish" depends on "transcodee", which doesn't exist in this document.

  [OWG_UNKNOWN_PROPERTY] /tasks/1
    Unknown property "performed_bye". Did you mean "performed_by"?
```

Failure, `--json`:

```json
{
  "valid": false,
  "errors": [
    {
      "code": "OWG_CYCLE_DETECTED",
      "path": "/tasks",
      "message": "Cycle detected in task dependency graph: grade → qc → regrade → grade.",
      "docs": "https://openworkflowgraph.org/validation#graph-checks"
    }
  ]
}
```

Successes go to stdout, failures to stderr, exit code 0 or 1.

## Pass 1 — structural

JSON Schema draft 2020-12, evaluated in all-errors mode so you get every structural problem at once rather than one per run.

The check worth calling out separately: **unknown properties are errors.** Every object in the specification sets `additionalProperties: false`, so a misspelled or invented field fails loudly instead of being silently accepted.

```
[OWG_UNKNOWN_PROPERTY] /tasks/1
  Unknown property "performed_bye". Did you mean "performed_by"?
```

This is the single most valuable check for machine-authored documents. A hallucinated property is the most common way a generated workflow validates and then does nothing, and closing the object shape is what converts that silent failure into a caught one. Where you genuinely need to carry extra data, use [`metadata` or an `x-` prefix](/specification#extensions).

Also enforced here, because JSON Schema can express them conditionally:

- A `param` with `required: true` must not carry a `default`.
- A `default` must match its parameter's declared `type`.
- A relationship with `assurance: "inferred"` must carry `confidence` and `method`; one with `attested` or `asserted` must not.

## Pass 2 — referential

Every identifier reference must resolve within the document. These are the errors that catch typos, and they are the difference between a document that validates and a document that runs.

| Reference | Must resolve to |
|---|---|
| `task.depends_on[]` | A task |
| `task.used[]`, `task.produced[]` | An asset |
| `task.performed_by` | A participant |
| `task.ran_on` | An infrastructure entry |
| `task.on_pass`, `task.on_fail` | A task |
| `task.on_failure_compensate` | A task |
| `task.reroute_feedback.to_task` | A task |
| `asset.predecessor` | An asset |
| `asset.produced_by` | A task |
| `asset.composes[].component` | An asset |
| `participant.operated_by` | A participant |
| `participant.organization_id`, `works_for` | An organization |
| `relationship.from.id`, `to.id` | An entity of the stated `kind` |
| `relationship.proposed_by`, `accepted_by` | A participant |
| `context` (string form) | An entry in `contexts[]` |
| `on_workflow_failure` | A task |

Consistency checks in the same pass:

- **`produced_by` must agree with `produced[]`.** If an asset names a producing task, that task must list the asset in `produced[]`. Emits `OWG_PROVENANCE_MISMATCH`.
- **An asset must not be produced by more than one task.** Two producers means the lineage is ambiguous.
- **A nested OWG subgraph's required parameters must be satisfied** by the parent task's `inputs`. A child parameter with `required: true` and no matching parent input emits `OWG_SUBGRAPH_PARAM_UNSATISFIED`. See [Subgraphs](/subgraphs-and-turnovers#format-owg-a-nested-document).

## Pass 3 — graph

| Check | Code |
|---|---|
| The `depends_on` graph is acyclic | `OWG_CYCLE_DETECTED` |
| Task ids are unique | `OWG_DUPLICATE_ID` |
| Asset, participant, infrastructure, organization, context, relationship ids are unique within their registry | `OWG_DUPLICATE_ID` |
| The `operated_by` chain terminates at a `human` or `organization` participant | `OWG_AUTHORITY_CYCLE` |
| Gate routing (`on_pass` / `on_fail`) does not form an unbounded loop without `max_reroutes` | `OWG_UNBOUNDED_REROUTE` |
| Nested OWG subgraph references do not form a cycle | `OWG_SUBGRAPH_CYCLE` |
| Subgraph nesting does not exceed depth 8 | `OWG_SUBGRAPH_DEPTH` |

Cycle errors name the whole cycle rather than one member, because a cycle is only fixable if you can see it:

```
[OWG_CYCLE_DETECTED] /tasks
  Cycle detected in task dependency graph: grade → qc → regrade → grade.
```

The authority check exists because an agent operated by an agent operated by the first agent has no accountable human, which quietly defeats the entire attribution model.

The re-route check is the one people find surprising. A gate whose `on_fail` routes back upstream is normal and intended; a gate that does so **without** `max_reroutes`, and in a document without `governance.max_total_reroutes`, describes a loop with no exit. That is a validation error rather than a run-time discovery.

## Pass 4 — expressions

Every `when` condition and every `$.` reference is parsed against the [grammar](/reference-syntax), then resolved and type-checked.

| Check | Code |
|---|---|
| The expression parses | `OWG_EXPRESSION_SYNTAX` |
| Every reference resolves to a declared parameter, task, or field | `OWG_UNRESOLVED_REFERENCE` |
| Comparison operands are type-compatible | `OWG_TYPE_MISMATCH` |
| Ordering operators are applied to numbers | `OWG_TYPE_MISMATCH` |
| An interpolated value is a scalar, not a structure | `OWG_TYPE_MISMATCH` |
| A referenced task is not downstream of the referring task | `OWG_FORWARD_REFERENCE` |

```
[OWG_UNRESOLVED_REFERENCE] /tasks/2/when
  Reference "$.params.add_caption" does not resolve. The document declares
  no parameter "add_caption". Did you mean "add_captions"?
```

```
[OWG_TYPE_MISMATCH] /tasks/4/when
  Cannot compare string to number: "$.params.territory > 3".
```

The forward-reference check deserves explanation: a task cannot read the output or status of a task that does not precede it in the dependency graph, because at evaluation time that value does not exist. It is a common authoring error and a genuinely confusing run-time failure, so it is caught here.

## Error codes

| Code | Pass | Meaning |
|---|---|---|
| `OWG_FILE_NOT_FOUND` | — | The file could not be read |
| `OWG_INVALID_JSON` | — | The file is not valid JSON |
| `OWG_SCHEMA_ERROR` | 1 | A JSON Schema constraint failed |
| `OWG_UNKNOWN_PROPERTY` | 1 | An unrecognised key, with a suggestion where one is close |
| `OWG_VERSION_UNSUPPORTED` | 1 | `owg_version` is not a version this validator knows |
| `OWG_UNKNOWN_DEPENDENCY` | 2 | `depends_on` names a task that does not exist |
| `OWG_UNKNOWN_REFERENCE` | 2 | Any other id reference that does not resolve |
| `OWG_PROVENANCE_MISMATCH` | 2 | `produced_by` and `produced[]` disagree |
| `OWG_DUPLICATE_ID` | 3 | Two entities in one registry share an id |
| `OWG_CYCLE_DETECTED` | 3 | The dependency graph is cyclic |
| `OWG_AUTHORITY_CYCLE` | 3 | An `operated_by` chain does not terminate |
| `OWG_UNBOUNDED_REROUTE` | 3 | Gate routing loops with no ceiling |
| `OWG_SUBGRAPH_PARAM_UNSATISFIED` | 2 | A nested subgraph's required parameter has no matching parent input |
| `OWG_SUBGRAPH_CYCLE` | 3 | Nested subgraph references form a cycle |
| `OWG_SUBGRAPH_DEPTH` | 3 | Subgraph nesting exceeds depth 8 |
| `OWG_EXPRESSION_SYNTAX` | 4 | An expression does not parse |
| `OWG_UNRESOLVED_REFERENCE` | 4 | A `$.` reference does not resolve |
| `OWG_TYPE_MISMATCH` | 4 | Operand or interpolation types are wrong |
| `OWG_FORWARD_REFERENCE` | 4 | A reference points at a task that is not upstream |
| `OWG_COMPENSATE_MISSING_TARGET` | 2 | `COMPENSATE` without `on_failure_compensate` |
| `OWG_FANOUT_TOLERANCE_CONFLICT` | 2 | `fan_out` declares both `tolerated_failure_percentage` and `tolerated_failure_count` |

Every error carries `code`, `path` (a JSON Pointer into the document), `message`, and a `docs` URL.

## Validating OMC instances

Inbound MovieLabs OMC data is validated before anything enters a graph:

```bash
omc-validate instance.json
```

```
✓ Valid OMC-JSON — 1243 entities pass schema v3.0
```

The OMC validator distinguishes **errors** from **warnings**. Warnings never affect the verdict; they catch things a schema pass structurally cannot:

| Code | Why a schema pass cannot catch it |
|---|---|
| `OMC_SCHEMA_VERSION` | An older OMC document can validate cleanly against a newer schema |
| `OMC_UNKNOWN_CONTROLLED_VALUE` | Controlled vocabularies are advisory annotations, so out-of-vocabulary values pass untouched |
| `OMC_UNKNOWN_EDGE_PREDICATE` | OMC leaves its edge map open, so undeclared predicates validate |
| `OMC_UNEXPECTED_EDGE_TARGET` | Likewise for unexpected target types |

All four are driven off the OMC schema itself rather than hardcoded tables — the vocabularies, predicate set, and declared target ranges are read out of the schema — because MovieLabs is actively developing relationship domains and ranges, so the tables must move when the schema moves.

Warnings are grouped and counted, so one defect reports once:

```
⚠ 323 conformance warning(s) in 37 group(s) — not schema failures:
     86×  assetStructureType: "digital.audiovisual" is not in the controlled
          vocabulary (extension point — allowed, but unrecognised)
```

Referential integrity is reported **informationally, never fatally**, because external references are legal in OMC:

```
⚠ 12 reference(s) point to entities not defined in this file
  (legal if they're external)
```

`customData` and `annotation` subtrees are never inspected — they are free-form by definition.

## Nested documents

A task whose `subgraph` declares `format: "owg"` contains another OWG document, and that document is **validated recursively** — all four passes, to a maximum nesting depth of 8.

Error paths in a nested document are prefixed with the path of the task that contains it, so a failure names where it actually lives:

```
[OWG_UNKNOWN_REFERENCE] /tasks/4/subgraph/tasks/1/used/0
  Task "comp_0140" uses "plate_014", which doesn't exist in this document.
  Did you mean "plate_0140"?
```

Subgraphs with any other `format` are **not** descended into. They are foreign structures, stored and never parsed — so nothing inside them can be validated, and nothing inside them can be referenced from outside.

## Multi-document projects

A document with registries and no tasks is a **registry document**: it holds the shared participants, organizations, infrastructure and contexts for a production, and the workflow documents carrying the same `project_id` reference them.

That means a workflow document from such a project cannot be fully checked on its own — its `performed_by`, `works_for` and `organization_id` references resolve into the registry, not into itself. Supply the siblings:

```ts
validateOWG(doc, { registry: [projectRegistryDoc] });
```

Entity references resolve across the documents you supply. **Task references never do** — `depends_on`, `on_pass`, `on_fail` and `on_workflow_failure` must name a task in the same document, because nesting another document's work is explicit, via [`subgraph`](/subgraphs-and-turnovers).

Without a registry, an unresolved reference in a document that declares `project_id` says so in the error message, rather than leaving you to guess whether it is a typo or a cross-document link.

## Conformance note

The four passes above are what the specification requires of a conformant validator. An implementation that performs only the structural pass is doing JSON Schema validation, not OWG validation — useful, but it will accept documents that cannot execute.

If you are relying on a validator you did not write, establish which passes it actually performs before trusting a clean result. The [pre-flight checklist](/for-agents#pre-flight-checklist) covers what to verify yourself in the meantime.

The reference validator implements all four, and the [conformance corpus](/conformance) is checked by all four on every commit — which is the only reason the corpus can be offered as reference data rather than as illustrations.
