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
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 participantsFailure, 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:
{
"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.
Also enforced here, because JSON Schema can express them conditionally:
- A
paramwithrequired: truemust not carry adefault. - A
defaultmust match its parameter's declaredtype. - A relationship with
assurance: "inferred"must carryconfidenceandmethod; one withattestedorassertedmust 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_bymust agree withproduced[]. If an asset names a producing task, that task must list the asset inproduced[]. EmitsOWG_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 withrequired: trueand no matching parent input emitsOWG_SUBGRAPH_PARAM_UNSATISFIED. See Subgraphs.
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, 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:
omc-validate instance.json✓ Valid OMC-JSON — 1243 entities pass schema v3.0The 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:
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.
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 covers what to verify yourself in the meantime.
The reference validator implements all four, and the conformance corpus 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.