# Open Workflow Graph (OWG) — Complete Documentation Specification v0.92 — DRAFT An open specification for describing media production workflows as portable, validatable, executable JSON documents, built on MovieLabs' Ontology for Media Creation (OMC). Published by Entertainment Technologists Inc. This is an open specification, not a ratified industry standard. v0.92 is a draft: expect field-level change, and pin the schema you validate against. --- ## Contents **Introduction** - Overview - Quickstart - Core concepts - Agentic workflows and certainty **Specification** - Document structure - Tasks and executors - Assets and versioning - Participants and authority - Relationships and assurance - Subgraphs and turnovers - Reference syntax - Profiles - Run records **Implementing** - Validation - Conformance corpus - Persistence requirements - For agents - Examples - Implementation considerations - Failure-handling best practices **Ecosystem** - Standards bindings - Security model - AI disclosure and compliance - Versioning and stability - Changelog --- # Overview *Section: Introduction* > What the Open Workflow Graph is, the problem it solves, and how it relates to the standards already in your pipeline. The Open Workflow Graph (OWG) is a portable, vendor-neutral way to describe a media production workflow as **data** rather than as a diagram or a folder convention. An OWG document is a JSON file. It says what work happens, in what order, who or what performs it, which assets it consumes and produces, and where it runs — precisely enough that an engine can execute it and a query can audit it afterwards. OWG is built on MovieLabs' **Ontology for Media Creation (OMC)**. OMC supplies the vocabulary — the agreed names and relationships for assets, tasks, participants, and the contexts that give them meaning. OWG adds an execution and governance layer expressed in that vocabulary: a way to *run* on the ontology, hand pieces of a workflow across company lines, and answer questions about what happened. The layering is strict and one-directional: - **OMC** defines what things are. - **OWG** defines how work moves between them. - An **implementation** is one way to operate the result. Several are possible, and the specification favours none of them. Nothing above the foundation redefines anything below it. An OWG document is OMC-conformant data with additional structure on top. > **Status: v0.92, draft.** OWG is an open specification published by Entertainment Technologists Inc., not a ratified industry standard. The schema, validator, and conformance corpus are open (Apache-2.0 code, CC-BY documentation). > > v0.92 is the current version. The number reaches 1.0 once there is enough implementation experience behind the specification to promise compatibility — promising stability before anyone has built against a specification is how standards acquire mistakes they cannot remove. Pin the schema you validate against, and see [Versioning and stability](/versioning-and-stability) for how much movement to expect where. ## The problem Every studio, broadcaster, post house, and VFX vendor runs on the same quiet fiction: that the workflow exists somewhere in precise, shared, structured form. It does not. - It exists as **tribal knowledge**, redrawn from memory for each production and lost when the person holding it leaves. - It exists as a **whiteboard sketch** — often the single most accurate workflow document in the building — until the room is cleared. - It exists **piecemeal inside tools**: the production tracker knows the tasks, the asset manager knows the files, the render manager knows the jobs, and none of the three knows about the other two. The consequence is that every hand-off is an act of faith. A comp returns to conform with no structured record of which artist touched it, on what plate, through how many rounds; it is reconciled by filename and hope. This problem predates generative AI by decades and would be worth solving if not one AI tool existed in the pipeline. What AI changes is the price of never having solved it: it multiplies the volume and speed of work moving through exactly those gaps, and it introduces a participant — the autonomous agent — with no tribal memory to fall back on. It also removes the last safeguard. Every one of those informal workflow records at least implied that a person had thought the plan through. Once workflows are generated rather than written, that stops being true, and the industry needs a way to establish that a plan is sound, bounded, and accountable *before* it runs, without a human reading each one. That argument is set out in full in [Agentic workflows and certainty](/agentic-workflows). ## What OWG adds A workflow expressed as an OWG document can be: - **Saved and versioned** like any other production asset, and reused across shows. - **Shared** with a vendor as a scoped subgraph rather than re-explained on a call. - **Edited and reviewed** collaboratively, with each change a recorded event. - **Validated** against a published schema before anything executes. - **Executed** by an engine, emitting provenance as a side effect rather than as an extra chore. - **Written by an agent** in the same format a human would use — and checked before it runs. That last point is the one that matters most as pipelines fill with autonomous tools. An agent-authored workflow is only useful if you can trust it enough to execute; OWG makes that a matter of schema validation, named authority, honest confidence scores, and declared ceilings rather than of faith. See [Agentic workflows and certainty](/agentic-workflows) for why that matters, then [Validation](/validation) and [For agents](/for-agents) for how it works. ## A first look The smallest useful OWG document is two tasks and a dependency between them: ```json { "owg_version": "0.92", "id": "transcode-to-webm", "description": "Normalize a source file, then encode WebM for web delivery", "participants": [ { "id": "transcode_svc", "kind": "service" } ], "assets": [ { "id": "source_mov", "role": "source", "type": "video" }, { "id": "normalized_mp4", "role": "intermediate", "type": "video", "version_relation": "representation", "predecessor": "source_mov", "produced_by": "ingest" }, { "id": "delivery_webm", "role": "delivery", "type": "video", "version_relation": "representation", "predecessor": "normalized_mp4", "produced_by": "web_delivery" } ], "tasks": [ { "id": "ingest", "executor": { "type": "service", "ref": "transcode" }, "ai_role": "none", "performed_by": "transcode_svc", "used": ["source_mov"], "produced": ["normalized_mp4"], "failure_mode": "HALT" }, { "id": "web_delivery", "executor": { "type": "service", "ref": "transcode" }, "ai_role": "none", "performed_by": "transcode_svc", "depends_on": ["ingest"], "used": ["normalized_mp4"], "produced": ["delivery_webm"], "failure_mode": "CONTINUE" } ] } ``` Two things are doing the real work here. **`used` and `produced` are the lineage** — they make this a graph rather than a list, and they fall out of describing the work rather than being recorded separately. **`version_relation`** says *how* each output relates to its input: both hops are `representation`, a format change of the same content, not a revision of it. Every larger workflow in this documentation — multi-vendor turnovers, live sports ingest, camera-to-consumer delivery with AI disclosure — is the same primitive composed at scale: a task, an executor, a used edge, a produced edge, an attributed participant. Continue to the [Quickstart](/quickstart) to write and validate one, or [Core concepts](/concepts) for the model behind it. ## How OWG relates to other standards OWG holds one disciplined rule: **it references the asset-level objects of adjacent standards; it never absorbs their internals.** A workflow graph that tried to mirror every scene primitive or every media segment would inherit the scale and semantics of other domains and collapse under them. Each neighbouring standard owns a plane, and OWG occupies the one they each deliberately leave outside their scope: | Plane | What it holds | Owned by | |---|---|---| | Essence / timerange | The bytes, addressed by identity and time | BBC TAMS | | Spatial / composition | What a scene contains, how assets compose | OpenUSD | | Editorial / time | The ordered cut — which timeranges, in what sequence | OpenTimelineIO | | Creation-time marking | Signed content credentials at the point of capture or generation | C2PA | | Governance / execution | Which versions exist, who made them, from what, under what authority | **OWG**, in OMC's vocabulary | Full field-level mappings are in [Standards bindings](/standards-bindings). --- # Quickstart *Section: Introduction* > Write a valid OWG document, validate it, and understand what the graph now knows — in about ten minutes. This walks through the smallest complete OWG document, then adds the pieces that make it useful. You need Node 20 or newer. ## 1. What you need A JSON editor and a validator that implements the [four validation passes](/validation). Pointing your editor at the schema gives you completion and inline errors as you type: ```json { "$schema": "https://openworkflowgraph.org/schemas/core/v0.92.json" } ``` ## 2. The smallest complete document A single task that transcodes one file. Save as `hello.owg.json`: ```json { "$schema": "https://openworkflowgraph.org/schemas/core/v0.92.json", "owg_version": "0.92", "id": "hello-transcode", "profiles": ["core", "cloud-services"], "description": "Transcode one source file MOV→MP4.", "objective": "Produce an MP4 delivery from a MOV source.", "organizations": [ { "id": "org_mine", "name": "My Facility" } ], "participants": [ { "id": "transcode_svc", "kind": "service", "name": "Transcode Service", "organization_id": "org_mine" } ], "infrastructure": [ { "id": "cloud_saas", "type": "saas_platform", "owner": "org_mine" } ], "assets": [ { "id": "source_mov", "role": "source", "type": "video.master", "content_type": "video/quicktime", "storage": { "provider": "s3", "locator": "s3://my-bucket/in/clip.mov" } }, { "id": "delivery_mp4", "role": "delivery", "type": "video.delivery", "content_type": "video/mp4", "version_relation": "representation", "predecessor": "source_mov", "produced_by": "transcode", "storage": { "provider": "s3", "locator": "s3://my-bucket/out/clip.mp4" } } ], "tasks": [ { "id": "transcode", "label": "Transcode MOV → MP4", "type": "work", "executor": { "type": "service", "ref": "transcode", "environment": "saas" }, "ai_role": "none", "performed_by": "transcode_svc", "ran_on": "cloud_saas", "used": ["source_mov"], "produced": ["delivery_mp4"], "failure_mode": "HALT" } ] } ``` Three things to notice: - **`used` and `produced` are the provenance edges.** They are what make this a graph rather than a list. The asset's `produced_by` mirrors the task's `produced[]` — the task is authoritative. - **`version_relation: "representation"`** says the MP4 is a *format change of the same content*, not a revision of it. That distinction matters more than it looks — see [Assets and versioning](/assets-and-versioning#typed-version-relations). - **`ai_role: "none"`** is declared explicitly rather than left absent. AI involvement is never inferred from the executor type. ## 3. Validate it ```bash owg-validate hello.owg.json ``` ``` ✓ Valid — "hello-transcode" (v0.92): 1 task, 2 assets, 1 participant ``` Now break it deliberately. Change `"used": ["source_mov"]` to `"used": ["source_mv"]` and re-run: ``` ✗ Invalid — 1 error(s): [OWG_UNKNOWN_REFERENCE] /tasks/0/used/0 Task "transcode" uses "source_mv", which doesn't exist in this document. Did you mean "source_mov"? ``` Try a misspelled field too — change `performed_by` to `performed_bye`: ``` ✗ Invalid — 1 error(s): [OWG_UNKNOWN_PROPERTY] /tasks/0 Unknown property "performed_bye". Did you mean "performed_by"? ``` Both matter more than they look. Unknown properties are errors rather than silently accepted fields, which is what stops a generated document from validating and then doing nothing. See [Validation](/validation). > **Implementation note.** The reference validator currently implements the structural pass fully and the referential, graph, and expression passes partially. Until they land, run the [pre-flight checklist](/for-agents#pre-flight-checklist) yourself. ## 4. What the graph now knows From those few lines, without anything else being recorded: - **Lineage** — `delivery_mp4` came from `source_mov` via `transcode`, as a format representation. - **Attribution** — `transcode_svc` performed it, on behalf of `org_mine`. - **Placement** — it ran on `cloud_saas`, so cost attributes somewhere real. - **Location** — both assets carry locators, so nothing had to move to be governed. None of that required a separate provenance step. These are [structural edges](/concepts#edges-come-in-two-kinds) — they fall out of describing the work. ## 5. Add a chain and a human gate Real workflows have dependencies and decisions. Add a review task: ```json { "tasks": [ { "id": "transcode", "executor": { "type": "service" }, "performed_by": "transcode_svc", "used": ["source_mov"], "produced": ["delivery_mp4"], "failure_mode": "HALT" }, { "id": "qc", "type": "qc", "executor": { "type": "service", "ref": "qc_validate" }, "performed_by": "transcode_svc", "depends_on": ["transcode"], "used": ["delivery_mp4"], "checks": ["duration_match", "codec_compliance"], "on_pass": "approve", "on_fail": "transcode", "max_reroutes": 2 }, { "id": "approve", "type": "review", "executor": { "type": "human" }, "performed_by": "a.singh", "depends_on": ["qc"], "used": ["delivery_mp4"], "failure_mode": "HALT" } ] } ``` Add `a.singh` to `participants` as `kind: "human"` with a `role`, and the document stays valid. The `qc` task is a **gate**: it routes on outcome rather than merely succeeding, and `max_reroutes` caps how many times it can send work back. A human review is an ordinary task with `executor.type: "human"` — the same shape as a machine task, which is what lets a schedule and a cost model span both. See [Gates and re-routes](/tasks#gates-and-re-routes). ## 6. Add an AI step ```json { "id": "generate_thumb", "type": "work", "executor": { "type": "comfyui_graph", "environment": "cloud" }, "ai_role": "generative", "performed_by": "agent_thumb", "ran_on": "gpu_cloud", "depends_on": ["approve"], "used": ["delivery_mp4"], "produced": ["thumb_png"], "subgraph": "graphs/sdxl_thumb.json" } ``` With `agent_thumb` declared as an agent carrying `operated_by`: ```json { "id": "agent_thumb", "kind": "agent", "operated_by": "a.singh", "organization_id": "org_mine" } ``` And the produced asset carrying credentials: ```json { "id": "thumb_png", "type": "image.generated", "version_relation": "derivation", "predecessor": "delivery_mp4", "produced_by": "generate_thumb", "credentials": { "scheme": "c2pa" } } ``` That gives you three things at once: 1. **Accountability** — the agent's work traces to `a.singh` via `operated_by`. 2. **Disclosure** — `ai_role: "generative"` means an [Article 50 manifest](/compliance) can be computed from lineage. 3. **Encapsulation** — the whole node graph is [one task with an interior](/subgraphs-and-turnovers), not fifty governed nodes. ## Next steps - [Core concepts](/concepts) — the model behind what you just wrote - [Document structure](/specification) — every top-level field, parameters, and governance - [Examples](/examples) — full annotated corpus workflows - [For agents](/for-agents) — if you are a machine implementing this - [Implementation considerations](/implementation-considerations) — the decisions left to you, with recommended approaches - [Versioning and stability](/versioning-and-stability) — what you can rely on, and what will grow --- # Core concepts *Section: Introduction* > The five node types, the provenance backbone beneath them, and the two design choices that make a workflow both a plan and a record. OWG describes a production with five node types, wired by named, typed relationships. Every workflow document, however large, is built from these and nothing else. It helps to think of a production as an organism: five cell types inside one membrane (OMC), connected by a nervous system of typed edges. ## The five node types ### Participants Every human, agent, service, or department that can act — each with an identity and a place in a hierarchy. An agent always carries an `operated_by` reference (the participant on whose authority it acts) and an organization. So any asset traces back through the agent that produced it to the responsible human and organization. This is the backbone the rest of the model depends on: accountability is structural rather than procedural. **What it enables:** zero-trust access, authorship records, traceable approvals, and AI disclosure that falls out of the data rather than being compiled by hand. ### Tasks The work itself — OMC's Task entity made executable. Where a legacy pipeline hard-codes "call tool X", OWG generalizes the executor as an attribute. A task may run in a cloud service, at an external API endpoint, in a node-graph tool such as ComfyUI, in a desktop application such as Nuke, Houdini, or DaVinci Resolve on an artist's own workstation, in an autonomous agent, or in a human's hands. Treating "runs on cloud GPU", "runs in a DCC on a workstation", and "is a human review gate" as three values of one attribute is what puts AI and traditional work in a single accountable graph, interchangeably. A task may also carry a `subgraph` — a nested OWG document — which is how a turnover hands a vendor exactly the context they need and nothing more. **What it enables:** scheduling, per-step cost and QC, and human and machine work under one contract. ### Assets Typed, versioned content, each carrying a locator that identifies wherever it physically lives: a cloud bucket, on-premises storage, a SaaS asset manager, a camera card, or a drive in transit. Assets carry their own governance — content identity (hash, C2PA manifest, URN), rights and clearances, copyright status, approvals, and a preservation policy with fixity records — rather than depending on whichever system happens to hold the bytes. Critically, the relationship between versions is **typed, not merely numbered**. See [Assets and versioning](/assets-and-versioning). **What it enables:** lineage, instant rights answers, and archives that satisfy deep-library requirements by construction. ### Infrastructure Cloud compute, GPUs, workstations, on-premises render farms, and SaaS platforms in one topology, described by capability and environment. Because a task records what it `ran_on`, cost attribution and multi-cloud portability are properties of the graph. Capability matching also lets an engine place work dynamically instead of relying on hand-routing. **What it enables:** scheduling, cost attribution, multi-cloud portability. ### Context The show, sequence, scene, shot, or territory to which work belongs. Tasks and assets are **associated with** a context rather than contained in it, because the same asset means different things in different places — a plate is one thing in the cut and another in a territory-specific master. **What it enables:** scoping, per-context permissions, and views that match how productions actually think. ## The provenance backbone Beneath all five sits the W3C provenance model, **PROV**. The mapping is direct: | OWG | PROV | |---|---| | A task attempt | `Activity` | | An asset | `Entity` | | A participant | `Agent` | | Task consumed asset | `used` | | Asset came from task | `wasGeneratedBy` | | Task performed by participant | `wasAssociatedWith` | | Asset came from asset | `wasDerivedFrom` | | Agent acts for a human | `actedOnBehalfOf` | Every asset can therefore answer *"which participant touched me, on whose authority"* with no extra tooling, because the answer is simply the graph. Building on PROV rather than inventing a provenance vocabulary is deliberate: it is a published W3C model with existing tooling, and it is the same choice C2PA made for content credentials. ## Two design choices that matter ### Definition and run are separate documents A **workflow definition** is a static, reusable, versionable template. A **run** is one execution that references a definition. This is what lets the same graph render two ways: as a plan before anything starts, and as a live status board while work is under way. It is also what makes a definition reusable across shows — the definition holds no execution state, so nothing needs stripping out before it is used again. Re-routes and retries create new **task attempts** under the same run, so a step that failed twice and succeeded on the third try is three attributed activities, not one overwritten record. ### Edges come in two kinds **Structural edges** — `used`, `produced`, `performed_by`, `ran_on`, `approved`, `derived_from` — fall out of executing the workflow automatically, at zero extra cost. Nobody types them in. They are the by-product of running the work. **Semantic edges** — this asset *features* that character, *depicts* that scene — require intelligence to produce. In OWG the tool that writes a semantic edge is itself just another task, with an executor and an attributed author, so an inferred relationship carries the same provenance as any other piece of work. That distinction is why the provenance record does not depend on anyone's diligence. The expensive, interesting metadata is optional and attributed; the accountability metadata is automatic. ## Assurance: how sure is this edge? Because a graph is assembled from many systems and some relationships are inferred rather than declared, every explicit relationship carries an assurance level: - **`attested`** — backed by a verified content credential, such as a C2PA manifest. - **`asserted`** — declared by a system of record, or confirmed by a human. - **`inferred`** — proposed rather than declared, carrying a **required** confidence score and an identifier for the method that produced it. So the graph records not just *that* two things are related, but *how certain* the relationship is and *who or what* vouched for it. That is what makes machine-proposed structure safe to accept selectively rather than wholesale — an implementation can apply whatever acceptance policy it judges appropriate, and the record shows what was proposed, by what, and who accepted it. The same three levels, and the same rule that `inferred` must carry a score and a method, apply wherever assurance appears. See [Relationships and assurance](/relationships) for the payload, and [For agents](/for-agents) for how to propose responsibly. ## Composing the five cells The same five node types build every structure a production needs: - Link **tasks** and you have a workflow. - Link **participants** and you have an org and authority chart. - Link **assets** and you have asset management with real lineage. - Link **tasks to infrastructure** and you have a schedule and a cost model. - Scope all of it by **context** and you have per-show, per-territory views over one graph. A mid-sized post facility generates tens of thousands of tracked edges a year; a studio across several years reaches the billions. What an implementation must be able to answer at that scale is in [Persistence requirements](/persistence). --- # Agentic workflows and certainty *Section: Introduction* > What changes when workflows are written by machines rather than people, the four things you must be able to establish about a workflow before running it, and how OWG makes each of them checkable. The workflows running a production have always been written by people — in a scheduling tool, on a whiteboard, or in the head of the coordinator who knows how this show does things. That is changing quickly. A production tracker can already emit its own task list. An agent that knows how to run a step can propose the subgraph for that step. A supervisor can describe an episode's finishing plan in a sentence and get back a hundred tasks wired into a dependency graph. This page is about what that does to the industry's oldest assumption: that somebody read the plan before the work started. ## The bottleneck moves Authoring a workflow used to be the expensive part. It is about to be the cheap part. When a machine can emit two hundred workflow documents in an hour, the constraint is no longer writing them. It is establishing that any given one is safe to execute — and prose review does not scale to that volume. A human reading a hundred generated plans reads the first three carefully and skims the rest, which is worse than not reading them, because it produces the appearance of oversight. So review has to become mechanical. Not "a person looked at it", but: **the document was checked against a published schema, its references resolved, its authority chain terminated at a named human, and its ceilings were declared before anything ran.** What a person then reviews is the small remainder those checks could not settle. That is what we mean by workflow certainty, and it is a property of the format rather than of the model that generated the document. ## What a prompt cannot do The natural place to put an agent-written workflow is in prose — a request, a plan, a chat transcript. Prose is a poor container for a plan somebody is accountable for. | | Prose plan | OWG document | |---|---|---| | Can a machine check it before it runs? | No | Yes — [validation](/validation) | | Can two versions be diffed? | Only by reading | Yes, it is structured data | | Can it be scoped for a vendor? | No | Yes — [subgraphs](/subgraphs-and-turnovers) | | Can it be replayed exactly? | No | Yes — [definition and run](/runs) are separate | | Does it say who is accountable? | Sometimes | Required — [`operated_by`](/participants-and-authority) | | Does it record what happened? | No | Automatically, as structural edges | A generated OWG document is a proposal expressed in a form that can be argued with. That is the whole difference. ## The four certainties Before executing a workflow — whoever or whatever wrote it — four questions have answers, and in OWG each answer is checkable rather than assumed. ### 1. Structural: will it run at all? Every document is validated against the published schema before a single task executes. Cycles in the dependency graph, `depends_on` pointing at a task that does not exist, an asset consumed that was never declared, a `when` expression that does not parse, a field name that the model invented — all of these are validation failures rather than mid-shoot discoveries. This is the one certainty that generative authoring makes *more* necessary rather than less. People mistype field names occasionally. Models produce plausible field names constantly, and a plausible field name is far more dangerous than an obvious typo. `additionalProperties: false` across every object is what turns an invented field into an error instead of a silently ignored instruction. One caveat worth stating plainly, because it is the difference between a guarantee and a claim: the four passes are what the specification *requires of a conformant validator*, and a validator performing only the structural pass will accept documents that cannot execute. Establish which passes yours implements before treating a clean result as certainty. See [Validation](/validation#conformance-note) for the passes and the error codes. ### 2. Authority: who is accountable for this? An agent is a participant like any other, and every agent participant carries `operated_by` — a chain that must terminate at a human or an organization. There is no anonymous actor in an OWG graph and no way to express one. So "an AI did it" is never a complete answer. The complete answer is *this agent, operated by this person, at this company, at this time*, and it is a query rather than an investigation. When an agent writes a workflow instead of merely performing a task, the same rule applies to the authorship: the document records what proposed it and who accepted it. See [Participants and authority](/participants-and-authority). ### 3. Assurance: how much of this is a guess? A machine-authored workflow is not uniformly confident. Some of it is certain — this file is that file, because the checksums match. Some of it is a matcher's opinion with a score attached. OWG requires that distinction to be carried in the document rather than flattened away. Every explicit relationship is `attested`, `asserted`, or `inferred`, and an `inferred` edge must carry both a confidence score and a stable identifier for the method that produced it. An accepted proposal stays `inferred` and gains `accepted_by` — it is never promoted, because promoting it would erase the fact that a machine proposed it and a named person agreed. The practical consequence is an acceptance policy instead of a review queue nobody reads: above your threshold, edges accept automatically; below your floor, never; the band in between is the only thing a human sees. And because `method` is recorded, a class of matches that later proves unreliable can be selected and re-reviewed as a set. The failure mode this guards against is specific and worth naming: **an agent that rounds its confidence to 1.0.** Overstating confidence is the one behaviour that corrupts the graph rather than merely wasting a review. See [Relationships and assurance](/relationships). ### 4. Bounds: what can it spend, touch, and re-run? An autonomous author working at machine speed needs limits expressed in the document rather than in somebody's judgement about how far it is likely to go. - `governance` ceilings cap the run — total cost, total re-routes. - `max_reroutes` bounds any gate that routes backwards, so a self-correcting loop terminates. - Permissions attach per object, so an agent handed a subgraph gets that subgraph and nothing adjacent to it. - `executor.credentials_key` names a credential rather than carrying one, so a generated document can never contain a secret it should not have had. The ceiling is part of the plan, and it is checked before execution rather than enforced by somebody noticing. ## What certainty does not mean It does not mean predicting the output of a generative task. A diffusion model will produce a different image on the second run, and no schema changes that. Certainty in OWG is about four other things, all of which *are* knowable in advance: - The **plan** is structurally sound and will not fail on a reference error. - The **authority** is named and terminates at a human. - The **bounds** are declared and enforceable. - The **record** the run will produce is determined by the document, so you know before you start what the audit trail will say. That last one matters more than it sounds. Structural edges — `used`, `produced`, `performed_by`, `ran_on` — fall out of executing the work, at no extra cost and requiring nobody's diligence. A workflow that runs has documented itself. High-volume AI work is precisely the case where nobody has time to document anything afterwards. ## Why this is now a legal question too EU AI Act Article 50 came into force on 2 August 2026. It requires deployers to disclose AI-generated public-interest content and deepfakes. Disclosure is a lineage question — *was a model involved anywhere upstream of this frame* — and a pipeline that cannot answer it cannot comply. OWG makes it answerable by requiring `ai_role` on every task and refusing to infer AI involvement from the executor type: an `agent` executor may be shuffling files, and a `saas_api` executor may be running a diffusion model, so only the explicit declaration counts. The obligation lands hardest on organizations whose agent-authored workflows generate content faster than anyone records what generated it. See [AI disclosure and compliance](/compliance). ## The symmetry that makes it work Nothing on this page is a special path for machine-authored documents. There is no agent mode, no separate schema, no relaxed validation for a trusted model. An agent writes in the same format a person would use, and the document is checked in exactly the same way. A workflow written by a supervisor gets the same scrutiny as one written by a model, and a workflow written by a model can be edited by a supervisor without conversion, because there is only one representation. That symmetry is what allows an agent-authored workflow to be trusted at all. Trust does not come from believing the author. It comes from the document being checkable, the authority being named, the confidence being honest, and the bounds being declared — regardless of who or what wrote it. Next: [For agents](/for-agents) for the authoring rules and pre-flight checklist, or [Core concepts](/concepts) for the model underneath. --- # Document structure *Section: Specification* > The OWG v0.92 document — every top-level field, the entity registries, parameters, and governance ceilings. An OWG document is a single JSON object describing a production workflow: five entity registries, the tasks that connect them, the parameters a caller supplies, and the ceilings the run is held to. ```json { "$schema": "https://openworkflowgraph.org/schemas/core/v0.92.json", "owg_version": "0.92", "id": "episodic-dailies", "profiles": ["core", "cloud-services"], "objective": "Turn camera cards into approved dailies with disclosure records.", "params": { }, "organizations": [ ], "participants": [ ], "infrastructure": [ ], "assets": [ ], "contexts": [ ], "relationships": [ ], "tasks": [ ], "governance": { } } ``` > **v0.92 is a draft specification.** It is the current version; v0.91 and v0.9 remain resolvable at their own schema URLs and a document targeting either stays valid — v0.92 is additive over v0.91, adding an optional `profiles` extension point on `context`, `asset`, and `relationships[]` (see [Extensions](#extensions)), and v0.91 was additive over v0.9, adding `task.fan_out` (see [Fan-out](/tasks#fan-out-v091)). Earlier internal iterations before v0.9 are superseded and are not described here. The version number will reach 1.0 when the specification is stable enough to promise compatibility. Until then, expect field-level change, and pin the schema you validate against. See the [changelog](/changelog) for what changed and when. ## Required fields Only two: | Field | Type | Notes | |---|---|---| | `owg_version` | string | Must be `"0.9"`, `"0.91"`, or `"0.92"`. Pattern `^\d+\.\d+(\.\d+)?(-[a-z0-9]+)?$` | | `id` | string | Pattern `^[a-zA-Z0-9][a-zA-Z0-9._-]*$`, 1–128 chars | Everything else is optional, including `tasks`. A document with registries and no tasks is a valid **registry document** — a shared project registry of participants, assets, and infrastructure that other documents reference. This is used in practice and is deliberate. ## All top-level fields | Field | Type | Purpose | |---|---|---| | `owg_version` | string | **Required.** The specification version this document targets | | `id` | string | **Required.** Document identifier | | `$schema` | string | Schema URI. Advisory, but if present must match the version | | `project_id` | string | Groups multiple documents under one production | | `profiles` | string[] | Conformance profiles in use. See [Profiles](/profiles) | | `description` | string | Max 2000 chars | | `objective` | string | The macro-objective agents are governed against | | `params` | object | Caller-supplied parameters. See below | | `organizations` | array | Registry — studios, vendors, service providers | | `participants` | array | Registry — humans, agents, services | | `infrastructure` | array | Registry — compute, storage, facilities, devices | | `assets` | array | Registry — media and data objects | | `contexts` | array | Registry — productions, scenes, deliverable groupings | | `context` | string \| object | The primary context: inline, or an id from `contexts[]` | | `relationships` | array | Explicit and proposed edges. See [Relationships](/relationships) | | `tasks` | array | The work, and the authoritative source of provenance edges | | `governance` | object | Run-level ceilings and gates | | `on_workflow_failure` | string | Task id to run if the workflow fails | | `webhook_url` | string | HTTPS endpoint for run status updates | | `timeout_seconds` | integer | Whole-workflow wall clock, ≥ 1 | | `metadata` | object | Freeform. See [Extensions](#extensions) | ## Parameters `params` declares what a caller must supply when submitting the document for execution. This is how a reusable workflow definition stays reusable: the definition holds no run-specific values, and the caller provides them. ```json { "params": { "source_url": { "type": "string", "required": true, "description": "Public HTTPS URL of the source file" }, "add_captions": { "type": "boolean", "required": false, "default": false }, "territories": { "type": "array", "required": false, "default": ["US"] } } } ``` Each entry is keyed by parameter name and takes: | Field | Type | Notes | |---|---|---| | `type` | enum | **Required.** `string` \| `number` \| `boolean` \| `array` \| `object` | | `required` | boolean | Default `false` | | `default` | any | Only permitted when `required` is `false` | | `description` | string | What the caller should supply | Two rules the schema enforces rather than merely stating: - A parameter with `required: true` **must not** carry a `default`. - A `default`, when present, must match the declared `type`. Tasks read parameters with `$.params.`. The grammar is in [Reference syntax](/reference-syntax). > **Validation resolves parameter references.** A task referencing `$.params.add_caption` when the document declares `add_captions` is a validation error, not a run-time surprise. See [Validation](/validation). ## Governance Run-level ceilings. These are hard limits, not advisory budgets — an engine must refuse to exceed them. ```json { "governance": { "max_total_reroutes": 12, "budget_usd": 4800, "disclosure_gate": true } } ``` | Field | Type | Purpose | |---|---|---| | `max_total_reroutes` | integer ≥ 0 | Ceiling on QC re-routes across the whole run | | `budget_usd` | number ≥ 0 | Spend ceiling for the whole run | | `disclosure_gate` | boolean | Blocks delivery tasks until compliance certifies | `max_total_reroutes` matters more than it looks: it is the circuit breaker on an agent-and-gate loop that would otherwise re-route indefinitely. See [Gates and re-routes](/tasks#gates-and-re-routes). ## Context Context is what scopes work. It may be given inline or by reference into `contexts[]`. ```json { "contexts": [ { "id": "ctx_ep104", "show_id": "SHOW_001", "scene_id": "sc_014", "territory": "US", "work_type": "creative-fictional" } ], "context": "ctx_ep104" } ``` | Field | Type | Notes | |---|---|---| | `id` | string | | | `show_id` | string | Production code | | `scene_id` | string | | | `territory` | string | e.g. `US`, `EMEA` | | `organization_id` | string | | | `work_type` | enum | `creative-fictional` \| `factual-news` \| `advertising` \| `sports` \| `other` | | `profiles` | object | Profile-contributed extensions, keyed by profile id (v0.92). See [Extensions](#extensions) | `work_type` is load-bearing for compliance rather than descriptive — it selects which disclosure obligations apply. See [AI disclosure](/compliance). Tasks and assets are **associated with** a context rather than contained in it, because the same asset means different things in different places. > **No shot identifier.** OWG v0.9 deliberately has no `shot_id`. MovieLabs' OMC v3.0 has no Shot entity; the video pipeline covering production scene through editorial shots and sequences arrives in a later OMC release. Rather than invent a mapping that will conflict, v0.9 omits it. Carry a shot identifier in `identifiers[]` with your own scope until OMC defines one. ## Extensions Every object defined by this specification sets `additionalProperties: false`. An unrecognised key is a **validation error**, not a silently accepted field. This is deliberate, and it is the single most important protection the schema offers an agent-authored document: a hallucinated or misspelled property fails loudly rather than validating and doing nothing. Extensibility is preserved through three explicit escape hatches: - **`metadata`** — a freeform object available on the document and on every entity. Put anything here. - **`x-` prefixed keys** — permitted anywhere, for vendor extensions that want to live alongside specified fields. - **`profiles`** (v0.92) — a namespaced object on `context`, `asset`, and `relationships[]`, keyed by profile id. Unlike `metadata`, a profile can publish a schema for its own key so the shape is documented and, once the validator resolves it, checkable — and unlike `x-`, two profiles' contributions on the same entity cannot collide, because each owns only its own key. See [Profiles → Registry extension profiles](/profiles#registry-extension-profiles). ```json { "id": "grade_0140", "metadata": { "internal_ticket": "POST-4471" }, "x-acme-cost-centre": "CC-88", "profiles": { "broadcast-newsroom": { "evidential_position": "PRIMARY" } } } ``` All three are ignored by the validator's structural checks today and preserved byte-for-byte on round-trip — `profiles` resolution against a named profile's own schema is not yet wired in, see [Profiles → Registry extension profiles](/profiles#registry-extension-profiles) for what that means in practice. Anything you would have been tempted to add as a bare unknown key belongs in one of the three. ## Schema identity | Artifact | URI | |---|---| | Core v0.92 (current) | `https://openworkflowgraph.org/schemas/core/v0.92.json` | | Core v0.91 | `https://openworkflowgraph.org/schemas/core/v0.91.json` | | Core v0.9 | `https://openworkflowgraph.org/schemas/core/v0.9.json` | | `broadcast-newsroom` profile v0.1 | `https://openworkflowgraph.org/schemas/profiles/broadcast-newsroom/v0.1.json` | | JSON-LD context | `https://openworkflowgraph.org/context/v0.9.jsonld` | Both are JSON Schema **draft 2020-12**. ## The JSON-LD context The context document maps OWG terms onto published vocabularies, so lineage is portable rather than locked to this schema: ```json { "@context": { "@version": 1.1, "owg": "https://openworkflowgraph.org/schemas/core/v0.9#", "omc": "https://movielabs.com/omc/json/schema/v3.0#", "prov": "http://www.w3.org/ns/prov#", "c2pa": "https://c2pa.org/specifications/...#", "did": "https://www.w3.org/ns/did#", "schema": "https://schema.org/", "id": "@id", "description": "schema:description", "owg_version": "schema:version", "objective": "owg:objective", "organizations": { "@id": "owg:hasOrganization", "@container": "@set" }, "participants": { "@id": "owg:hasParticipant", "@container": "@set" }, "infrastructure": { "@id": "owg:hasInfrastructure","@container": "@set" }, "assets": { "@id": "owg:hasAsset", "@container": "@set" }, "contexts": { "@id": "owg:hasContext", "@container": "@set" }, "relationships": { "@id": "owg:hasRelationship", "@container": "@set" }, "tasks": { "@id": "owg:hasTask", "@container": "@set" }, "used": { "@id": "prov:used", "@type": "@id" }, "produced": { "@id": "prov:generated", "@type": "@id" }, "produced_by": { "@id": "prov:wasGeneratedBy", "@type": "@id" }, "performed_by": { "@id": "prov:wasAssociatedWith", "@type": "@id" }, "operated_by": { "@id": "prov:actedOnBehalfOf", "@type": "@id" }, "works_for": { "@id": "prov:actedOnBehalfOf", "@type": "@id" }, "predecessor": { "@id": "prov:wasDerivedFrom", "@type": "@id" }, "depends_on": { "@id": "owg:dependsOn", "@type": "@id" }, "ran_on": { "@id": "owg:ranOn", "@type": "@id" }, "kind": "owg:participantKind", "role": "owg:role", "organization_id": { "@id": "omc:organization", "@type": "@id" }, "identity": "owg:identity", "credential_scope": { "@id": "owg:credentialScope","@container": "@set" }, "did_identity": { "@id": "did:subject", "@type": "@id" }, "type": "owg:entityType", "version_relation": "owg:versionRelation", "state": "owg:lifecycleState", "assurance": "owg:assurance", "confidence": "owg:confidence", "method": "owg:inferenceMethod", "credentials": { "@id": "c2pa:manifest", "@type": "@id" }, "identifiers": { "@id": "owg:identifier", "@container": "@set" }, "scope": "owg:identifierScope", "storage": "owg:storage", "provider": "owg:storageProvider", "locator": "owg:storageLocator", "timerange": "owg:timerange", "composes": { "@id": "owg:composes", "@container": "@set" }, "component": { "@id": "owg:component", "@type": "@id" }, "arc_type": "owg:compositionArc", "executor": "owg:executor", "ai_role": "owg:aiRole", "failure_mode": "owg:failureMode", "when": "owg:condition", "checks": { "@id": "owg:check", "@container": "@set" }, "on_pass": { "@id": "owg:onPass", "@type": "@id" }, "on_fail": { "@id": "owg:onFail", "@type": "@id" }, "editorial_signoff": "owg:editorialSignoff", "billing": "owg:billing", "show_id": "omc:Context.identifier", "territory": "omc:territory", "work_type": "owg:workType", "depicts": { "@id": "owg:depicts", "@type": "@id" }, "depicts_real_entity": "owg:depictsRealEntity", "governance": "owg:governance", "disclosure_gate": "owg:disclosureGate", "max_total_reroutes": "owg:maxTotalReroutes", "budget_usd": "owg:budget" } } ``` Three mapping decisions are worth explaining, because they are the ones a reader would otherwise query. **Provenance terms resolve to PROV, not OMC.** A JSON-LD term carries one IRI, and OMC's current identifier is a JSON Schema URL rather than a published RDF vocabulary. The OMC edge correspondences are normative in their own right and listed in [Standards bindings](/standards-bindings#omc-the-vocabulary-not-a-neighbour); a direct `omc:` RDF mapping follows MovieLabs' RDF/SHACL release. **Registry arrays are `@set`, not `@list`.** Order carries no meaning anywhere in an OWG document — execution order comes from `depends_on`, never from array position. Declaring them unordered makes that explicit to an RDF consumer rather than leaving it as an assumption. **`works_for` and `operated_by` share an IRI.** Both are PROV delegation; they differ only in whether the principal is an organization or a participant. Collapsing them in RDF is correct — the distinction lives in the endpoint's `kind`, which is where a query should look for it. Fields deliberately unmapped: `params`, `inputs`, `outputs`, `retry`, `subgraph`, `fan_out`, and `metadata`. These are execution mechanics rather than assertions about the world, and giving them RDF semantics would imply a meaning they do not carry. ## Where to go next - [Tasks and executors](/tasks) — the work objects, and the fields that route them - [Reference syntax](/reference-syntax) — the formal grammar for `$.` references and `when` expressions - [Relationships](/relationships) — explicit edges, assurance, and confidence - [Validation](/validation) — what is checked, and what a failure looks like --- # Tasks and executors *Section: Specification* > The task object in full — executors, provenance edges, dependencies, failure and compensation, retry, gates and re-routes. A task is the unit of work, of billing, and of observability. Everything else in a workflow exists to be consumed or produced by one. ```json { "id": "normalize", "label": "Normalise loudness and encode MP3", "type": "work", "executor": { "type": "service", "ref": "transcode_from_url", "environment": "saas" }, "ai_role": "none", "performed_by": "transcode_svc", "ran_on": "cloud_platform", "depends_on": ["edit"], "used": ["edited_wav"], "produced": ["episode_mp3"], "failure_mode": "CONTINUE", "billing": { "tier": "standard", "unit": "per_minute" }, "retry": { "max_attempts": 2, "initial_interval_seconds": 5 } } ``` Only `id` is required by the schema. In practice a task that omits `executor`, `performed_by`, or its provenance edges is a task that cannot be executed or audited — see the [pre-flight checklist](/for-agents#pre-flight-checklist). ## Identity and classification | Field | Type | Notes | |---|---|---| | `id` | string | **Required.** Pattern `^[a-zA-Z0-9_-]+$` | | `label` | string | Human-readable display name | | `description` | string | Max 500 chars | | `type` | string | Open set. Common: `work`, `qc`, `review`, `delivery`, `ingest`, `handoff` | | `tool` | string | Tool name; should match an executor declared in an active profile | | `metadata` | object | Freeform. See [Extensions](/specification#extensions) | `qc` and `review` are **gate types** — they carry the routing fields under [Gates and re-routes](#gates-and-re-routes). A `review` task may carry `editorial_signoff`. `delivery` is the type the [disclosure gate](/compliance#governance-ceilings) blocks. ## Provenance edges These four fields are the heart of the model, and they are **authoritative**: the graph's lineage comes from here and nowhere else. | Field | Type | PROV mapping | |---|---|---| | `performed_by` | string (participant id) | `wasAssociatedWith` | | `used` | string[] (asset ids) | `used` | | `produced` | string[] (asset ids) | `generated` (inverse) | | `ran_on` | string (infrastructure id) | `owg:ranOn` | An asset's own `produced_by` field is a **derived convenience** for readers walking the graph from the asset side. The task's `produced[]` is the source of truth, and validation enforces that the two agree. A task with no `used` is legitimate — it is an origin task, capturing or generating something that did not exist before. > **Only assert an edge you can guarantee.** Listing an asset in `used[]` claims the task consumed it. If the producing task is conditional, gate on `exists()` and pass the value through `inputs` instead — see the [worked pair](/reference-syntax#a-worked-pair). ## Executors The executor is what generalizes "call tool X" into an attribute, and it is the design choice that puts human, machine, and AI work in one accountable graph. ```json { "executor": { "type": "comfyui_graph", "ref": "graphs/sdxl_txt2img.json", "environment": "cloud" } } ``` | Field | Type | Notes | |---|---|---| | `type` | enum | **Required.** See below | | `ref` | string | Tool, application, or graph reference | | `environment` | enum | `cloud` \| `saas` \| `desktop` \| `on_prem` \| `manual` | | `credentials_key` | string | Names a secret held by the orchestrator | | `permissions` | string[] | e.g. `["read:assets", "write:outputs"]` | `executor.type` is one of nine values: | Value | Meaning | |---|---| | `service` | A deterministic software service | | `mcp` | A tool invoked over the Model Context Protocol | | `saas_api` | An external SaaS API | | `comfyui_graph` | A whole node-graph, treated as one task with a zoomable interior | | `comfyui_node` | A single node within such a graph | | `local_app` | A desktop application on a workstation (Nuke, Houdini, Resolve) | | `render_farm` | A batch job on an on-premises or cloud farm | | `agent` | An autonomous agent | | `human` | A person | Two rules that matter: - **AI involvement is never inferred from `executor.type`.** An `agent` executor may be doing deterministic file shuffling; a `saas_api` executor may be running a diffusion model. Declare [`ai_role`](/compliance#declaring-ai-involvement) explicitly. - **Secrets are referenced, never inlined.** `credentials_key` names a secret the orchestrator holds, so the document itself stays safe to commit and to hand to a vendor. ## Dependencies Dependencies are a list of task ids, with AND semantics — a task becomes eligible when all of its dependencies have finished. ```json { "id": "publish", "depends_on": ["edit", "thumbnail_review"] } ``` There is no edge object, no port-level wiring, and no cross-document dependency form. Ordering within `tasks[]` does not govern execution; `depends_on` does. **The dependency graph must be acyclic.** Acyclicity cannot be expressed in JSON Schema, so it is a [validator rule](/validation#pass-3-graph). Skip propagates: a task depending on a `skipped` task is itself `skipped`. That is what stops an optional branch from stalling the graph. ## Conditional execution ```json { "when": "$.params.add_captions == true" } ``` If the expression is false, the task is `skipped`. The expression language is small and fully specified — grammar, precedence, type rules, and the `exists()` function are in [Reference syntax](/reference-syntax#when-expressions). Unlike everything downstream of a run, `when` is checked at **validation** time: an unresolvable reference or a type-mismatched comparison is a validation error, not a run-time surprise. ## Failure and compensation ```json { "failure_mode": "SKIP_DEPENDENTS" } ``` | Value | Behaviour | |---|---| | `HALT` | Stop the entire workflow. **The default** | | `CONTINUE` | Mark this task failed, let other branches proceed | | `SKIP_DEPENDENTS` | Fail this task and skip everything downstream of it | | `COMPENSATE` | Run the named compensation task, then halt | `HALT` is the schema default, so omitting `failure_mode` is safe — but declaring it makes intent explicit, which matters on a task where a reader would reasonably expect something gentler. ### Compensation `COMPENSATE` requires `on_failure_compensate`, naming the task that undoes partial work: ```json { "id": "upload_master", "type": "delivery", "executor": { "type": "saas_api" }, "performed_by": "delivery_api", "used": ["master_prores"], "produced": ["delivery_receipt"], "failure_mode": "COMPENSATE", "on_failure_compensate": "rollback_partial_upload" } ``` Two validator rules keep this honest: `COMPENSATE` without `on_failure_compensate` is an error, and `on_failure_compensate` naming a task that does not exist is an error. The compensation task is an ordinary task — it can have its own executor, participant, and cost. ## Fan-out (v0.91) ```json { "id": "generate_all_idents", "executor": { "type": "agent" }, "subgraph": { "format": "owg", "ref": "graphs/generate_ident.owg.json" }, "fan_out": { "over": "$.params.shot_list", "as": "shot", "max_concurrency": 500, "tolerated_failure_percentage": 3 } } ``` `fan_out` runs one task — and its `subgraph`, if it has one — once per item in a list, concurrently, instead of once. It exists for the case `depends_on` and `failure_mode` were never meant to cover: not "these five named tasks," but "this same subgraph, several hundred times, over a list only known at run time." | Field | Type | Notes | |---|---|---| | `over` | string | **Required.** A `$.` reference resolving to an array — `$.params.shot_list`, or `$.tasks.list_shots.outputs.items` | | `as` | string | Name bound to the current item inside this instance, referenced as `$.fan_out.`. Defaults to `item` | | `max_concurrency` | integer ≥ 1 | Upper bound on instances running at once. This only *lowers* an engine's own ceiling — it can never raise one | | `tolerated_failure_percentage` | number, 0–100 | The fan-out task is treated as **succeeded** if no more than this percentage of instances fail | | `tolerated_failure_count` | integer ≥ 0 | Same idea, as an absolute count. Mutually exclusive with the percentage — declaring both is `OWG_FANOUT_TOLERANCE_CONFLICT` | ### Why this needed its own field, not just `failure_mode: CONTINUE` `failure_mode: CONTINUE` (see [above](#failure-and-compensation)) already isolates one task's failure from its siblings — and still should be your first reach for a handful of named parallel branches, like the [worked pair](/implementation-considerations#composing-independent-branches) of a video path and an audio path. What it does not give you is a **threshold**: with five hand-authored sibling tasks, "did enough of them succeed" is a judgment call a human makes reading the run. With 500 instances of the same subgraph, nobody is reading 500 rows — the run itself has to be able to say "497 of 500 succeeded, that's a pass" without a human deciding it after the fact. That is the one thing `fan_out` adds that composing existing fields cannot: a **declared, checkable pass/fail line** for a whole batch, evaluated by the engine, not eyeballed afterward. ### Addressing instances Each instance is addressable in run records as `$.tasks.[]` rather than `$.tasks.` — see [Run records](/runs#fan-out-attempts) for how attempts are recorded per instance, and how a fan-out task's own `succeeded`/`failed` status is derived from its tolerance. ### What `fan_out` does not change - **`used`/`produced` still describe the task once**, not once per instance — they name the *kind* of thing consumed and produced, not each instance's specific asset. Per-instance lineage lives in the run's attempt records. - **A fan-out task can still declare `failure_mode`.** It governs what happens to *this task's own dependents* if the fan-out as a whole fails (tolerance exceeded) — the same semantics as any other task, layered on top of, not instead of, the tolerance check. - **`max_concurrency` is a ceiling an author states, not a capacity an author is promised.** An engine may run fewer instances at once than the ceiling allows — for its own rate limits, quota, or scheduling reasons — but must never exceed it. See [Implementation considerations](/implementation-considerations#fan-out-concurrency-is-a-ceiling-not-a-guarantee). ## Retry ```json { "retry": { "max_attempts": 3, "initial_interval_seconds": 10, "backoff_coefficient": 2.0, "retryable_errors": ["RATE_LIMIT", "QUOTA_EXCEEDED", "TRANSIENT_ERROR"] } } ``` Delay before attempt *n* is `initial_interval_seconds × backoff_coefficient ^ n`. | Field | Range | Default | |---|---|---| | `max_attempts` | 1–10 | 1 | | `initial_interval_seconds` | 0.1–3600 | 1 | | `backoff_coefficient` | 1.0–10.0 | 1.0 | | `retryable_errors` | string[] | empty — any error retries | `retryable_errors` values are executor-defined; the specification does not enumerate error codes, because they belong to whatever system the executor calls. Declare them in your [profile](/profiles). Retries create new **task attempts** under the same run. A task that failed twice and succeeded on the third try is three attributed activities, not one overwritten record — which is why `$.task.attempt` is addressable. ## Gates and re-routes QC and review tasks route on outcome rather than merely succeeding or failing. This is the mechanism that makes a human review gate a first-class part of an automated pipeline, and it is what lets an agent be held to a standard rather than merely invoked. ```json { "id": "qc_review", "type": "qc", "executor": { "type": "service", "ref": "qc_validate" }, "performed_by": "qc_svc", "depends_on": ["grade"], "used": ["graded_master"], "checks": ["duration_match", "codec_compliance", "audio_sync"], "on_pass": "deliver", "on_fail": "regrade", "max_reroutes": 3, "reroute_feedback": { "to_task": "regrade", "carry": ["qc_notes"] } } ``` | Field | Type | Purpose | |---|---|---| | `checks` | string[] | Named checks the gate evaluates. Profile-defined | | `on_pass` | string | Task id to route to on pass | | `on_fail` | string | Task id to route to on fail | | `max_reroutes` | integer ≥ 0 | Per-gate re-route ceiling | | `reroute_feedback` | string \| object | Where feedback goes, and what context travels with it | | `editorial_signoff` | object | `{ editor (required), statement, timestamp }` | `reroute_feedback` in object form takes `to_task` and `carry` — an array of output keys that travel back to the target task, so the agent being asked to try again receives the notes explaining why. Two ceilings apply, and both are needed: `max_reroutes` bounds one gate, and [`governance.max_total_reroutes`](/specification#governance) bounds the whole run. Without the run-level ceiling, three gates each permitting three re-routes can still loop far longer than anyone intended. ## Billing ```json { "billing": { "tier": "standard", "unit": "per_minute" } } ``` The core specification imposes no billing vocabulary — `tier` and `unit` are free strings, defined by the active [profile](/profiles). This is deliberate: pricing models change faster than schemas should. ## Timeouts `timeout_seconds` bounds one task, minimum 1. A whole-workflow ceiling is set at the [document level](/specification#all-top-level-fields). ## Data flow Tasks pass values through `inputs` and `outputs`. ```json { "id": "web_delivery", "executor": { "type": "service", "ref": "transcode_video" }, "depends_on": ["ingest"], "inputs": { "input_key": "$.tasks.ingest.outputs.output_key", "output_format": "webm" }, "outputs": { "output_key": "jobs/${workflow.id}/${task.id}/delivery.webm", "content_type": "video/mp4" }, "used": ["normalized_mp4"], "produced": ["delivery_webm"] } ``` `outputs` recognises four conventional keys — `output_key`, `content_type`, `duration_seconds`, `file_size_bytes` — and permits any others your profile declares. The last two are set by the executor at run time rather than authored. Keep the distinction clear: **`inputs`/`outputs` carry values; `used`/`produced` assert lineage.** A task typically has both, and they describe different things — the input is a storage key, the used asset is the governed object that key belongs to. Reference syntax, interpolation, and escaping are specified in [Reference syntax](/reference-syntax). --- # Assets and versioning *Section: Specification* > Asset identity, storage locators, the five typed version relations, lifecycle states, and how composition and external identifiers ride along. An asset in OWG is a typed, versioned, governed object that carries a **locator** to wherever the bytes physically live. The asset node holds governance; the storage system holds content. That separation is what lets the same governance record survive a migration between clouds. ```json { "id": "delivery_mp4", "role": "delivery", "type": "video.delivery", "content_type": "video/mp4", "version_relation": "representation", "predecessor": "source_mov", "produced_by": "transcode", "state": "delivered", "storage": { "provider": "s3", "locator": "s3://studio-media/out/clip.mp4", "zone": "us-east-2" } } ``` Only `id` is required. ## Fields | Field | Type | Notes | |---|---|---| | `id` | string | **Required.** Document-local identifier | | `identity` | string | Global content-derived identity — content hash or credential manifest reference | | `type` | string | Asset kind. Open set: `video`, `audio`, `image`, `text`, `model`, `lora`, `document` | | `role` | string | Its function in this workflow: `source`, `delivery`, `thumbnail`, `captions` | | `content_type` | string | MIME type | | `storage` | object | Provider, locator, and optional zone or timerange | | `produced_by` | string | Task id — derived; the task's `produced[]` is authoritative | | `predecessor` | string | The asset this one came from | | `version_relation` | enum | How it relates to its predecessor. See below | | `state` | enum | Lifecycle position. See below | | `assurance` | enum | `attested` \| `asserted` \| `inferred`. See below | | `credentials` | object | e.g. `{ "scheme": "c2pa" }` | | `identifiers` | array | External identifier pass-through | | `depicts` | — | What the asset shows | | `depicts_real_entity` | — | Deepfake trigger. See [AI disclosure](/compliance) | | `composes` | array | Composition arcs. See below | | `profiles` | object | Profile-contributed extensions, keyed by profile id (v0.92). See [Extensions](/specification#extensions) | **`id` versus `identity`.** `id` is local to the document and is what tasks reference. `identity` is the global, content-derived identity — a hash, URN, or content-credential manifest reference — and it is what lets the same asset be recognised across many runs, systems, and organizations. Populate both. ## Typed version relations This is one of the most consequential details in the specification. The relationship between versions is **typed, not merely numbered**. | Relation | Meaning | Example | |---|---|---| | `revision` | Incremental change, same context | Rejected comp re-rendered after notes | | `variant` | Change for a different context | Pristine versus battle-damaged prop | | `derivation` | A new thing that acknowledges its source | A model trained from a base checkpoint | | `representation` | Pure format change, same meaning | The proxy of a master | | `alternative` | One pick from equivalent options | Concept sketches to choose between | Correctly labelling the relation is the difference between semantically correct lineage and a version counter that quietly lies. Two rules are worth memorising: - **A proxy is not a `revision` of the master.** It is a `representation` — same meaning, different format. - **An approved retake is a `revision`, not a new asset.** Get these wrong and every downstream question — *which shots use the latest approved version?*, *what is this derived from?* — returns a confidently incorrect answer. A numbered version chain cannot distinguish "this replaces that" from "this is another way of expressing that", and those have opposite implications for approval. ```json { "id": "hero_prop_battle_damaged", "version_relation": "variant", "predecessor": "hero_prop_pristine" } ``` ```json { "id": "master_proxy_h264", "version_relation": "representation", "predecessor": "master_prores" } ``` ## Lifecycle states | State | Meaning | |---|---| | `draft` | Work in progress | | `growing` | Still being appended to — a live feed. See below | | `in_review` | Submitted, awaiting a decision | | `accepted` | Approved | | `delivered` | Handed over | | `superseded` | Replaced by a newer version | | `rejected` | Not approved — and **retained** in the graph | A rejected asset stays in the graph as a rejected state rather than disappearing. Nothing is overwritten, which is what makes the audit trail a record rather than a reconstruction. ### Growing assets `growing` is how OWG handles media that is still being captured, without mirroring a single byte: - While an asset is `growing`, its `identity` is the **source UUID** from `identifiers[]` rather than a content hash — you cannot hash a file that is still being written. - The producing ingest task **stays open** for the duration. - When the referenced range closes, fixity is computed over the now-bounded range and the state advances. This is what carries the model into live sports and news. See the [TAMS binding](/standards-bindings#bbc-tams-growing-assets-and-opaque-timeranges). ## Storage locators ```json { "storage": { "provider": "tams", "timerange": "[1500:0_1620:0)" } } ``` `provider` is an open string. Commonly used values — an implementation may register its own: | Provider | Meaning | |---|---| | `s3`, `gcs` | Object storage | | `saas_dam`, `iconik`, `frameio` | SaaS asset managers | | `camera_media_card` | Media not yet ingested | | `tams` | Time-addressable media store | | `usd_resolver` | An OpenUSD logical asset identifier | | `protected_store` | A hardened or protective storage tier | > **`timerange` is stored opaquely.** OWG does not parse or interpret time. The half-open `[s:ns_s:ns)` syntax belongs to TAMS, and resolving it to bytes is TAMS's job. OWG deliberately does not attempt to solve the International Atomic Time to SMPTE timecode mapping. ## External identifiers OWG never strips an incoming identifier, even one it does not use itself. ```json { "identifiers": [ { "scope": "tams.source", "id": "0f7e...-a91c" }, { "scope": "usd.assetInfo", "id": "asset://hero_prop/v23" }, { "scope": "frameio.id", "id": "b2c8e9f1" }, { "scope": "c2pa", "id": "urn:uuid:9e21..." } ] } ``` Both `scope` and `id` are required per entry. Documented scopes: `tams.source`, `usd.assetInfo`, `frameio.id`, `c2pa`, `urn`. Preserved identifiers are the highest-confidence signal for relating objects that arrive from different systems — see [declared assurance](/standards-bindings#2-declared-assurance-on-every-cross-system-edge). ## Composition `composes[]` records that an asset is assembled from other assets, using the arc vocabulary of the composition system it came from. ```json { "id": "shot_0140_assembly", "composes": [ { "component": "hero_prop_v23", "arc_type": "reference", "prim_path": "/World/Set/Hero", "variant_selection": "lod=high" }, { "component": "env_backlot_v4", "arc_type": "payload", "prim_path": "/World/Env" } ] } ``` Arc types: `reference`, `payload`, `sublayer`, `inherit`, `specialize`, `variant`. Only `component` is required. **The graph never descends below the published-asset boundary.** `prim_path` and `variant_selection` are opaque edge properties — OWG stores them and does not interpret them. That boundary is what keeps a governance graph from turning into a scene graph. ## Assurance and credentials ```json { "assurance": "attested", "credentials": { "scheme": "c2pa" } } ``` Assurance states how strongly an asset's identity is backed. It uses the same three levels, and the same rule about confidence, as [relationships](/relationships#the-three-assurance-levels): | Level | Meaning | Requires `confidence` + `method` | |---|---|---| | `attested` | A cryptographically verified content credential backs this | No | | `asserted` | Declared by a participant or system, without cryptographic attestation | No | | `inferred` | Identity established by a matcher rather than declared | **Yes** | An inferred identity carries its score and method, exactly as a proposed edge does: ```json { "id": "recovered_plate_0140", "identity": "sha256:9f2c…", "assurance": "inferred", "confidence": 0.79, "method": "m-corroborated-2" } ``` The schema enforces the pairing in both directions: `inferred` without `confidence` and `method` is a validation error, and `attested` or `asserted` **with** them is also an error — a confidence score on a human assertion is meaningless. Offline and physical steps default to `asserted`: a manually logged camera card is an assertion, whereas signed device output is an attestation. The ingest station — where physical provenance becomes digital provenance — is the boundary where one can become the other. Note that the C2PA declaration lives on the **asset**, not on the task that produced it. --- # Participants and authority *Section: Specification* > How humans, agents, and services are declared, and how an agent's authority chains to a responsible human. Every actor in a workflow is a participant, whether it breathes or not. That uniformity is deliberate: it is what lets human and machine work be scheduled, attributed, and audited under one contract. ```json { "participants": [ { "id": "podcast_host", "kind": "human", "name": "Podcast Host", "role": "host", "organization_id": "org_studio_media" }, { "id": "caption_agent", "kind": "agent", "name": "Caption Agent", "operated_by": "podcast_host", "organization_id": "org_studio_media" }, { "id": "publish_api", "kind": "service", "name": "Example Publishing" } ] } ``` ## Fields `id` and `kind` are both required — `kind` is the only required field beyond an identifier anywhere in the entity registries, which signals how load-bearing it is. | Field | Type | Notes | |---|---|---| | `id` | string | **Required** | | `kind` | enum | **Required.** See below | | `name` | string | Display name | | `role` | string | Open set: `host`, `editor`, `vfx-supervisor`, `colorist` | | `organization_id` | string | The organization this participant belongs to | | `works_for` | string | Organization-level delegation (PROV `actedOnBehalfOf`) | | `operated_by` | string | **For agents:** the accountable participant | | `identity` | string \| object | Verifiable identity. See below | ### `kind` | Value | Meaning | |---|---| | `human` | A natural person | | `agent` | Autonomous software, including generative AI | | `service` | A deterministic software service | | `department` | An organizational unit | | `organization` | An organization acting as a participant | The `agent` / `service` distinction is the one that matters most. A `service` does the same thing every time. An `agent` exercises judgement, which is why it needs an accountability chain that a service does not. > A participant with no `organization_id` is a third party — an external SaaS, say. That is legitimate and used in practice, but it means nothing in the graph vouches for it. Treat unowned participants as a review signal. ## Agent authority An agent's authority is never ambient. It is a declared edge to a responsible principal: ```json { "id": "agent_dubber", "kind": "agent", "name": "Localization Agent", "operated_by": "s.okafor", "organization_id": "org_studio", "identity": { "scheme": "did", "id": "did:web:example.com:agents:dubber", "credential_scope": ["read:assets", "write:outputs"] } } ``` - **`operated_by`** names the participant on whose authority the agent acts. It maps to PROV `actedOnBehalfOf`. The generative-AI profile makes it **required** for agent executors. - **`works_for`** is the organization-level equivalent. - **`identity.credential_scope`** bounds what the agent may do. This is what makes *"who did this, and on whose say-so"* a query rather than an investigation, and it is what makes AI disclosure fall out of the data instead of being compiled by hand. An asset produced by `agent_dubber` traces to `s.okafor` and `org_studio` with no extra record-keeping. ### Chains `operated_by` may chain — an agent operated by an agent operated by a human. The chain **must terminate** at a `human` or `organization` participant; an authority loop is meaningless, and a validator is required to reject one. See [Validation](/validation#pass-3-graph). ### Identity `identity` accepts a bare string or an object: | Field | Notes | |---|---| | `scheme` | Open set. `urn`, `did` | | `id` | **Required** within the object form | | `credential_scope` | string[] — what this identity is permitted to do | Decentralized identifiers (`did:`) are the intended direction for agent identity, so that an agent's identity is verifiable rather than merely asserted by whoever wrote the document. > **`credential_scope` is a declaration, not an enforcement mechanism.** The specification defines where an agent's permitted scope is recorded; enforcing it is the implementation's responsibility. Do not assume a document that declares a scope was executed within it. ## Organizations ```json { "organizations": [ { "id": "org_studio_media", "name": "Example Studio Media" }, { "id": "org_vfx_north", "name": "North VFX", "role": "vfx-vendor" } ] } ``` Only `id` is required. `role` is an open set — `studio`, `vfx-vendor`, `broadcaster`, `service-provider`. Organizations are the top of the delegation chain and the natural boundary for a turnover: cross-company work means edges that cross an organization boundary, which is exactly what a [scoped subgraph](/subgraphs-and-turnovers) controls. ## Infrastructure Infrastructure is where work runs. Declaring it is what makes cost attribution and multi-cloud portability properties of the graph rather than of a spreadsheet. ```json { "infrastructure": [ { "id": "studio_ws", "type": "workstation", "owner": "org_studio_media", "apps": ["audacity", "premiere"] }, { "id": "gpu_cloud", "type": "gpu_compute", "owner": "org_mediasvc", "spec": "A100 80GB" }, { "id": "cloud_platform", "type": "saas_platform" } ] } ``` | Field | Type | Notes | |---|---|---| | `id` | string | **Required** | | `type` | string | Open set: `cloud_gpu`, `gpu_compute`, `workstation`, `render_farm`, `saas_platform`, `storage`, `camera`, `facility` | | `owner` | string | Organization id | | `location` | string | Region or physical site | | `host`, `product` | string | Platform detail | | `spec` | string \| object | e.g. `"A100 80GB"` | | `apps` | string[] | Applications available on this target | A task's `ran_on` points here. Because capability is described rather than assumed, an engine can match work to a target dynamically instead of relying on hand-routing — the difference between a workflow that runs where it was told and one that runs where it fits. --- # Relationships and assurance *Section: Specification* > Explicit edges, the three assurance levels, and exactly where a machine proposal's confidence score and method live in the payload. Most edges in an OWG graph are never written by hand. They fall out of running the work: a task that declares `used` and `produced` has stated its own lineage. Those are **structural edges**, and they cost nothing. This page is about the other kind — edges that require judgement to assert, including every edge a machine proposes while federating two systems together. Those need somewhere to live and something to say about how much they should be trusted. ## Structural versus explicit | | Structural | Explicit | |---|---|---| | Where | Implicit in `tasks[]` — `used`, `produced`, `performed_by`, `ran_on`, `depends_on` | The `relationships[]` array | | Cost | Free. A by-product of describing work | Requires intelligence or a human | | Assurance | Always `asserted` by the executing system | Any of three levels | | Examples | This task consumed that plate | This asset is the same as that one in Frame.io | The rule: **if executing the workflow proves the edge, it is structural. If something had to decide, it is explicit.** Never duplicate a structural edge in `relationships[]`. ## The relationship object ```json { "relationships": [ { "id": "rel_0001", "relation": "same_as", "from": { "kind": "asset", "id": "plate_0140" }, "to": { "kind": "asset", "id": "frameio_b2c8e9f1" }, "assurance": "inferred", "confidence": 0.82, "method": "m-corroborated-2", "proposed_by": "connector_frameio", "proposed_at": "2026-08-20T09:14:00Z" } ] } ``` | Field | Type | Notes | |---|---|---| | `id` | string | **Required.** Identifies the edge, so a decision about it can be recorded | | `relation` | string | **Required.** The predicate. See [vocabulary](#relation-vocabulary) | | `from` | object | **Required.** `{ kind, id }` | | `to` | object | **Required.** `{ kind, id }` | | `assurance` | enum | **Required.** `attested` \| `asserted` \| `inferred` | | `confidence` | number | 0.0–1.0. **Required when `assurance` is `inferred`**; forbidden otherwise | | `method` | string | **Required when `assurance` is `inferred`**; forbidden otherwise | | `proposed_by` | string | Participant id — the connector, agent, or person proposing | | `proposed_at` | string | RFC 3339 timestamp | | `accepted_by` | string | Participant id of whoever confirmed it. Absent while pending | | `accepted_at` | string | RFC 3339 timestamp of acceptance | | `metadata` | object | Freeform | | `profiles` | object | Profile-contributed extensions, keyed by profile id (v0.92). See [Extensions](/specification#extensions) | `kind` in an endpoint is one of `asset`, `participant`, `task`, `organization`, `infrastructure`, `context`. The conditional requirement is enforced by the schema, not merely described: an `inferred` relationship **without** `confidence` and `method` is a validation error, and an `attested` or `asserted` relationship **with** them is also an error. A confidence score on a human assertion is meaningless, and an inference without one is unauditable. ## The three assurance levels | Level | Meaning | Carries confidence | |---|---|---| | `attested` | Backed by a verified content credential — a C2PA manifest, a signed device output | No | | `asserted` | Declared by a system of record, or confirmed by a human | No | | `inferred` | Proposed rather than declared, carrying its score and method identifier | **Yes, required** | These three appear in the same form wherever assurance is expressed — on relationships here, and on [assets](/assets-and-versioning#assurance-and-credentials). One vocabulary, one rule about confidence. ## An inferred edge, end to end This is the full lifecycle of a machine proposal, which is what makes federation safe rather than merely automatic. **1. A connector proposes.** It has found a plausible match between an asset in the graph and one in an external system, and scores it by whatever method it used: ```json { "id": "rel_0042", "relation": "same_as", "from": { "kind": "asset", "id": "plate_0140" }, "to": { "kind": "asset", "id": "ext_frameio_b2c8e9f1" }, "assurance": "inferred", "confidence": 0.74, "method": "m-probabilistic-1", "proposed_by": "connector_frameio", "proposed_at": "2026-08-20T09:14:00Z" } ``` **2. An acceptance policy decides what happens next.** What confidence warrants automatic acceptance, what warrants review, and what is discarded outright is **entirely an implementation decision** — the specification takes no position, because the right policy depends on the content, the systems involved, and the consequences of a wrong match. What the specification does require is that whatever happens next is recorded. **3. A human confirms.** Acceptance does not overwrite the proposal — it annotates it: ```json { "id": "rel_0042", "relation": "same_as", "from": { "kind": "asset", "id": "plate_0140" }, "to": { "kind": "asset", "id": "ext_frameio_b2c8e9f1" }, "assurance": "inferred", "confidence": 0.74, "method": "m-probabilistic-1", "proposed_by": "connector_frameio", "proposed_at": "2026-08-20T09:14:00Z", "accepted_by": "s.okafor", "accepted_at": "2026-08-20T11:03:00Z" } ``` Note what is preserved. The edge **stays `inferred`** with its original score and method, and gains an acceptance. It does not get promoted to `asserted`, because that would erase the fact that a machine proposed it at 0.74 confidence and a named person waved it through. Six months later, when the match turns out to be wrong, the question "who accepted this and on what evidence" has an answer. This is the graph's own provenance rules applied to the graph's own construction — the claim is only meaningful if the acceptance is recorded, which is why `accepted_by` exists. **4. Acceptance may be automatic.** Where an implementation's policy accepts a proposal without human review, the accepting actor is still named: ```json { "id": "rel_0043", "relation": "same_as", "from": { "kind": "asset", "id": "plate_0141" }, "to": { "kind": "asset", "id": "ext_frameio_c9d1f0a2" }, "assurance": "inferred", "confidence": 1.0, "method": "m-exact-identifier", "proposed_by": "connector_frameio", "proposed_at": "2026-08-20T09:14:00Z", "accepted_by": "connector_frameio", "accepted_at": "2026-08-20T09:14:00Z" } ``` The connector accepted its own proposal, and says so. An auto-accept is still an accept, with an actor. ## The `method` field `method` is an opaque identifier naming **which** method produced the inference. The specification requires that it be present and stable; it does not define a vocabulary, and an implementation's methods are its own. The reason it is required at all is auditability rather than description: when a class of match later proves unreliable, you need to select every edge produced by that method and re-review the set. That is only possible if the method is recorded as a stable, filterable value. Two rules follow: - **Keep method identifiers stable.** Renaming one orphans every edge produced under the old name. - **Make them specific enough to be useful as a filter.** A single value used for every inference tells a future reviewer nothing. > **Confidence is not accuracy.** `confidence` states how strongly *that* method believes the match, in its own terms. Two different methods' scores are not directly comparable — which is exactly why `method` is required alongside, and why acceptance policy belongs to the implementation that knows what its own scores mean. ## Relation vocabulary An open set — the specification does not constrain `relation`, because domains will need predicates it has not anticipated. Documented values: | Relation | Meaning | |---|---| | `same_as` | The two endpoints are the same real thing in different systems | | `derived_from` | The `from` endpoint originates in the `to` endpoint | | `version_of` | Endpoints are versions of one lineage. Prefer asset `predecessor` where it applies | | `depicts` | The asset shows the referenced subject | | `features` | The asset features the referenced participant or character | | `part_of` | Structural containment | | `references` | A weaker link than `derived_from` — cites without deriving | **Direction matters and is easy to get backwards.** For `derived_from`, `from` is the newer thing and `to` is the source: the edge reads *from derived_from to*. Every relation reads in that order — subject, predicate, object. Asserting the reverse silently inverts lineage answers, so it is worth a test. ## Where semantic edges come from An edge like `features` requires understanding the content. In OWG, whatever produced that understanding is **itself a task** — with an executor, a participant, and a cost: ```json { "tasks": [ { "id": "tag_faces", "type": "work", "label": "Identify featured performers", "executor": { "type": "agent" }, "ai_role": "assistive", "performed_by": "agent_vision", "used": ["master_mp4"], "produced": ["face_tags"], "failure_mode": "CONTINUE" } ], "relationships": [ { "id": "rel_0100", "relation": "features", "from": { "kind": "asset", "id": "master_mp4" }, "to": { "kind": "participant", "id": "perf_lead" }, "assurance": "inferred", "confidence": 0.88, "method": "m-subject-recognition", "proposed_by": "agent_vision", "proposed_at": "2026-08-20T14:02:00Z" } ] } ``` So an inferred relationship carries the same provenance as any other piece of work: `proposed_by` names the agent, and the agent's own `operated_by` chain names the human accountable for it. Enrichment is not a special case that escapes accountability. ## Guidance - **Do not mirror structural edges here.** If a task's `used`/`produced` already states it, adding a relationship is duplication that will drift. - **Always populate `proposed_by`.** An edge with no proposer cannot be reviewed, revoked as a class, or trusted. - **Never round confidence to 1.0** unless the match is genuinely deterministic. Overstating confidence is the one agent failure mode that corrupts the graph rather than merely wasting a review. - **Keep `method` stable.** It is a filter key. Renaming a method orphans every edge produced under the old name. --- # Subgraphs and turnovers *Section: Specification* > How a task nests an interior graph, and how that same mechanism scopes a cross-company hand-off to exactly what a vendor needs. One mechanism serves two purposes that look unrelated until you see them side by side: **zooming into the interior of a complex task**, and **handing a vendor a scoped slice of a production**. Both are `subgraph`. ## The field `subgraph` takes an object with a required `format` discriminator, and either a `ref` to a stored graph or an inline `graph`. ```json { "id": "generate", "label": "SDXL text→image graph", "executor": { "type": "comfyui_graph", "ref": "sdxl_txt2img", "environment": "cloud" }, "subgraph": { "format": "comfyui", "ref": "graphs/sdxl_txt2img.json" } } ``` | Field | Type | Notes | |---|---|---| | `format` | string | **Required.** `owg` for a nested OWG document; any other value names a foreign graph format | | `ref` | string | Path or URI to a stored graph. Mutually exclusive with `graph` | | `graph` | object | The graph inline. Mutually exclusive with `ref` | **A bare string is shorthand for a foreign graph.** `"subgraph": "graphs/x.json"` means `{ "format": "opaque", "ref": "graphs/x.json" }` — convenient, and it matches how most interiors are actually referenced. Nesting an OWG document requires the explicit object form, because that case carries real semantics. ### `format: "owg"` — a nested document The interior is an OWG document, and the interface between parent and child is already specified by fields that exist: - The child's **`params` are satisfied by the parent task's `inputs`.** A child parameter with `required: true` and no matching parent input is a validation error. - The child's **terminal task outputs become the parent task's `outputs`.** A terminal task is one no other task in the child depends on. - The child is **validated recursively**, all four passes. ```json { "id": "vendor_comp_0140", "type": "handoff", "inputs": { "plate_key": "$.tasks.pull_plate.outputs.output_key" }, "outputs": { "comp_key": null }, "subgraph": { "format": "owg", "ref": "turnovers/shot_0140_north.owg.json" } } ``` No new binding vocabulary is needed, which is the point — `params`, `inputs`, and `outputs` already describe exactly this relationship. ### Any other `format` — an opaque interior The interior is **stored, never parsed**. OWG does not validate it, resolve references into it, or interpret its structure. The parent task's `inputs`, `outputs`, `used`, and `produced` are the entire contract. This is the same discipline as the [USD boundary](/standards-bindings#openusd-composition-arcs-become-typed-edges) and [TAMS timeranges](/assets-and-versioning#storage-locators): a foreign structure is referenced, not absorbed. ### `executor.ref` and `subgraph` describe different things They are not duplicates and are not required to match. `executor.ref` names **what the engine invokes**; `subgraph` names **the interior a viewer renders or a validator descends into**. A task may have either, both, or neither. ### Nesting limits - **Maximum nesting depth is 8.** Exceeding it is a validation error (`OWG_SUBGRAPH_DEPTH`). The limit exists so a validator can recurse without an unbounded stack, and no legitimate production graph approaches it. - **Subgraph references must not form a cycle.** A document that reaches itself through a chain of `format: "owg"` references is a validation error (`OWG_SUBGRAPH_CYCLE`). This is checked in [pass 3](/validation#pass-3-graph), alongside dependency acyclicity. ## Nesting: the zoomable interior A generative pipeline is a node graph of its own. Modelling every ComfyUI node as a first-class OWG task would flood the governance graph with detail nobody governs — and would break the rule that OWG references adjacent systems rather than absorbing them. Instead, the whole node graph is **one task with a zoomable interior**. The graph definition is the interior; OWG treats it as a single accountable unit with an executor, a participant, a cost, and used/produced edges. ```json { "tasks": [ { "id": "generate", "executor": { "type": "comfyui_graph" }, "ai_role": "generative", "performed_by": "concept_bot", "ran_on": "gpu_cloud", "used": ["text_prompt"], "produced": ["generate_out"], "subgraph": { "format": "comfyui", "ref": "graphs/sdxl_txt2img.json" } } ] } ``` From the governance layer this is one step that consumed a prompt and produced a credentialed image. From inside, it is a network of nodes. Both views are true at once, and neither pollutes the other. The `comfyui_node` executor type exists for the cases where a single node genuinely does need to be its own governed task. This is the same discipline as the [USD boundary](/standards-bindings#openusd-composition-arcs-become-typed-edges): the graph does not descend below the published boundary. ## Turnovers: the scoped hand-off Now the second use. A show is one graph of many thousands of nodes. A vendor needs a specific slice of it — and must not receive the rest. A turnover extracts precisely the subgraph the vendor needs: plates, an EDL, a LUT, notes. Only that crosses the wall, with per-object permissions attached. The vendor's own pipeline stays their business. Two properties make this an enforcement mechanism rather than a packaging convention: - **Scope** is bounded by what the subgraph contains. The vendor sees the shot they were handed and nothing beyond it. - **Time** is bounded by the task's own validity window. Access expires when the task closes, rather than lingering until someone remembers to revoke it. Nothing is reachable by default, and every grant is itself a structural edge in the same graph that records who touched what. This is zero trust by construction rather than by policy — see [Security model](/security-model). ### The return path The valuable half is what happens when work comes back. A returning comp does not land in an inbox to be reconciled by filename. It **reconnects to the show's graph** with lineage attached: which participant, what plate, how many rounds. The turnover created the edges in advance; the return populates them. ```json { "id": "vendor_comp_0140", "type": "handoff", "performed_by": "org_vfx_north", "used": ["plate_0140", "edl_reel2", "lut_show"], "produced": ["comp_0140_v4"], "subgraph": { "format": "owg", "ref": "turnovers/shot_0140_north.owg.json" } } ``` `comp_0140_v4` arrives with a `predecessor` and a `version_relation` of `revision`, so the conform can answer *is this the latest approved version* without a spreadsheet. See the [OTIO binding](/standards-bindings#opentimelineio-the-timeline-as-a-governed-asset). ## The turnover document A turnover that crosses a company boundary is a **standalone OWG document**, so the receiving organization can validate it without access to the parent graph. Its shape is specified: | Requirement | Rule | |---|---| | Identity | Its own `id`. `project_id` **must** match the parent document's `project_id` | | Parent link | A `part_of` [relationship](/relationships#relation-vocabulary) from this document's `id` to the parent's `id`, `assurance: "asserted"` | | Ids | Entity ids from the parent **must be preserved unchanged**. Renumbering breaks reconnection | | Registries | Must include every participant, asset, infrastructure and context entry its tasks reference — and nothing more | | Validation | Must pass all four passes on its own | ```json { "$schema": "https://openworkflowgraph.org/schemas/core/v0.92.json", "owg_version": "0.92", "id": "turnover-shot-0140-north", "project_id": "ep104", "objective": "Composite shot 0140 from the supplied plate, EDL and LUT.", "organizations": [ { "id": "org_studio", "name": "Example Studio" }, { "id": "org_vfx_north", "name": "North VFX", "role": "vfx-vendor" } ], "assets": [ { "id": "plate_0140", "role": "source", "type": "video", "identity": "sha256:4c1a…", "storage": { "provider": "s3", "locator": "s3://turnovers/0140/plate.exr" } }, { "id": "edl_reel2", "role": "edl", "type": "text" }, { "id": "lut_show", "role": "lut", "type": "text" } ], "participants": [ { "id": "org_vfx_north", "kind": "organization" } ], "tasks": [ { "id": "comp_0140", "type": "work", "executor": { "type": "local_app", "ref": "nuke", "environment": "desktop" }, "ai_role": "none", "performed_by": "org_vfx_north", "used": ["plate_0140", "edl_reel2", "lut_show"], "produced": ["comp_0140_v4"], "failure_mode": "HALT" } ], "relationships": [ { "id": "rel_parent", "relation": "part_of", "from": { "kind": "context", "id": "turnover-shot-0140-north" }, "to": { "kind": "context", "id": "ep104-master" }, "assurance": "asserted" } ] } ``` ### Reconnecting the return Because ids are preserved, the returned document merges deterministically: 1. **Validate the returned document** before merging. It is untrusted input. 2. **Match entities by id first**, then by `identifiers[]` for anything the vendor's own system named. 3. **Never strip an identifier the vendor added.** It is how the next round matches. 4. **The returned artifact is a `revision`** whose `predecessor` is the asset the vendor was given — that single edge is what lets a conform answer *is this the latest approved version*. An implementation may add its own merge policy on top (conflict handling, staging, review before merge), but the document shape above is what makes two implementations interoperable. ## What remains an implementation concern For a cross-company turnover, keep one distinction sharp: the specification defines how a scope is **expressed**, not how it is **enforced**. A document describing a scoped turnover is not evidence that any system honoured the scope — see [Security model](/security-model#what-this-specification-does-and-does-not-do) and [Enforcing what the document expresses](/implementation-considerations#enforcing-what-the-document-expresses). --- # Reference syntax *Section: Specification* > The formal grammar for $. references, string interpolation, and when expressions — with the run-state vocabulary they evaluate against. References are how a task reads a parameter or an upstream task's output. Conditions are how a task decides whether to run at all. Both are given here as complete grammars, because a syntax without a grammar cannot be parsed reliably by a person writing a regex or by a model generating a document. Both grammars are in **ISO/IEC 14977 EBNF**. Terminals are quoted; `{ }` is zero-or-more; `[ ]` is optional. ## Shared lexical rules ``` identifier = ( letter | digit ) { letter | digit | "_" | "-" } ; letter = "A" … "Z" | "a" … "z" ; digit = "0" … "9" ; ws = { " " | "\t" } ; ``` Identifiers match the `id` patterns used elsewhere in the specification, so any task, parameter, or asset id is a legal path segment without escaping. ## References A reference addresses a value in the run state. There are two syntactic positions, and the distinction matters because they return different things. ``` value-reference = "$." path ; interpolation = "${" path "}" ; path = params-path | task-path | workflow-path | task-self-path ; params-path = "params." identifier ; task-path = "tasks." identifier "." task-field ; task-field = "outputs." identifier | "status" ; workflow-path = "workflow." ( "id" | "version" ) ; task-self-path = "task." ( "id" | "attempt" ) ; ``` ### Value references When a JSON string consists **entirely** of a value reference, the whole string is replaced by the referenced value, **preserving its type**. ```json { "inputs": { "input_key": "$.tasks.ingest.outputs.output_key", "add_captions": "$.params.add_captions" } } ``` `add_captions` resolves to a boolean, not the string `"true"`. This is why the whole-string form exists: it is the only way to pass a non-string parameter through. ### Interpolation When a reference appears **inside** a larger string, it uses the brace form and is substituted as text. ```json { "outputs": { "output_key": "jobs/${workflow.id}/${task.id}/output.mp4" } } ``` Interpolated values are stringified. A boolean becomes `true`, a number its decimal form, and an array or object is a validation error — you cannot interpolate a structure into a path. ### Escaping A literal dollar sign is written `$$`. ```json { "inputs": { "label": "Cost: $$40 per hour" } } ``` `$$` is the only escape. There is no backslash escaping, because JSON already owns the backslash. ### What is deliberately absent Stated so you do not go looking: - **No array indexing or filters.** No `[0]`, no `[*]`, no `[?(...)]`. If you need an element, name an output that holds it. - **No arithmetic.** References read; they do not compute. - **No nested references.** `$.params.${...}` is invalid. - **No cross-document references.** A reference resolves within one document. Cross-document linkage is a [relationship](/relationships), not a reference. - **No scatter/gather construct.** Fan-out over a collection is modelled as explicit tasks, which keeps per-item status, cost, and disclosure individually addressable — see [Implementation considerations](/implementation-considerations#fanning-work-out-over-a-collection). > **This is not JSONPath.** The syntax borrows JSONPath's `$.` sigil and nothing else. Do not reach for a JSONPath library to evaluate it — the grammar above is the whole language, and it is small enough to parse directly. ## Run state vocabulary Conditions frequently test a task's status, so the status values are part of the specification rather than left to an implementation. | Status | Meaning | |---|---| | `pending` | Not yet eligible; dependencies incomplete | | `running` | Started, not finished | | `succeeded` | Completed successfully | | `failed` | Completed unsuccessfully, after any retries | | `skipped` | Not run, because its `when` evaluated false or a dependency was skipped | `$.tasks..status` resolves to one of exactly these five strings. Note the interaction: a task depending on a `skipped` task is itself `skipped`, not `pending`. Skip propagates. That is what stops an optional branch from blocking the graph. ## `when` expressions `when` decides whether a task runs. If the expression evaluates false, the task is `skipped`. ``` expression = or-expr ; or-expr = and-expr { ws "||" ws and-expr } ; and-expr = unary { ws "&&" ws unary } ; unary = [ "!" ws ] primary ; primary = "(" ws expression ws ")" | function | comparison | operand ; comparison = operand ws comp-op ws operand ; comp-op = "==" | "!=" | "<=" | ">=" | "<" | ">" ; function = "exists" "(" ws value-reference ws ")" ; operand = value-reference | literal ; literal = string | number | boolean | "null" ; string = "'" { character - "'" } "'" ; number = [ "-" ] digit { digit } [ "." digit { digit } ] ; boolean = "true" | "false" ; ``` **Precedence**, tightest first: `!` → comparison operators → `&&` → `||`. Parentheses override. ### Semantics - **Type discipline.** Comparisons are between like types. Comparing a string to a number is a validation error, not a coercion. `==` and `!=` work on all types; the ordering operators (`<`, `<=`, `>`, `>=`) apply only to numbers. - **Strings are single-quoted.** Double quotes would need escaping inside JSON. - **`exists()`** returns true when the reference resolves to a value that is present and not `null`. This is the intended answer to the optional-upstream problem: a task that consumes an output which may or may not have been produced. - **An unresolvable reference is a validation error**, not a false. A typo should fail loudly at validation, not quietly skip a task at run time. ### Examples ``` $.params.add_captions == true ``` ``` $.tasks.qc.status == 'succeeded' && $.params.territory != 'JP' ``` ``` exists($.tasks.caption.outputs.captions_key) ``` ``` !($.tasks.grade.status == 'failed') && $.params.rounds <= 3 ``` The third example is the one worth internalising. A task that consumes an optional upstream output should gate on `exists()` rather than assuming the output is there — otherwise the document claims a dependency it cannot guarantee. ## A worked pair A conditional caption step, and a publish step that tolerates its absence: ```json { "params": { "add_captions": { "type": "boolean", "required": false, "default": false } }, "tasks": [ { "id": "caption", "type": "work", "executor": { "type": "agent" }, "ai_role": "generative", "performed_by": "caption_agent", "depends_on": ["edit"], "when": "$.params.add_captions == true", "used": ["master_mp4"], "produced": ["captions_vtt"], "failure_mode": "SKIP_DEPENDENTS" }, { "id": "publish", "type": "delivery", "executor": { "type": "saas_api" }, "performed_by": "youtube_api", "depends_on": ["edit", "caption"], "when": "$.tasks.edit.status == 'succeeded'", "used": ["master_mp4"], "inputs": { "captions_key": "$.tasks.caption.outputs.captions_key", "include_captions": "exists($.tasks.caption.outputs.captions_key)" }, "produced": ["publish_record"], "failure_mode": "HALT" } ] } ``` `publish` depends on `caption` so ordering is defined, but gates on `edit` succeeding rather than on `caption` succeeding — and passes an `exists()` result so the executor knows whether captions are actually available. The alternative, listing `captions_vtt` in `used[]` unconditionally, would assert a lineage edge to an asset that may never exist. ## Implementer's note The grammars above are small on purpose. A complete recursive-descent parser for both is a few hundred lines, and that is the intended cost — a language small enough to implement correctly beats a large one implemented three incompatible ways. If you are generating documents rather than parsing them, the practical rules are: - Whole-value reference for typed values; `${...}` only inside a larger string. - Single-quoted strings in conditions. - Gate on `exists()` whenever an upstream output is conditional. - Keep conditions to one or two clauses. A condition that needs parentheses is usually a task that should be split. --- # Profiles *Section: Specification* > How profiles declare the executors, tool vocabularies, and registry extensions a workflow may use, keeping domain and vendor detail out of the core specification. The core specification deliberately says nothing about what tools exist. `task.tool` is a free string, and `billing.tier` and `billing.unit` carry no vocabulary. Profiles are where concrete detail lives, so the core can stay stable while domains and vendors evolve at their own pace. A document declares the profiles it uses: ```json { "owg_version": "0.92", "id": "episodic-dailies", "profiles": ["core", "cloud-services"] } ``` `core` is implicit and may be listed for clarity. Every other entry names a profile whose vocabulary the document draws on. ## What a profile declares A profile is a registry, published separately from the core schema. It declares: | Element | Purpose | |---|---| | `profile_id`, `profile_name`, `version` | Identity | | `conformance` | The level this profile targets — see below | | `executor_types` | Which core executor types the profile covers | | `tools` | Named tools, each with its inputs, outputs, and billing shape | Each tool entry names its required and optional inputs, what it produces, and how it bills: ```json { "profile_id": "cloud-services", "profile_name": "Cloud Media Services (illustrative)", "version": "1.0", "conformance": "L2", "executor_types": ["service"], "tools": { "transcode": { "description": "Transcode a source file to a delivery format", "executor_type": "service", "inputs": { "required": ["source_url", "output_format"], "properties": { "source_url": { "type": "string" }, "output_format": { "type": "string" }, "resolution": { "type": "string" } } }, "outputs": { "produces": ["output_key", "content_type", "duration_seconds", "file_size_bytes"] }, "billing": { "tier": "standard", "unit": "per_job" } } } } ``` The profile above is illustrative — it shows the shape, not a published vocabulary. ## Profile kinds Profiles fall into two kinds, and the distinction matters for who should write them. **Domain profiles** describe a category of work rather than a product, and are the natural candidates for community ownership: | Domain | Executor types | What it covers | |---|---|---| | Generative AI | `comfyui_graph`, `comfyui_node`, `agent`, `saas_api` | Generative pipelines, autonomous agents, model-backed services | | VFX and post | `local_app`, `human`, `render_farm` | Desktop applications and human review gates | | Physical production | `human`, `local_app` | On-set capture and the ingest boundary | **Vendor profiles** describe one provider's tools. A vendor publishes and versions its own; the specification neither blesses nor enumerates them. ### Two constraints worth knowing Both come from domain profiles, and both affect how you author a document: - **Generative AI: `operated_by` is required** on any participant acting as an `agent` executor. An autonomous agent with no accountable principal is not usable under that profile — see [Participants and authority](/participants-and-authority#agent-authority). The same profile marks content credentials on generated assets as **recommended**. - **Physical production: provenance defaults to `asserted`.** Offline and physical steps are assertions, not attestations — a manually logged camera card is somebody's word. The **ingest station is the boundary** where physical provenance becomes digital, and where an assertion can be upgraded to an attestation by a signed checksum. This is the origin of the [`assurance` vocabulary](/relationships#the-three-assurance-levels). ### Dispatch versus execution One semantic that catches people out, from the VFX and post domain: for `local_app` executors, **OWG dispatches and awaits status — it does not drive the application in-process.** Status arrives in the graph by ingestion from whatever system tracks that work. So a task with a `local_app` executor is a request and a record, not a remote-control instruction. Model it accordingly. ## Registry extension profiles Domain and vendor profiles (above) extend what `task.tool` can name. A third kind, added in v0.92, extends the registries themselves. `context`, `asset`, and `relationships[]` each carry an optional `profiles` object, keyed by profile id, reserved for exactly this: ```json { "contexts": [{ "id": "story-0472", "profiles": { "broadcast-newsroom": { "lifecycle": { "phase": "BREAKING" } } } }] } ``` Core reserves the key and does not interpret what is inside it. A profile declaring a `registry_extensions` object states the shape of its own key on `context`, `asset`, and/or `relationships[]`: ```json { "profile_id": "broadcast-newsroom", "registry_extensions": { "context": { "type": "object", "properties": { "lifecycle": { "type": "object" } } } } } ``` **Why not just widen core?** The same reason a billing tier or a device type does not belong in core: a field meaningful to one domain and meaningless to every other implementer is domain vocabulary, not a shared primitive. `context.work_type: "factual-news"` and `asset.state: "growing"` earned core placement because they generalize past the standard that motivated them, see [Changelog → v0.92](/changelog#v092) for the test applied to a concrete case. Story lifecycle phases, evidential sourcing tiers, and compliance-gate status did not pass that test, which is why they live in the `broadcast-newsroom` profile instead. **Namespacing is what makes two profiles safe on one document.** Nothing stops a second profile from also wanting something phase-and-priority-shaped on `context` — a live-sports profile, say, with its own notion of a running clock. Because each profile owns only its own key under `.profiles`, two such profiles compose on the same context with no collision, by construction rather than by convention. **Not yet validator-enforced.** A document's `context[].profiles.` validates today as "any object" under core, exactly like `metadata`. A profile's `registry_extensions` schema documents the intended shape; nothing currently loads and checks it automatically at validation time. Treat a document using this mechanism as L1-conformant only until that lands, this is new spec surface, not yet a promise the tooling keeps. ## Conformance levels Three levels, each testable given the right inputs. ### L1 — the document is valid All four [validation passes](/validation) succeed. Testable from the document alone. ### L2 — the document is executable on its profiles Given the document **and** its declared profiles resolved, all of the following hold for every task: 1. If the task has a `tool`, some active profile declares it. 2. The declaring profile lists the task's `executor.type` in its `executor_types`. 3. Every input the profile marks `required` is present in the task's `inputs`. 4. Every input present satisfies the profile's declared type and enum constraints. 5. Every key in the task's `outputs` appears in the profile's `produces` list for that tool, unless it is a `metadata` or `x-` key. A task with no `tool` is L2-conformant by default — it names no profile contract to satisfy. ### L3 — the run captured full provenance Given a [run document](/runs), for every attempt with `status: "succeeded"`: 1. `performed_by` is present and resolves to a participant in the definition. 2. `ran_on` is present and resolves to an infrastructure entry. 3. Every asset in the definition's `produced[]` for that task appears in the attempt's `produced`. 4. Every produced asset carries an `identity` and an `assurance`. 5. Any participant acting as an `agent` has an `operated_by` chain terminating at a `human` or `organization`. L3 is a property of an execution, not of a definition. A definition cannot be L3-conformant; only a run can. > **State which level you mean.** "OWG-conformant" alone is ambiguous — L1 is about a file, L2 about a file plus its profiles, L3 about an execution. See [Versioning and stability](/versioning-and-stability#conformance). ## Practical guidance - **Always declare `profiles`.** It states which vocabulary a document draws on, and it is what an L2 conformance check reads. - **Resolve profiles at validation time if you want L2.** The core schema treats `tool` as a free string by design — checking a tool name against its profile requires the profile, so that check belongs to whatever loads them. - **Do not put domain vocabulary in the core.** If you find yourself wanting a new core enum value for a tool, a billing tier, or a device type, that belongs in a profile. The core stays small on purpose. - **Version profiles independently.** A profile can iterate without a core release, which is the entire reason the split exists. --- # Run records *Section: Specification* > 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.[]` | | `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. --- # Validation *Section: Implementing* > 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. --- # Conformance corpus *Section: Implementing* > The reference workflows, what each tier proves, how the regression harness runs them, and the true inventory. The conformance corpus is a body of reference workflows that runs as a regression suite. Its purpose is to keep implementations honest: once a workflow is green, it stays green. ## Download them Every document is published as a real file. They are the same documents the regression suite runs, so each has passed all four [validation](/validation) passes — use them as reference data, as fixtures for your own implementation, or as worked examples to read. | Resource | URL | |---|---| | Machine-readable index | [`/corpus/index.json`](/corpus/index.json) | | Any document | `/corpus/` | ```bash # What is in the corpus, with task and asset counts curl https://openworkflowgraph.org/corpus/index.json # A single conformance workflow curl https://openworkflowgraph.org/corpus/W01.owg.json # The whole set curl -s https://openworkflowgraph.org/corpus/index.json \ | jq -r '.documents[].url' | xargs -n1 curl -sO ``` > **Validate the project documents together, not one at a time.** The named project documents use the multi-document registry pattern: a registry document holds the shared participants, organizations, infrastructure and contexts, and the workflow documents sharing its `project_id` reference them. Checked in isolation, those references look unresolved. `index.json` carries `project_id` on every entry so you can group them. ## Inventory | Set | Count | In the regression suite | |---|---|---| | Numbered conformance workflows (`W01`–`W14`) | **14** | Yes | | Named project documents | 8 | No | | **Total OWG documents** | **22** | — | | OMC-JSON reference datasets | 2 | No | > **On the number.** The corpus is planned to extend to `W35`; **`W15` onward are not yet encoded**, because they need applications and on-set hardware that broader coverage will require. The figure to cite today is **14** — the numbered, tiered documents that run as a suite. Use 22 if you mean every OWG document in the corpus, including the named project documents. ## Tiers | Tier | Shape | Workflows | |---|---|---| | **tier-0** | Atomic — a single task | W01, W02, W03 | | **tier-1** | Topology — linear chains, conditionals, parallel fan-out | W04–W08 | | **tier-2** | Full production scenarios by domain | W09–W14 | Tier-2 covers live sports (W09), sports graphics (W10), news QC (W11), VFX and DI (W12), AI dubbing (W13), and generative VFX (W14, the headline case). Complexity climbs as intended: | Workflow | Tasks | Assets | Participants | |---|---|---|---| | W01–W03 | 1 | 2 | 1 | | W04 | 4 | 4 | 4 | | W07 | 6 | 6 | 4 | | W11 | 7 | 7 | 4 | | W13 | 11 | 12 | 5 | | W14 | 10 | 10 | 7 | Naming is `W.owg.json`, zero-padded, with the number **global rather than per-tier** — tier-1 starts at W04, not W01. > Tier boundaries are a convention of the corpus rather than a normative part of the specification — they organise coverage, and nothing in the schema depends on them. Each document carries its tier in its `description` prefix (`T0 Atomic`, `T1 Linear Chain`, `T2 Sports Live`). ## Project documents Eight further documents exercise features the numbered tiers do not, and are worth reading even though CI skips them (the harness only scans directories beginning `tier-`): | Document | What it demonstrates | |---|---| | `europa-project`, `helios-project` | **Registry-only documents** — zero tasks, pure participant and asset registries. Proof that `tasks` really is optional | | `helios-concept-dev` | The only document exercising the full QC re-route vocabulary: `on_pass`, `on_fail`, `reroute_feedback`, `max_reroutes` | | `tams-live-sports` | The TAMS binding — a growing asset with QC hanging off it | | `usd-scene-composition` | The USD binding — composition arcs as typed edges | | `hybrid-genai-pipeline` | Mixed human and generative work in one graph | | `europa-sc3-postlock` | A post-lock scenario | All eight validate against v0.9. The registry-only pair are worth running early if you are building a reader — a document with no `tasks` is legal, and code that assumes otherwise fails on them. ## Running the corpus The corpus is intended to be run as a regression suite: every document in it must validate, and once a document is green it stays green. That is the whole discipline — a corpus that is not run is documentation, not conformance. A conformant test run reports, per document, whether it passed each [validation pass](/validation). A summary looks like: ``` OWG conformance corpus — 14 document(s) ✓ W01 structural:✓ referential:✓ graph:✓ expression:✓ ✓ W02 structural:✓ referential:✓ graph:✓ expression:✓ … ✓ W14 structural:✓ referential:✓ graph:✓ expression:✓ 14/14 green ``` Reporting per pass rather than a single pass/fail matters: a document that clears the structural pass but not the referential one tells you something specific about your validator, and a single boolean hides it. ### The corpus does not test rejection Worth stating plainly, because it is the corpus's main limitation: **every document in it is expected to pass.** There are no negative fixtures — no documents that are supposed to be rejected, and no per-document expectation metadata. So the corpus demonstrates that a valid document is accepted. It says nothing about whether an invalid one is caught, and a validator that accepted everything would score 14/14. **If you are building a conformant implementation, write your own invalid fixtures.** At minimum, cover one case per [error code](/validation#error-codes): a cycle, a dangling reference, a duplicate id, an unknown property, an unresolvable expression reference, a type-mismatched comparison, an authority cycle, and a `COMPENSATE` without a target. That set is what actually tells you your validator works. A shared negative corpus is a natural candidate for community contribution, and the [error-code table](/validation#error-codes) is the checklist it should cover. ## Conformance levels Profiles declare a level of L1, L2, or L3: | Level | Meaning | Checked today | |---|---|---| | **L1** | Valid against the JSON Schema | **Yes** | | **L2** | Executable on a profile's declared tools | No | | **L3** | Full provenance capture | No | Only L1 is verified by any tooling today. There is no profile-conformance checking and no provenance checking in the implementation. See [Profiles](/profiles#conformance-levels). ## Using the corpus as an implementer A practical sequence: 1. **Start with W01.** One task, two assets, one participant — the smallest complete document. Load it, project it, render it. 2. **Add W04.** Four tasks, a linear chain, four executor types in one document, and a real `retry` block. 3. **Add W05.** A conditional (`when`), a human review gate, and fan-in via `depends_on: ["edit", "thumbnail_review"]`. 4. **Add a registry-only document** (`helios-project`) to confirm you handle a document with no tasks. 5. **Then W14**, the headline case: ten tasks, LoRA training, two human gates, C2PA signing. 6. **Write your own negative fixtures**, since the corpus has none. Full annotated examples are in [Examples](/examples). --- # Persistence requirements *Section: Implementing* > What an implementation must be able to answer about an OWG graph — stated as required capabilities, not as a prescribed storage design. OWG specifies a **document format**, not a database. An OWG document is JSON, and any store that can hold it and answer the queries below is conformant. This is deliberate and it is a portability guarantee: the specification does not oblige you to adopt anyone's storage design, and it does not privilege any vendor's. Implementations differ, and should. ## Required query capabilities A conformant implementation must be able to answer four classes of question about a stored graph. These are stated as capabilities because they are what interoperability depends on — not as a schema, which is yours to choose. | Class | The question | Why it is required | |---|---|---| | **Entity lookup** | Give me this asset, task, or participant by id | The floor for any read path | | **Workflow retrieval** | Give me this workflow and everything it references | Rendering, hand-off, and export all need a bounded subgraph | | **Lineage traversal** | What did this asset come from, transitively? | Impact analysis, rights answers, and AI-disclosure all reduce to this | | **Attribution traversal** | Who touched this, and on whose authority? | The accountability claim is void without it | Two properties matter more than how you achieve them: - **Lineage traversal must be bounded.** A caller must be able to cap traversal depth, and the implementation must terminate on cyclic data rather than looping. Structural lineage should be acyclic, but a store cannot assume its inputs were validated. - **Retrieval must be scopeable.** A workflow, a context, or a turnover subgraph must be retrievable without reading the whole graph — that is what makes [scoped hand-offs](/subgraphs-and-turnovers) enforceable rather than advisory. ## Export and portability Two hard requirements, and the reason the format is worth having at all: - **Export must be lossless.** A document exported from a conformant store must validate against the schema and preserve every field it went in with — including `metadata`, `x-` extensions, and identifiers the implementation does not itself use. - **Unrecognised identifiers must survive.** An implementation must never strip an identifier from `identifiers[]` because it did not recognise the scope. This is the single rule that keeps a graph interoperable over time. See [Standards bindings](/standards-bindings#1-identifier-pass-through). Together these mean the graph you build is yours: exportable, re-importable elsewhere, and not dependent on the implementation that produced it. ## Storage locators, not storage Assets carry a **locator** identifying where the bytes physically live — object storage, on-premises storage, a SaaS asset manager, a camera card, a time-addressable store. The governance record travels with the asset node; the content stays where it is. The consequence worth stating plainly: **media never has to move in order to be governed**, and a production can span clouds, vendors, and on-premises facilities without the graph caring. An implementation must resolve locators; it must not require content to be centralised first. See [Assets and versioning](/assets-and-versioning#storage-locators). ## Scale expectations For sizing, not as a requirement: a mid-sized post facility running a few hundred jobs a year generates tens of thousands of tracked edges annually. A studio across several years reaches the billions. Graph metadata is small relative to media — the node and edge data for a very large estate is a modest database by modern standards, because the bytes live behind locators rather than in the graph. **Storage-heavy productions scale the media, not the model.** Plan capacity accordingly: your traversal performance is the thing to engineer, not your metadata volume. ## What the specification does not prescribe Left entirely to the implementation, and intentionally so: - The storage engine — relational, graph-native, document, or a combination. - The physical schema, indexing strategy, and query formulation. - Caching, projection, and read-replica architecture. - Access-control enforcement mechanism (the [security model](/security-model) specifies what must be expressible, not how to enforce it). - Federation and ingestion strategy, including how an implementation relates entities arriving from different systems. If two implementations both hold the same document, answer the four query classes, and export losslessly, they interoperate. Nothing beyond that is the specification's business. --- # For agents *Section: Implementing* > A machine-oriented implementation guide — canonical endpoints, the minimum viable document, a pre-flight checklist, and what an agent-authored workflow must satisfy to be trusted. This page is written for autonomous agents and scripted implementers. It is deliberately terse and checklist-shaped. Every claim here is stated more fully elsewhere and linked. ## Machine-readable endpoints | Resource | Path | |---|---| | Documentation index for LLMs | `/llms.txt` | | Full documentation, single file | `/llms-full.txt` | | Any page as raw markdown | `/raw/.md` | | Core schema (project document, current) | `/schemas/core/v0.92.json` | | Core schema (project document, prior) | `/schemas/core/v0.91.json`, `/schemas/core/v0.9.json` | | Core schema (legacy step document) | `/schemas/core/v0.8.json` | | Domain/vendor profile schemas | `/schemas/profiles//v0.9.json` (botverse, generative-ai, physical-production, vfx) | | `broadcast-newsroom` (SOM) profile schema | `/schemas/profiles/broadcast-newsroom/v0.1.json` | | JSON-LD context | `/context/v0.9.jsonld` | | Conformance corpus index | `/corpus/index.json` | | Any corpus document | `/corpus/` | The corpus is 22 complete, validated documents — the closest thing to reference data. Fetch `index.json` first: it carries `kind`, `project_id`, and task/asset/participant counts for each, so you can pick one at the complexity you need rather than downloading all of them. No authentication. Everything is public and fetchable directly. ```bash curl https://openworkflowgraph.org/llms-full.txt curl https://openworkflowgraph.org/schemas/core/v0.92.json ``` ## Minimum viable document The smallest thing that validates as a project document: ```json { "owg_version": "0.92", "id": "minimal" } ``` The smallest thing that is *useful* — one task with real provenance edges: ```json { "owg_version": "0.92", "id": "minimal-useful", "participants": [ { "id": "svc", "kind": "service" } ], "assets": [ { "id": "in", "role": "source" }, { "id": "out", "role": "delivery", "produced_by": "work", "predecessor": "in", "version_relation": "representation" } ], "tasks": [ { "id": "work", "type": "work", "ai_role": "none", "executor": { "type": "service" }, "performed_by": "svc", "used": ["in"], "produced": ["out"], "failure_mode": "HALT" } ] } ``` ## Authoring rules Ordered by how often they are got wrong. 1. **Set `failure_mode` explicitly.** `HALT` is the default, but declaring it states intent — especially on a task where a reader would expect something gentler. 2. **Declare `ai_role` on every task.** `generative`, `assistive`, or `none`. Never leave it unset on a task that could involve AI — that is validator rule R1. AI-ness is *never* inferred from `executor.type`. 3. **Give every agent participant an `operated_by`.** Required by the generative-AI profile, and the basis of the whole accountability chain. Ensure the chain terminates at a human or organization — nothing checks this for you. 4. **Use `used[]` and `produced[]` as the source of truth.** An asset's `produced_by` is a derived convenience; keep them consistent. 5. **Choose `version_relation` deliberately.** A proxy is a `representation`, not a `revision`. An approved retake is a `revision`, not a new asset. This is the single highest-leverage correctness decision in a document. 6. **Populate both `id` and `identity` on assets.** `id` is document-local; `identity` is the global content-derived identity that lets the asset be recognised elsewhere. 7. **Never inline a secret.** Use `executor.credentials_key`. 8. **Preserve every inbound identifier** in `identifiers[]` with its `scope`. Never strip one you do not recognise. 9. **Write `when` expressions against the grammar.** It is fully specified — comparisons, `&&`/`||`/`!`, parentheses, and `exists()`. Strings are single-quoted. Gate on `exists()` whenever an upstream output is conditional. See [Reference syntax](/reference-syntax#when-expressions). 10. **Use `metadata`, an `x-` prefix, or (on `context`/`asset`/`relationships[]`) a namespaced `profiles.` key for anything the schema does not define.** Unknown bare keys are validation errors, by design. ## Pre-flight checklist v0.92 specifies all of the following as validator checks. **Do not assume the validator you are using performs them all** — establish which [passes](/validation) it implements, and verify the rest yourself. A clean result from a structural-only validator does not mean a runnable document. **Referential integrity:** - [ ] Every `depends_on` entry names a task that exists. - [ ] Every `used` and `produced` entry names an asset that exists. - [ ] Every `performed_by` names a participant; every `ran_on` names an infrastructure entry. - [ ] Every `predecessor` names an asset. - [ ] Every `on_pass`, `on_fail`, `on_failure_compensate`, and `reroute_feedback.to_task` names a task. - [ ] Every `organization_id`, `works_for`, and `operated_by` resolves. - [ ] Every `composes[].component` resolves. - [ ] Every `relationships[].from.id` and `.to.id` resolves to an entity of the stated `kind`. - [ ] Each asset's `produced_by` agrees with that task's `produced[]`, and no asset has two producers. **Graph shape:** - [ ] The `depends_on` graph is acyclic. - [ ] Ids are unique within each registry. - [ ] Every `operated_by` chain terminates at a `human` or `organization`. - [ ] Every gate that routes backwards has `max_reroutes`, or the document has `governance.max_total_reroutes`. **Expressions:** - [ ] Every `when` expression parses against the grammar. - [ ] Every `$.` reference resolves to a declared parameter, task, or field. - [ ] Comparison operands are type-compatible; ordering operators are used only on numbers. - [ ] No reference points at a task that is not upstream of the referring task. **Field hygiene:** - [ ] Every `failure_mode: "COMPENSATE"` task carries `on_failure_compensate`. - [ ] Every `inferred` assurance carries `confidence` and `method`; no `attested` or `asserted` one does. - [ ] Anything the schema does not define lives in `metadata`, under an `x-` prefix, or under a namespaced `profiles.` key. ## Validating ```bash owg-validate workflow.owg.json --json ``` Success: ```json { "valid": true, "id": "episodic-dailies", "owg_version": "0.9" } ``` Failure: ```json { "valid": false, "errors": [ { "code": "OWG_UNRESOLVED_REFERENCE", "path": "/tasks/2/when", "message": "Reference \"$.params.add_caption\" does not resolve.", "docs": "https://openworkflowgraph.org/reference-syntax" } ] } ``` Treat any non-empty `errors` array as fatal. Errors carry `code`, `path` (a JSON Pointer), `message`, and `docs`. Full code list in [Validation](/validation#error-codes). ## Being trusted to execute An agent-authored workflow is only useful if it can be trusted enough to run. Three properties make that possible. ### 1. Schema validation is the floor Every document, whoever wrote it, is checked before a single task runs. Malformed dependencies, unresolvable references, cycles, and hallucinated field names are caught at validation rather than discovered mid-shoot — that is what `additionalProperties: false` and the four validation passes are for. That floor is only as high as your validator actually reaches, so confirm which passes it performs and cover the remainder with the checklist above. ### 2. Assurance tagging is how proposals are accepted selectively A machine-proposed relationship or workflow is tagged, not silently trusted: | Level | Meaning | Carries confidence | |---|---|---| | `attested` | Backed by a verified content credential | No | | `asserted` | Declared by a system of record, or confirmed by a human | No | | `inferred` | Proposed by a matcher | **Required** | An implementation applies its own acceptance policy — what score warrants automatic acceptance versus human review is not specified. What is specified: an accepted edge **stays `inferred`** and gains `accepted_by`. It is never promoted to `asserted`, because that would erase the fact that a machine proposed it and a named actor accepted it. ```json { "id": "rel_0042", "relation": "same_as", "from": { "kind": "asset", "id": "plate_0140" }, "to": { "kind": "asset", "id": "ext_frameio_b2c8e9f1" }, "assurance": "inferred", "confidence": 0.74, "method": "m-probabilistic-1", "proposed_by": "connector_frameio", "proposed_at": "2026-08-20T09:14:00Z" } ``` **So an agent's job is to propose with an honest confidence score, not to assert.** Overstating confidence is the one failure mode that corrupts the graph rather than merely wasting a review. Populate `method` with a stable identifier too — it is the key by which a whole class of bad matches gets re-reviewed later. Full payload rules in [Relationships](/relationships). ### 3. Provenance is not optional Structural edges — `used`, `produced`, `performed_by`, `ran_on` — are the record. An agent that produces an asset without declaring what it consumed and on whose authority has produced an unaccountable asset, whatever else it got right. ## Hard constraints Do not violate these; they are the boundaries that keep the model coherent. - **Never absorb another standard's internals.** Reference them. `prim_path` and `timerange` are **opaque** — store, never parse. - **Never descend below the published-asset boundary** when reading USD composition. - **Never strip an identifier** you do not recognise. - **Never infer AI involvement** from an executor type. - **Never overwrite.** New versions are new assets with a typed relation to their predecessor. A rejected asset stays in the graph as `rejected`. - **Never promote a referenced operation to a task** unless it is genuinely *work* — a generative transition or a colour transform, not a composition resolution the other standard already performs. ## Before you build Read [Versioning and stability](/versioning-and-stability) for how much movement to expect in each area, and [Implementation considerations](/implementation-considerations) for the decisions the specification leaves to you — fan-out, turnover documents, compliance rule packs, confidence calibration, and enforcement. The one habit that matters most: **establish what the tooling you depend on actually does** before treating a clean validation as a guarantee. A validator performing only the structural pass will accept documents that cannot execute. --- # Examples *Section: Implementing* > Complete annotated workflows from the conformance corpus — atomic, generative, linear chain, and conditional with a human gate. Every document below comes from the [conformance corpus](/conformance) — the workflows that run as a regression suite, so they cannot silently drift from the schema. **All 22 are downloadable**, not just the four annotated here: [`/corpus/index.json`](/corpus/index.json) lists them with task, asset and participant counts, and each is at `/corpus/`. Fetch one as a starting point rather than retyping from this page. ```bash curl -O https://openworkflowgraph.org/corpus/W05.owg.json owg-validate W05.owg.json --json ``` > These are the corpus documents brought up to v0.9 — every one validates against the current schema. Where an earlier iteration of a document omitted something v0.9 requires, the omission has been corrected here and the correction is noted, because a specification's own examples should obey it. ## W01 — atomic single task The simplest complete workflow: transcode one file. One task, one participant, one input, one output. ```json { "$schema": "https://openworkflowgraph.org/schemas/core/v0.9.json", "owg_version": "0.9", "id": "W01-transcode-mov-mp4", "profiles": ["core", "cloud-services"], "description": "The simplest possible OWG workflow: transcode one source file MOV→MP4. One task, one participant, one input, one output.", "objective": "Produce an MP4 delivery from a MOV source.", "organizations": [ { "id": "org_mediasvc", "name": "Example Media Services" } ], "participants": [ { "id": "transcode_svc", "kind": "service", "name": "Transcode Service", "organization_id": "org_mediasvc" } ], "infrastructure": [ { "id": "cloud_platform", "type": "saas_platform", "owner": "org_mediasvc" } ], "assets": [ { "id": "source_mov", "role": "source", "type": "video.master", "content_type": "video/quicktime", "storage": { "provider": "s3", "locator": "s3://studio-media/in/clip.mov", "zone": "us-east-2" } }, { "id": "delivery_mp4", "role": "delivery", "type": "video.delivery", "content_type": "video/mp4", "version_relation": "representation", "predecessor": "source_mov", "produced_by": "transcode", "storage": { "provider": "s3", "locator": "s3://studio-media/out/clip.mp4", "zone": "us-east-2" } } ], "tasks": [ { "id": "transcode", "label": "Transcode MOV → MP4", "executor": { "type": "service", "ref": "transcode", "environment": "saas" }, "performed_by": "transcode_svc", "ran_on": "cloud_platform", "used": ["source_mov"], "produced": ["delivery_mp4"], "failure_mode": "HALT", "billing": { "tier": "standard", "unit": "per_job" } } ] } ``` **What to take from it:** - The five registries in order: `organizations`, `participants`, `infrastructure`, `assets`, `tasks`. - The **double linkage** between assets and tasks — `assets[].produced_by` points at the task, `tasks[].produced` points at the asset. The task side is authoritative. - `version_relation: "representation"` plus `predecessor` is the whole lineage statement. An MP4 of a MOV is a format change, not a revision. - Assets carry `storage.locator`, so nothing moved in order to be governed. ## W03 — generative task with a subgraph A single generative step, encapsulating a whole node graph, producing a credentialed asset. ```json { "$schema": "https://openworkflowgraph.org/schemas/core/v0.9.json", "owg_version": "0.9", "id": "W03", "project_id": "generative-ai", "profiles": ["core", "generative-ai"], "description": "W03 (T0 Atomic) — Single text→image via one ComfyUI graph. One generative task on cloud GPU, producing a C2PA-credentialed asset.", "objective": "Generate a single AI image from a text prompt with C2PA provenance.", "organizations": [ { "id": "org_mediasvc", "name": "Example Media Services" } ], "participants": [ { "id": "s.okafor", "kind": "human", "name": "Art Director", "role": "art-director", "organization_id": "org_mediasvc" }, { "id": "concept_bot", "kind": "agent", "name": "SDXL Generator", "operated_by": "s.okafor", "organization_id": "org_mediasvc" } ], "infrastructure": [ { "id": "gpu_cloud", "type": "gpu_compute", "owner": "org_mediasvc", "spec": "cloud GPU" } ], "assets": [ { "id": "text_prompt", "role": "source", "type": "text.prompt", "content_type": "text/plain" }, { "id": "generate_out", "role": "generated_image", "type": "image.generated", "content_type": "image/png", "version_relation": "derivation", "predecessor": "text_prompt", "produced_by": "generate", "credentials": { "scheme": "c2pa" } } ], "tasks": [ { "id": "generate", "label": "SDXL text→image graph", "executor": { "type": "comfyui_graph", "ref": "sdxl_txt2img", "environment": "cloud" }, "ai_role": "generative", "performed_by": "concept_bot", "ran_on": "gpu_cloud", "used": ["text_prompt"], "produced": ["generate_out"], "failure_mode": "HALT", "billing": { "tier": "custom", "unit": "per_job" }, "subgraph": { "format": "comfyui", "ref": "graphs/sdxl_txt2img.json" } } ] } ``` **What to take from it:** - `profiles: ["core", "generative-ai"]` declares the vocabulary in use. - **The agent carries `operated_by`**, chaining to a named human. The generative-AI profile requires this, and it is what makes the generated image attributable to a person rather than to a process. - **`ai_role: "generative"`** is declared explicitly. Without it the task would fail compliance rule [R1](/compliance#the-baseline-rule-pack) — an AI-capable executor with no declaration is a finding, not an assumption of innocence. - `subgraph` with `format: "comfyui"` encapsulates the entire node network as [one governed task](/subgraphs-and-turnovers) whose interior is stored opaquely. - `executor.ref` names the tool; `subgraph.ref` names the interior. They describe different things and need not match. - `credentials: { "scheme": "c2pa" }` sits on the **asset**, not the task. - `version_relation: "derivation"` — an image from a prompt is a new thing acknowledging its source, not a reformat. ## W04 — linear chain across human, desktop, and SaaS Four tasks, four executor types, two organizations. The first workflow where the executor abstraction earns its keep. ```json { "$schema": "https://openworkflowgraph.org/schemas/core/v0.9.json", "owg_version": "0.9", "id": "W04", "project_id": "audio-workflows", "profiles": ["core", "cloud-services"], "description": "W04 (T1 Linear Chain) — Record raw podcast audio (human), edit in DAW (local_app), normalise levels (cloud service), publish to an RSS host (SaaS API).", "objective": "Produce and publish a finished podcast episode to an RSS host.", "organizations": [ { "id": "org_studio_media", "name": "Example Studio Media" }, { "id": "org_mediasvc", "name": "Example Media Services" } ], "participants": [ { "id": "podcast_host", "kind": "human", "name": "Podcast Host", "role": "host", "organization_id": "org_studio_media" }, { "id": "audacity_app", "kind": "service", "name": "Audacity", "organization_id": "org_studio_media" }, { "id": "media_svc", "kind": "service", "name": "Media Services", "organization_id": "org_mediasvc" }, { "id": "publish_api", "kind": "service", "name": "Example Publishing" } ], "infrastructure": [ { "id": "studio_ws", "type": "workstation", "owner": "org_studio_media", "apps": ["audacity"] }, { "id": "cloud_platform", "type": "saas_platform", "owner": "org_mediasvc" }, { "id": "publish_platform", "type": "saas_platform" } ], "assets": [ { "id": "raw_wav", "role": "source", "type": "audio.raw", "content_type": "audio/wav", "produced_by": "record" }, { "id": "edited_wav", "role": "edited_audio", "type": "audio.edited", "content_type": "audio/wav", "version_relation": "revision", "predecessor": "raw_wav", "produced_by": "edit" }, { "id": "episode_mp3", "role": "delivery", "type": "audio.delivery", "content_type": "audio/mpeg", "version_relation": "representation", "predecessor": "edited_wav", "produced_by": "normalize" }, { "id": "publish_out", "role": "publish_record", "type": "record.json", "content_type": "application/json", "version_relation": "derivation", "predecessor": "episode_mp3", "produced_by": "publish" } ], "tasks": [ { "id": "record", "label": "Record raw podcast audio", "executor": { "type": "human", "environment": "manual" }, "performed_by": "podcast_host", "ran_on": "studio_ws", "produced": ["raw_wav"], "failure_mode": "HALT" }, { "id": "edit", "label": "Edit recording in Audacity", "executor": { "type": "local_app", "ref": "audacity", "environment": "desktop" }, "performed_by": "audacity_app", "ran_on": "studio_ws", "depends_on": ["record"], "used": ["raw_wav"], "produced": ["edited_wav"], "failure_mode": "HALT" }, { "id": "normalize", "label": "Normalise loudness and encode MP3", "executor": { "type": "service", "ref": "transcode_from_url", "environment": "saas" }, "performed_by": "media_svc", "ran_on": "cloud_platform", "depends_on": ["edit"], "used": ["edited_wav"], "produced": ["episode_mp3"], "failure_mode": "CONTINUE", "billing": { "tier": "standard", "unit": "per_minute" }, "retry": { "max_attempts": 2, "initial_interval_seconds": 5 } }, { "id": "publish", "label": "Publish episode to host", "executor": { "type": "saas_api", "ref": "publish_upload", "environment": "saas" }, "performed_by": "publish_api", "ran_on": "publish_platform", "depends_on": ["normalize"], "used": ["episode_mp3"], "produced": ["publish_out"], "failure_mode": "HALT", "retry": { "max_attempts": 3, "initial_interval_seconds": 10, "backoff_coefficient": 2.0, "retryable_errors": ["RATE_LIMIT", "TRANSIENT_ERROR"] } } ] } ``` **What to take from it:** - **Four executor types in one graph** — `human`, `local_app`, `service`, `saas_api` — scheduled and attributed identically. This is the central claim of the model, made concrete. - The **version chain is typed at every hop**: raw → edited is a `revision`, edited → MP3 is a `representation`, MP3 → publish record is a `derivation`. Three different relationships, three different meanings. - `record` has **no `used`** — it is an origin task. Assets can enter the graph by being captured. - `failure_mode: "CONTINUE"` on `normalize` lets other branches proceed. - `publish_api` has **no `organization_id`** — a third party. Legitimate, but nothing in the graph vouches for it. - Two organizations means this document already crosses a company boundary. ## W05 — conditional execution and a human review gate Five tasks with a conditional AI step, a human gate, and fan-in. ```json { "$schema": "https://openworkflowgraph.org/schemas/core/v0.9.json", "owg_version": "0.9", "id": "W05", "project_id": "video-workflows", "profiles": ["core", "cloud-services"], "description": "W05 (T1 Linear + Conditional) — Import UGC footage (local Premiere), colour grade and edit, conditionally generate captions via AI agent, thumbnail review by human, then publish.", "objective": "Edit, caption, and publish a UGC video.", "params": { "add_captions": { "type": "boolean", "required": false, "default": false } }, "organizations": [ { "id": "org_studio_media", "name": "Example Studio Media" } ], "participants": [ { "id": "premiere_app", "kind": "service", "name": "Premiere Pro", "organization_id": "org_studio_media" }, { "id": "caption_agent", "kind": "agent", "name": "Caption Agent", "operated_by": "creator", "organization_id": "org_studio_media" }, { "id": "creator", "kind": "human", "name": "Creator", "role": "creator", "organization_id": "org_studio_media" }, { "id": "youtube_api", "kind": "service", "name": "YouTube" } ], "infrastructure": [ { "id": "edit_ws", "type": "workstation", "owner": "org_studio_media", "apps": ["premiere"] }, { "id": "agent_cloud", "type": "saas_platform", "owner": "org_studio_media" }, { "id": "youtube_saas", "type": "saas_platform" } ], "assets": [ { "id": "project_prproj", "role": "project", "type": "project.premiere", "content_type": "application/octet-stream", "produced_by": "ingest" }, { "id": "master_mp4", "role": "edited_master", "type": "video.master", "content_type": "video/mp4", "version_relation": "derivation", "predecessor": "project_prproj", "produced_by": "edit" }, { "id": "captions_srt", "role": "captions", "type": "text.captions", "content_type": "text/vtt", "version_relation": "derivation", "predecessor": "master_mp4", "produced_by": "caption" }, { "id": "thumbnail_jpg", "role": "thumbnail", "type": "image.thumbnail", "content_type": "image/jpeg", "version_relation": "derivation", "predecessor": "master_mp4", "produced_by": "thumbnail_review" }, { "id": "publish_out", "role": "publish_record", "type": "record.json", "content_type": "application/json", "version_relation": "derivation", "predecessor": "master_mp4", "produced_by": "publish" } ], "tasks": [ { "id": "ingest", "label": "Import UGC footage into Premiere", "executor": { "type": "local_app", "ref": "premiere", "environment": "desktop" }, "performed_by": "premiere_app", "ran_on": "edit_ws", "produced": ["project_prproj"], "failure_mode": "HALT" }, { "id": "edit", "label": "Edit, grade and export master", "executor": { "type": "local_app", "ref": "premiere", "environment": "desktop" }, "performed_by": "premiere_app", "ran_on": "edit_ws", "depends_on": ["ingest"], "used": ["project_prproj"], "produced": ["master_mp4"], "failure_mode": "HALT" }, { "id": "caption", "label": "AI generate captions (SRT)", "executor": { "type": "agent", "ref": "generate_captions", "environment": "saas" }, "ai_role": "generative", "performed_by": "caption_agent", "ran_on": "agent_cloud", "depends_on": ["edit"], "when": "$.params.add_captions == true", "used": ["master_mp4"], "produced": ["captions_srt"], "failure_mode": "SKIP_DEPENDENTS" }, { "id": "thumbnail_review", "label": "Creator reviews thumbnail", "type": "review", "executor": { "type": "human", "environment": "manual" }, "performed_by": "creator", "ran_on": "edit_ws", "depends_on": ["edit"], "used": ["master_mp4"], "produced": ["thumbnail_jpg"], "failure_mode": "HALT" }, { "id": "publish", "label": "Upload to YouTube", "executor": { "type": "saas_api", "ref": "youtube_upload", "environment": "saas" }, "performed_by": "youtube_api", "ran_on": "youtube_saas", "depends_on": ["edit", "thumbnail_review", "caption"], "used": ["master_mp4", "thumbnail_jpg"], "inputs": { "captions_key": "$.tasks.caption.outputs.output_key", "include_captions": "exists($.tasks.caption.outputs.output_key)" }, "produced": ["publish_out"], "failure_mode": "HALT", "retry": { "max_attempts": 3, "initial_interval_seconds": 30, "backoff_coefficient": 2.0, "retryable_errors": ["RATE_LIMIT", "QUOTA_EXCEEDED", "TRANSIENT_ERROR"] } } ] } ``` **What to take from it:** - `"type": "review"` marks the human gate. The graph knows this is a decision point, not just work. - `depends_on: ["edit", "thumbnail_review"]` is **fan-in** — two branches converge. - `failure_mode: "SKIP_DEPENDENTS"` on the optional caption step contains the blast radius. **Three details worth studying, because each is a rule the earlier version of this document broke:** 1. **The condition resolves.** `"when": "$.params.add_captions == true"` reads a parameter the document declares in `params`. An earlier version referenced an undeclared parameter, which v0.9 rejects as `OWG_UNRESOLVED_REFERENCE` — the reference grammar is validated, not merely stored. See [Reference syntax](/reference-syntax). 2. **`publish` does not claim a lineage edge it cannot guarantee.** `captions_srt` is *not* in `used[]`, because `caption` may be skipped. The captions path is passed through `inputs` and gated with `exists()`, so the executor learns whether captions exist without the graph asserting an edge to an asset that may never have been produced. 3. **The agent has an accountability chain.** `caption_agent` carries `operated_by: "creator"`, so the generated captions trace to a person. The second is the subtle one, and the mistake people make most often. `depends_on` establishes ordering; `used[]` asserts that consumption actually happened. Conflating them produces lineage that reads as fact but is conditional. ## Going further Other corpus documents worth reading: - **W14** — the headline case. Ten tasks, LoRA training, two human gates, C2PA signing. - **`helios-concept-dev`** — the only document exercising the full QC re-route vocabulary (`on_pass`, `on_fail`, `reroute_feedback`, `max_reroutes`). - **`tams-live-sports`** and **`usd-scene-composition`** — the standards bindings in practice: a growing asset under governance, and USD composition arcs as typed edges. - **`europa-project`**, **`helios-project`** — registry-only documents with zero tasks. --- # Implementation considerations *Section: Implementing* > Decisions the specification deliberately leaves to implementers, with recommended approaches and the trade-offs behind them. A specification that dictated every detail would be a specification for one product. OWG defines the **document format and what it must record**, and leaves a set of decisions to the implementations built on it — because the right answer depends on the production, the deployment, and the systems already in place. This page covers those decisions. Each one states the consideration, a recommended approach, and the reasoning — so you can follow the recommendation or depart from it deliberately. ## Fanning work out over a collection **The consideration.** Some work repeats per item: localization across territories, the same treatment across every shot in a sequence, a deliverable per platform. The item count is often unknown when the workflow is authored — and it can be a handful, or several hundred. v0.9 had no `for_each` construct, deliberately: generating one task per item is usually better than a loop would be, for the reason below. v0.91's [`fan_out`](/tasks#fan-out-v091) exists for the case that argument stops holding — not a replacement for generating tasks, a second tool for when the item count makes the first one impractical. **Recommended for a modest, known-ish count (territories, platforms, shots in a sequence — tens, not hundreds): generate the tasks.** A workflow definition is data, so emit one task per item from whatever knows the collection — a production tracker, a script, or an agent: ```json { "params": { "territories": { "type": "array", "required": true } }, "tasks": [ { "id": "dub_es_419", "type": "work", "ai_role": "generative", "executor": { "type": "saas_api" }, "performed_by": "agent_dubber", "used": ["master_audio_en"], "produced": ["dub_es_419_v1"], "failure_mode": "CONTINUE" }, { "id": "dub_fr_133", "type": "work", "ai_role": "generative", "executor": { "type": "saas_api" }, "performed_by": "agent_dubber", "used": ["master_audio_en"], "produced": ["dub_fr_133_v1"], "failure_mode": "CONTINUE" } ] } ``` **Why this is preferable to a loop construct.** Every item becomes individually addressable — its own status, cost, attribution, approval, and AI-disclosure record. A collapsed fan-out node would hide exactly the per-item detail the graph exists to capture. If one territory's dub is rejected, you want that as a distinct rejected asset with its own lineage, not as an element inside an opaque batch. **Best practices:** - **Derive task ids deterministically** from the item (`dub_es_419`, not `dub_1`). Regeneration then produces the same ids, which makes the operation idempotent and the diffs readable. - **Use `failure_mode: "CONTINUE"`** on fan-out siblings so one failure does not halt the others. - **Keep the generator's output validated** before submission — a generated document is exactly as trustworthy as its generator, which is what the [validation passes](/validation) are for. - **Record the generator** as the `performed_by` of a preceding task if the fan-out itself was computed. The decision to produce forty tasks is work, and work is attributable. **When to reach for `fan_out` instead.** Generating tasks stops scaling somewhere between "a document a person can review" and "a document nobody opens directly" — several hundred hand- or agent-generated task objects for the same subgraph, run once, is where that line usually sits in practice. The concrete case this was built for: several hundred concurrent instances of the same generation subgraph, run live, where the question is not "list every instance" but "did enough of them succeed." That's a [`fan_out`](/tasks#fan-out-v091) with a `tolerated_failure_percentage` — a declared, checkable pass line the engine evaluates, instead of a person reading several hundred rows after the fact. Per-instance detail is not lost — each instance is still addressable in the run record — it just is not enumerated in the *definition*, which is the actual difference between the two approaches. ### Composing independent branches **The consideration.** A workflow with two genuinely unrelated branches — say, an image path and an audio path, both feeding a later step that needs both — raises a question neither `depends_on` nor `failure_mode` answers by itself: if one branch fails, should the other keep going? **Recommended: yes, by default, and it takes no configuration to get.** `depends_on` only creates a dependency where one is declared — a branch with no edge to the failed task is unaffected by its failure. `failure_mode: CONTINUE` (the [default is `HALT`](/tasks#failure-and-compensation)) governs what happens to that *failed* task's own dependents, never to a sibling with no edge to it. So two independent branches already run to their own conclusion independently; the thing worth double-checking is what happens where they meet. For the merge step itself, `depends_on: [branch_a, branch_b]` makes it eligible once both have *finished* — which includes a failed finish, not only a succeeded one. If the merge genuinely needs both outputs, let it fail naturally when a `used` reference resolves to nothing; that failure is honest, because the merge really cannot do its job. If the merge should proceed with whatever showed up, gate the optional input with [`exists()`](/reference-syntax#when-expressions) and read the [worked pair](/reference-syntax#a-worked-pair) — the same conditional-`used` pattern that page describes for an optional producer applies here without changes. **What not to do:** there is no `on_upstream_failure: require_any` field, and none is planned. `exists()` on the merge task already expresses "proceed with whatever arrived" without adding a second way to say the same thing — the specification prefers one composable mechanism over two overlapping ones. See [Reference syntax](/reference-syntax#when-expressions) for the full grammar. ### Fan-out concurrency is a ceiling, not a guarantee **The consideration.** `fan_out.max_concurrency` states an upper bound an author wants respected — often for a reason external to OWG entirely: a vendor API's rate limit, a GPU pool's real capacity, a cost ceiling that ties to concurrent spend. **Recommended:** treat it as a MUST-NOT-EXCEED, never a MUST-PROVIDE. An engine that is itself capacity-constrained (fewer GPUs free than `max_concurrency` allows, a downstream API already throttling) should run fewer instances at once and let the fan-out take longer — never queue-jump the ceiling to hit a target completion time. The ceiling exists specifically so an author with a hard external constraint (500 concurrent agents is a real number a vendor contract might cap, not an arbitrary one) can state it once, in the definition, rather than re-deriving it in every engine that runs the graph. ## Merging a returned turnover **The consideration.** The [turnover document shape](/subgraphs-and-turnovers#the-turnover-document) is specified, and so is how a return reconnects. What is left to you is merge policy — what happens when a return conflicts with the graph it is rejoining. **Recommended:** - **Stage before merging.** Validate the returned document, compute the diff against the current graph, and let a human see it before it lands. A vendor return that silently overwrote a newer internal revision is a bad afternoon. - **Detect the stale-parent case explicitly.** If the asset the vendor was given has itself been superseded since the turnover was sent, their return is a `revision` of something no longer current. That is a decision, not an error — surface it rather than resolving it silently. - **Keep the copy you sent**, versioned. When a dispute arises about what a vendor was actually given, the answer should be a file, not a memory. - **Never trust a returned document's registries over your own.** Take their assets and their attribution; keep your own entity definitions authoritative. **Best practice:** treat a return as a proposal until merged. The vendor asserted what they did; whether it enters your graph is your call, and recording who made that call is what `accepted_by` on a [relationship](/relationships) is for. ## Compliance rule sets **The consideration.** The [baseline rule pack](/compliance#the-baseline-rule-pack) (R1–R6) is specified and a conformant compliance validator implements all of it. What is left to you is how you structure additional rules, since obligations differ by territory and client and regulation keeps developing. **Recommended: rules as loaded data, not compiled logic.** - **Layer packs over the baseline.** A pack may add rules and tighten existing ones; it may never disable a baseline rule. A validator that can be configured below R1–R6 is not conformant. - **Version every pack, and record which version certified a delivery.** When regulation changes, you need to know what standard a past delivery was held to — and that is a question asked years later, by someone who was not there. - **Key rules on the declared fields.** `work_type` × `ai_role` × `depicts_real_entity` covers most obligations, which is why those three are declarations rather than inferences. - **Compute the per-asset conclusion from lineage at delivery time; never store it.** A stored conclusion drifts away from the work that produced it. Treat this one as non-negotiable. - **Make the manifest an asset**, produced by a task, performed by a responsible participant. A manifest that cannot account for its own origin is a strange thing to hand a regulator. **Best practice:** decide your R6 posture deliberately. R6 warns when a disclosure-relevant value rests on an unaccepted inference. Certifying anyway is defensible; certifying without knowing you did is not — so make it an explicit policy choice rather than a default your validator happens to have. ## Shot and scene identity **The consideration.** v0.9 has no `shot_id`. OMC v3.0 has no Shot entity, and MovieLabs' video pipeline — production scene through editorial shots and sequences — arrives in a later release. Inventing a core field now would create a conflict to unpick later. **Recommended: carry it as an identifier.** ```json { "id": "plate_0140", "identifiers": [ { "scope": "yourorg.shot", "id": "EP104_SC014_SH0140" } ] } ``` And use `contexts[]` with `scene_id` for grouping. **Why an identifier rather than a field.** An entry in `identifiers[]` is trivially remappable when OMC defines the real thing — you add the canonical scope alongside and migrate readers. A bespoke top-level field is not: it becomes something every consumer has hard-coded. Extension points exist so that anticipating the future does not require guessing it correctly. ## Time and timecode **The consideration.** Timeranges are stored opaquely: OWG does not parse or interpret them. The mapping between International Atomic Time and SMPTE timecode is an unresolved industry-wide problem, and a governance layer is the wrong place to solve it. **Recommended:** - **Never parse a timerange in the governance layer.** Store it, pass it, hand it back. The essence layer owns resolution. - **Delegate comparison** to the owning system's API rather than doing interval arithmetic on opaque strings. - **Record the time basis** in `metadata` on the asset when a source's basis is known. A future reconciliation needs that context, and capturing it costs nothing now. **Best practice:** if you find yourself needing frame-accurate arithmetic in the graph, that is a signal the operation belongs in a task with an essence-layer executor — not in the graph query. ## Calibrating confidence **The consideration.** An `inferred` relationship carries a `confidence` and a `method`. Scores from different methods are **not comparable** — 0.8 from one method may be far stronger evidence than 0.9 from another. The specification requires both fields precisely so this stays visible. **Recommended:** - **Set acceptance policy per method, not globally.** A single threshold across all methods will be simultaneously too strict for your strong signals and too loose for your weak ones. - **Calibrate against a labelled sample** before trusting any threshold, and re-calibrate when a method changes. - **Keep method identifiers stable.** They are the key by which a class of past inference gets re-reviewed. Renaming one orphans its history. - **Prefer under-claiming.** A proposal reviewed unnecessarily costs someone a minute. A wrong edge accepted silently corrupts every lineage answer that traverses it. **Best practice:** record enough in `metadata` to re-evaluate a decision later — what was compared, and what the alternatives scored. When a match proves wrong, the useful question is why it looked right. ## Operating run records **The consideration.** The [run document](/runs) is specified. What is left to you is how you emit, store, and retain it. **Recommended:** - **Emit attempts as they complete**, not as a batch at the end. A run document that only materialises on success cannot explain a failure, which is when you most need it. - **Compute and store the definition `digest` at submission**, and verify it before trusting a run's claims. A definition edited after execution should break verification loudly. - **Retain runs as long as the assets they produced.** An asset whose run record has been pruned has lost the provenance the graph existed to hold — the retention policy for runs is really a retention policy for accountability. - **Stream status separately from the run record.** Live progress and the durable audit record have different consistency needs; conflating them tends to make the record lossy under load. **Best practice:** make run writes idempotent on `(run_id, task, attempt)`. Retries and reconnecting workers will re-report, and a duplicate attempt record is indistinguishable from a real retry once written. ## Staging a validator **The consideration.** The [four validation passes](/validation) are what conformance requires. Implementing them takes time, and partial coverage is a legitimate intermediate state — as long as it is not mistaken for completeness. **Recommended order**, by value returned per unit of work: 1. **Structural** — JSON Schema, including closed objects. Catches the most common authoring and generation errors, and it is nearly free. 2. **Referential** — every id reference resolves. Catches the errors people actually make. 3. **Graph** — acyclicity, uniqueness, authority-chain termination. Cheap once references resolve. 4. **Expression** — parse and resolve `when` and `$.` references. The largest piece of work, and the one that needs the [grammar](/reference-syntax). **Best practices:** - **Report per pass, not as a single boolean.** "Structural ✓, referential ✗" tells a user something actionable; "invalid" does not. - **Be explicit about coverage.** A validator that performs only the structural pass should say so, because a clean result from it does not mean a runnable document. - **Write negative fixtures.** The [conformance corpus](/conformance#the-corpus-does-not-test-rejection) contains only documents expected to pass, so it cannot tell you whether your validator rejects correctly. Cover one case per [error code](/validation#error-codes). ## Enforcing what the document expresses **The consideration.** The specification defines what must be **expressible** about access — scope via `subgraph`, lifetime via a task's validity window, authority via `operated_by` and `credential_scope`. It does not define enforcement, which belongs to the system holding the content. **Recommended:** - **Enforce at the data layer, not only in the application.** A permission checked in one code path is a permission missing from every other. - **Treat a declared scope as an assertion to verify**, not a guarantee. A document saying an agent had a bounded scope is not evidence the scope was honoured. - **Make grants expire by default.** A grant tied to a task's lifetime cannot be forgotten; a grant that needs revoking will eventually be forgotten. - **Audit against the graph.** The record of who touched what is already there — reconciling actual access against it is the check that catches enforcement bugs. **Best practice:** when evaluating any implementation, including your own, verify enforcement independently of the document format. Expressiveness is a precondition for enforcement, never evidence of it. --- # Failure-handling best practices *Section: Implementing* > Four ways a task's failure can behave, when to reach for each one, and how they compose with fan-out and conditional merges. The specification gives you four `failure_mode` values, a conditional-`used` pattern for merges, and — as of v0.91 — a bounded fan-out with its own tolerance. None of them is a default that fits every case; this page is about which one actually fits which case, with the reasoning that should drive the choice. ## The four `failure_mode` values, and when each earns its keep | Value | What it does | Reach for it when | |---|---|---| | `HALT` (default) | Stops the entire workflow | The failure means continuing is actively wrong — a payment, a legal delivery gate, a step whose precondition every later step assumes | | `CONTINUE` | Marks this task failed, lets other branches proceed | This task's work is genuinely independent of the rest — a handful of named parallel branches | | `SKIP_DEPENDENTS` | Fails this task, skips everything downstream of *it specifically* | This branch is optional, but its own downstream steps can't do anything meaningful without its output | | `COMPENSATE` | Runs a named rollback task, then halts | This task partially committed something external — a partial upload, a reservation — that needs undoing before anything else makes sense | **`HALT` is the default for a reason worth respecting, not routing around.** A definition that never sets `failure_mode` is not an unfinished definition — it's one where the author decided every task's failure should stop everything, which is frequently the right call (a delivery pipeline where step 4 failing should not let step 7 ship anyway). Setting `CONTINUE` everywhere out of habit is how a workflow quietly ships partial work nobody meant to ship. **Worked example — a title-card generation branch and a mix-downmix branch, both optional relative to each other:** ```json { "tasks": [ { "id": "pull_asset", "executor": { "type": "service" }, "used": ["source_lora"], "produced": ["asset_ref"] }, { "id": "generate_ident", "depends_on": ["pull_asset"], "executor": { "type": "agent" }, "ai_role": "generative", "used": ["asset_ref"], "produced": ["ident_image"], "failure_mode": "CONTINUE" }, { "id": "downmix_audio", "depends_on": ["pull_asset"], "executor": { "type": "saas_api" }, "used": ["asset_ref"], "produced": ["stereo_mix"], "failure_mode": "CONTINUE" } ] } ``` Neither branch depends on the other, so `CONTINUE` on both is not adding tolerance that would not otherwise exist — `depends_on` already means a task with no edge to the failed one is unaffected (see [Composing independent branches](/implementation-considerations#composing-independent-branches)). What `CONTINUE` actually buys here is making that independence a **stated fact of the definition** rather than an accident of which tasks happen to share no edge — a reader (or a validator, eventually) can tell at a glance that `generate_ident` failing was never supposed to take `downmix_audio` down with it. ## Merging branches that may not both arrive A task that needs output from two upstream branches raises a question `failure_mode` doesn't answer: what happens at the join if one branch failed? **The honest default: let it fail.** `depends_on: [generate_ident, downmix_audio]` makes the merge task eligible once both have *finished* — a failed finish counts. If the merge genuinely cannot do its job with only one input, list both in `used[]` unconditionally and let it fail naturally when a reference resolves to nothing. That failure is correct: the merge really could not merge. **The tolerant version: gate the optional input with `exists()`.** ```json { "id": "combine_to_deliverable", "depends_on": ["generate_ident", "downmix_audio"], "executor": { "type": "service", "ref": "conform" }, "inputs": { "video_key": "$.tasks.generate_ident.outputs.output_key", "audio_key": "exists($.tasks.downmix_audio.outputs.output_key) ? $.tasks.downmix_audio.outputs.output_key : null" }, "used": ["ident_image"], "produced": ["combined_deliverable"] } ``` Now a failed `downmix_audio` produces a picture-only deliverable instead of no deliverable at all — a deliberate choice, expressed once, at the one place that needs to make it. **Resist adding a second field that says the same thing.** There is no `on_upstream_failure: require_any` in the specification, and none is planned — `exists()` already expresses "proceed with whatever arrived," and a second mechanism doing the same job would just be two places a reader has to check instead of one. See [Reference syntax](/reference-syntax#when-expressions) for the full grammar. ## Fan-out: the same question at a scale neither of the above answers `CONTINUE` and `exists()` both assume a small, named set of branches — you can point at `generate_ident` and `downmix_audio` because there are two of them. Neither one has an answer for "this same subgraph, several hundred times, over a list only known at run time" — and neither should, because [generating one task per item](/implementation-considerations#fanning-work-out-over-a-collection) is genuinely the better tool for the tens-of-items case that `CONTINUE`/`exists()` were built around. **[`fan_out`](/tasks#fan-out-v091) (v0.91) is for the case that argument stops holding.** The concrete scenario it was built for: several hundred concurrent instances of the same generation subgraph, running live, on stage, while someone presents — where the question is never "list every instance's status," it's "did enough of them come back." ```json { "id": "generate_all_idents", "executor": { "type": "agent" }, "subgraph": { "format": "owg", "ref": "graphs/generate_ident.owg.json" }, "fan_out": { "over": "$.params.presenter_prompts", "as": "prompt", "max_concurrency": 500, "tolerated_failure_percentage": 3 } } ``` **Why a percentage, and not just `CONTINUE` on 500 generated tasks:** at five tasks, a human reads the run and judges whether the outcome was good enough. At 500, nobody is reading 500 rows in real time — the run itself has to be able to say "485 of 500 succeeded, that's a pass" the moment the last instance finishes, without a person deciding it afterward. That's a **declared, checkable line**, not a bigger version of the same tolerance — see [why this needed its own field](/tasks#why-this-needed-its-own-field-not-just-failure_mode-continue) for the fuller version of this argument. **Choosing the threshold is a judgment call worth making deliberately, not defaulting.** A few starting points: - **Cosmetic/exploratory output** (a batch of concept images where a few misses are expected and easily re-run) — a generous tolerance, 5–10%, is reasonable. - **A live, on-stage demo** — set the tolerance to what still *reads* as a success to an audience watching a summary tile, not to what's merely acceptable on a spreadsheet. 3% of 500 is 15 visibly-missing tiles; decide if that's fine before the show, not during it. - **Anything feeding a delivery gate** — tolerate nothing (`tolerated_failure_count: 0`, or skip `fan_out`'s tolerance and let the default `HALT` apply to the fan-out task as a whole). A missing deliverable is not a statistic. **Retry still runs first, per instance.** If the task also declares `retry`, an instance only counts against the tolerance once its own retries are exhausted — `fan_out` and `retry` compose rather than one superseding the other. **`max_concurrency` bounds a real external constraint, not a target to hit.** State it at the number a vendor API, a GPU pool, or a cost ceiling actually allows — an engine may run fewer instances at once for its own reasons, but must never run more than the ceiling states. See [Fan-out concurrency is a ceiling, not a guarantee](/implementation-considerations#fan-out-concurrency-is-a-ceiling-not-a-guarantee). ## Putting it together A single production graph typically uses all three mechanisms, at the altitude each one fits: - `failure_mode` on individual named tasks, for the handful of branches an author can point at directly. - `exists()`-gated `inputs` on any task that merges two of those branches and should tolerate one being missing. - `fan_out` with a stated tolerance on the one task in the graph that is actually hundreds of instances of the same thing. None of these needs the other two present to be worth using on its own — a graph with one fan-out task and nothing else reaching for `CONTINUE` is a completely ordinary graph. Reach for each one for the shape of problem it actually solves, not by default. --- # Standards bindings *Section: Ecosystem* > How OWG references OpenUSD, BBC TAMS, OpenTimelineIO, C2PA, SOM, and OMC without absorbing their internals — and the field-level hooks each binding uses. The single most important design decision in OWG is not in the schema; it is a rule about what the schema must **not** contain: > **OWG references the asset-level objects and identifiers of adjacent standards. It never absorbs their internals.** A workflow graph that tried to mirror every media segment, every scene primitive, and every timeline event would inherit the scale and semantics of three other domains and collapse under them. Instead, each neighbouring standard keeps owning its plane, and OWG occupies the plane they each deliberately place outside their own scope. This is why the composite is complementary rather than overlapping: each standard's own scope document explicitly excludes the others' concerns. ## The planes | Plane | What it holds | Owner | |---|---|---| | Essence / timerange | The bytes, addressed by identity and time | BBC TAMS | | Spatial / composition | What a scene contains, how assets compose | OpenUSD | | Editorial / time | The ordered cut — which timeranges, in what sequence | OpenTimelineIO | | Creation-time marking | Signed content credentials at capture or generation | C2PA | | Narrative / editorial context | What a story means right now, and who has cleared what to go out | SOM | | Governance / execution | Which versions exist, who made them, from what, under what authority, rights, approvals | **OWG**, in OMC's vocabulary | ## Four mechanisms Every binding uses the same four mechanisms. Learning them once is enough to bind a standard that is not yet mapped. ### 1. Identifier pass-through Following MovieLabs' best practice for OMC-conformant systems, OWG **never strips an incoming identifier**, even one it does not itself use. An asset arriving with another system's identifier keeps it, stored in the asset's `identifiers` array alongside its scope. ```json { "id": "plate_0140", "type": "video", "identifiers": [ { "scope": "tams.source", "id": "0f7e...-a91c" }, { "scope": "usd.assetInfo", "id": "asset://hero_prop/v23" }, { "scope": "frameio.id", "id": "b2c8e9f1" }, { "scope": "c2pa", "id": "urn:uuid:9e21..." } ] } ``` Any system requesting the entity gets the full identifier set back. Preserved identifiers are not merely good manners — they are the highest-confidence signal available for relating objects that arrive from different systems. Documented scopes: `tams.source`, `usd.assetInfo`, `frameio.id`, `c2pa`, `urn`. The list is extensible; the scope string identifies the issuing system. ### 2. Declared assurance on every cross-system edge Relating an entity in one system to an entity in another is sometimes certain and often not. Is this scene file the same asset as that stored source? Sometimes a shared identifier settles it; sometimes only a judgement call can. OWG does not specify how an implementation reaches that judgement. It specifies that the answer must **carry its own assurance** into the graph: | Assurance | Meaning | |---|---| | `attested` | Backed by a verified content credential | | `asserted` | Declared by a system of record, or confirmed by a human | | `inferred` | Proposed rather than declared — carries a confidence score and a method identifier | So the graph records not just *that* two things are related but *how certain* the relationship is and *who or what* vouched for it. Acceptance is recorded too, which means the graph's own provenance rules apply to the graph's own construction. **Matching technique and acceptance policy are implementation concerns**, deliberately outside this specification. Two conformant implementations may relate the same two systems with different confidence and different methods; what interoperates is the record they leave. The required fields are in [Relationships](/relationships). ### 3. Overlay, not migration A conformant implementation can build a graph over the systems a production already runs, rather than requiring them to be replaced. The specification's contribution to that is narrow and important: a connector reads a standard's **native** construct and turns it into a typed edge, computing nothing the standard already computes. Composition algebra, time resolution, and playback stay exactly where they are. No production adopts a graph by re-entering its data — and nothing in OWG requires it to. How a given implementation performs ingestion is its own business. ### 4. Security as a property of the node Because a task's `subgraph` scopes a vendor turnover to only the nodes it needs, and because a task carries its own validity window, access to any asset is bounded in **both scope and time** without a separate access-control system to configure and keep in sync. See [Security model](/security-model). ## The bindings Each binding has a specific hook that already exists in the neighbouring standard — which is what makes this pattern engineering rather than analogy. ### OMC — the vocabulary, not a neighbour OMC is not adjacent to OWG; it is the language OWG is written in. The mapping is an identity rather than a translation. OWG inherits OMC's entity model and relationship vocabulary directly, and inbound OMC instances are validated against the OMC schema before anything enters a graph. Edge correspondences to OMC v3.0: | OWG | OMC v3.0 | |---|---| | `used` | `edges.uses` (Asset) | | `produced` | `edges.produces` (Asset; inverse `productOf`) | | `depends_on` | `edges.informedBy` (Task; inverse `informs`) | | `ran_on` | `edges.uses` (Infrastructure) | | `performed_by` | `Participant.participantFunction.Role` | | `works_for` | `edges.memberOf` | > **Tracking a moving target.** OMC v3.0 is still being finalised; v2.8 is the last stable release. MovieLabs issued the first preliminary v3.0 release on 2026-08-05, with further releases scheduled for September (video pipeline, transforms, colour spaces, infrastructure classes) and December (asset function/structure correspondence tables, relationship tables). Note in particular that **there is no Shot entity in OMC v3.0** — the video pipeline from production scene through editorial shots and sequences arrives in the September release, so no Shot mapping should be reintroduced before then. ### OpenUSD — composition arcs become typed edges USD composes a scene from reusable pieces and deliberately leaves version, approval, and rights questions to someone else. OWG is that someone. The hook: **USD ships a purpose-built `assetInfo` metadata slot on every published asset**, so OMC identifiers ride there today with no schema change. The reference, payload, and sublayer arcs that already exist in a stage file become typed edges a connector reads rather than infers. ```json { "id": "shot_0140_assembly", "composes": [ { "component": "hero_prop_v23", "arc_type": "reference", "prim_path": "/World/Set/Hero", "variant_selection": "lod=high" }, { "component": "env_backlot_v4", "arc_type": "payload", "prim_path": "/World/Env" } ] } ``` Arc types: `reference`, `payload`, `sublayer`, `inherit`, `specialize`, `variant`. Critically, **the graph never descends below the published-asset boundary.** Prim paths appear only as opaque edge properties — OWG does not parse or interpret them. That boundary is the whole discipline in one sentence. What this enables: because the graph governs the referenced pieces, it answers what neither an asset manager nor a tracker can — *which shots reference Hero v23, and if we retire it, which shots break and need re-approval*. In a conventional pipeline that question is answered by querying scene files, tribal memory, and a supervisor's best guess. Here it is a graph traversal, and the approval history of every component comes attached, because approvals are structural edges. ### BBC TAMS — growing assets and opaque timeranges A live feed is a flow still being appended: an asset that is *growing*, with QC and highlight tasks hanging off it in near real time. The hook: **TAMS's own addressing is a source UUID plus a timerange.** Two systems referencing overlapping timeranges of the same source can be related deterministically rather than guessed at, and OWG can track a feed still being captured without mirroring a single segment. ```json { "id": "match_feed_live", "type": "video", "state": "growing", "storage": { "provider": "tams", "timerange": "[1500:0_1620:0)" }, "identifiers": [{ "scope": "tams.source", "id": "0f7e...-a91c" }] } ``` Two rules make this work: - The timerange string is **stored opaquely**. OWG does not parse or interpret time. The half-open `[s:ns_s:ns)` syntax is TAMS's, and resolving it to bytes is TAMS's job. - While an asset's `state` is `growing`, its `identity` is the TAMS source UUID rather than a content hash, and the producing ingest task stays open. When the referenced range closes, fixity is computed over the now-bounded range and the state advances. A highlight is therefore a bounded timerange of the source: no copy, no transcode, lineage intact across the live seam. This carries the governance model from file-based work into live sports and news, using the storage layer the broadcaster already deployed. > **A deliberate non-goal.** The mapping between International Atomic Time and SMPTE timecode is an unresolved industry-wide problem. OWG does not attempt to solve it and defers to essence-layer tooling. ### OpenTimelineIO — the timeline as a governed asset A timeline is an asset whose clips are edges to other assets, each with a source timerange. The hook: **OTIO travels.** Its adapters already convert CMX 3600 EDLs, Avid AAF, Final Cut Pro XML, and ALE, so one OWG-to-OTIO binding covers the editorial ecosystem including the proprietary formats. Its open `metadata` dictionary is a ready two-way slot for the same identifiers used everywhere else in the graph. What this enables is editorial's oldest question, answered per clip: *does the current cut contain the latest approved version of everything?* One comp is stale — version 3 is in the cut, version 4 was approved after the edit was saved. The reconform becomes one action and one record: a task, attributed, timestamped, from v3 to v4, on the approving supervisor's authority, and the timeline advances by a typed revision. The conform stops being reconciliation-by-filename. A related benefit: reviewers with permission can stream a proxy assembly of the cut — guaranteed current — while the underlying assets never leave their store and were never within the reviewers' reach at all. ### C2PA — the attested tier C2PA content credentials supply the `attested` assurance level directly. A verified manifest is the strongest statement the graph can hold about an asset's identity and origin. The layering is worth stating precisely, because these three are often conflated: - **C2PA** is the creation-time marking layer. - **The EU AI Act icon set** is the publication-time labelling layer. - **OWG** is the production-time tracking layer between them — the lineage produced as a side effect of running the work. Generated assets *should* carry content credentials; the generative-AI profile states this as a recommendation. ### SOM — the narrative plane A story is what a newsroom's tools have been missing a shared description of: not the footage, not the rundown position, but what the desk currently believes is true, and who has cleared what to go out where. The hook: **SOM's own Link entity already has no identity beyond its endpoints.** It connects an asset to a destination and carries a mutable compliance gate — exactly a relationship's shape, not a registry's. A Telling references that link, not the asset directly, so the graph never needs a "which outlet, under what clearance" field bolted onto the asset itself. ```json { "relationships": [ { "id": "rel_link_e1", "relation": "routed_to", "from": { "kind": "asset", "id": "a1" }, "to": { "kind": "infrastructure", "id": "dest_breakfast_bulletin" }, "assurance": "asserted", "profiles": { "broadcast-newsroom": { "gate_status": "CLEARED", "link_id": "018f3a2e-...-e1" } } } ] } ``` What this enables: a claim later confirmed or withdrawn changes gate status on the edge, not on the asset. The same footage can be cleared for one outlet and held from another at the same moment, without two copies of the asset existing anywhere — and a downstream query ("which tellings used a since-withdrawn link?") is a graph traversal, not a cross-system reconciliation. > **A deliberate non-goal.** OWG does not model SOM's editorial vocabulary — story phases, evidential sourcing tiers, assertion review states — in core. It lives entirely in the `broadcast-newsroom` profile (see [Profiles → Registry extension profiles](/profiles#registry-extension-profiles)), the newest of the four mechanisms above, added specifically because this binding needed it: SOM's delivery messages fit `asset.storage` unchanged, but its editorial state fits nowhere in core without widening it for every implementer, broadcast or not. ## Extending the pattern The recipe should extend cleanly to standards not covered here — ACES colour transforms and MaterialX look graphs among them — because each already exposes: 1. Its own native composition or reference construct, which becomes a typed edge. 2. Its own extension slot for an external identifier, which carries OMC identifiers both ways. Binding a new standard is a connector to build, not a rearchitecture. One rule governs the boundary: leave every computation the standard already performs where it is. **Only when a referenced operation is itself *work* — a generative transition, a colour transform — does it get promoted to a task with an executor and an attributed author.** ## Known limitations Stated plainly, because interoperation claims deserve scepticism: - The mappings above are implemented at the schema and connector level. Deeper integrations — a governance-graph-backed asset resolver for USD, bidirectional write-back to federated systems, full growing-asset semantics for TAMS — require real engineering and, in some cases, design partners. - The value of interoperation is a network effect the model does not control. An OWG record is most valuable once the neighbouring standards' tools emit and consume the mapped identifiers, and OMC adoption in production remains thinner than in specification. - Connectors are a standing maintenance liability, not a one-time build. Every source system brings its own entity-resolution and event-ordering quirks. None of these undermines the central claim. They scope it. --- # Security model *Section: Ecosystem* > Why permissioning is a property of the graph rather than a system wrapped around it — and how scope- and time-bounded access falls out of the schema. OWG treats security as a property of the node, not a system wrapped around the graph. The three classic zero-trust principles map onto the workflow layer directly. ## Verify explicitly Every action attributes to a verified participant. Because agents are participants too — each with an `operated_by` chain to the human on whose authority it acts — the question *"who did this, and on whose say-so"* is a query rather than an investigation. ```json { "participants": [ { "id": "s.okafor", "kind": "human", "role": "vfx-supervisor", "organization_id": "org_studio" }, { "id": "agent_denoise", "kind": "agent", "operated_by": "s.okafor", "organization_id": "org_studio", "identity": { "scheme": "did", "id": "did:web:example.com:agents:denoise", "credential_scope": ["read:assets", "write:outputs"] } } ] } ``` An agent's authority is never ambient. It is a declared edge to a responsible principal, and its `credential_scope` bounds what it may do. An asset produced by that agent traces to the supervisor and the organization without additional record-keeping. The generative-AI profile makes `operated_by` **required** for agent executors, which is the one place the profile system currently expresses a real constraint. ## Least privilege Permissions attach **per object**, and the enforcement pattern is the turnover: a vendor receives the subgraph they need and nothing else. Access follows the graph, not the org chart. Two schema features do the work: - **`subgraph` on a task** scopes what crosses the wall. A vendor gets plates, an EDL, a LUT, and notes — not the show. - **A task's validity window** bounds *when*. Access expires with the task rather than lingering until someone remembers to revoke it. Together these make access bounded in both scope and time **by construction** rather than by policy. Nothing is reachable by default, every grant is itself a structural edge in the same graph that records who touched what, and there is no separate access-control system to keep in sync. This is what makes cross-company work tractable. The vendor's own pipeline stays their business; when the comp returns it does not land in an inbox, it reconnects to the show's graph with lineage attached — which participant, what plate, how many rounds. ## Assume breach The specification's contribution here is narrow: because an asset is addressed by a **storage locator** rather than by being held in the graph, an implementation is free to place high-consequence content behind a hardened storage tier without the workflow document changing at all. A locator names a provider and an address. What that provider does behind the address — ordinary object storage, an encrypted store, or a specialised protective tier — is outside this specification. The same document runs against any of them, which is the point: protection is a deployment decision, not a schema decision. What OWG does require is that the *governance* record stays with the asset node regardless of where the bytes sit. See [Assets and versioning](/assets-and-versioning#storage-locators). ## The audit trail is the system of record Approvals, rights, and clearances are **graph events** with participants and timestamps, not fields overwritten in place. Combined with typed version relations (see [Assets and versioning](/assets-and-versioning)), this means: - Nothing is overwritten, so nothing needs reconstructing. - An approval names who approved what, when, and on which version. - A rejected asset stays in the graph in a `rejected` state, not as an absence. Version chains preserve everything. The audit trail is not assembled after an incident; it is the ordinary operating record. ## Credentials A task's `executor.credentials_key` names a secret held by the orchestrator. Secrets are **referenced by key, never inlined**, so the workflow document itself stays safe to share, commit, and hand to a vendor. ```json { "id": "dub_es_419", "executor": { "type": "saas_api", "credentials_key": "tts_provider_api_key", "permissions": ["read:assets", "write:outputs"] } } ``` The `saas_api` profile entry marks `credentials_key` as required for exactly this reason. ## What this specification does and does not do Worth being precise, because a security model is only useful if its boundaries are clear. **The specification defines what must be expressible:** - Every action attributes to an identified participant. - An agent's authority is a declared edge to an accountable principal, with a bounded scope. - Access scope is expressible as a subgraph, and access lifetime as a task's validity window. - Approvals, rights, and clearances are recorded as events with actors and timestamps. - Nothing is overwritten; superseded and rejected states are retained. **The specification does not define how any of it is enforced.** Authentication, authorization mechanism, policy storage, key management, tenancy isolation, and audit retention are all implementation concerns. Two conformant implementations may enforce the same document very differently, and the specification takes no position on which is adequate for your content. The practical consequence: **a document that expresses a scoped turnover is not the same as a system that enforces one.** When evaluating any implementation — including your own — verify enforcement independently of the document format. Expressiveness is a precondition for enforcement, not evidence of it. If you take one rule from this page: **a permission is an edge, and an edge is only as good as the writer that created it and the assurance level it carries.** --- # AI disclosure and compliance *Section: Ecosystem* > How EU AI Act Article 50 disclosure becomes a lineage query, and the six schema fields that make a per-asset disclosure manifest computable. Article 50 of the EU AI Act came into force on **2 August 2026**. It requires deployers to disclose AI-generated public-interest content and deepfakes, with penalties reaching EUR 15M or 3% of global turnover. Disclosing correctly requires knowing, per asset, what is synthetic. That is a lineage question — and no tracker, asset manager, or render manager can answer it, because none of them held the whole chain. OWG holds the lineage as an ordinary by-product of running the work, so the obligation becomes a query rather than a project. ## What the regulation distinguishes Three distinctions matter, and conflating them is the most common mistake: | Category | Treatment | |---|---| | **AI-generated** — created or substantially altered by AI | Disclosure required | | **AI-assisted** — standard editing assistance, no substantive semantic alteration | Exempt | | **Depicts a real person** | Triggers the deepfake provisions | The exemption is narrower than people hope and wider than they fear. Denoise, up-res, and grammar fixes are assistive. A voice clone, synthetic imagery, AI-written text, or a lip-sync dub is generative. ## The six fields Disclosure is **declared** at the point of work and **computed** at the point of delivery. Six schema fields carry the declarations: | Field | Level | Values / purpose | Article | |---|---|---|---| | `ai_role` | Task | `generative` \| `assistive` \| `none` | 50(2) | | `depicts_real_entity` | Asset | Does this depict an identifiable real person? | 50(4) | | `assurance` | Asset | `attested` (verified credential) \| `asserted` \| `inferred` | — | | `work_type` | Context | `creative-fictional` \| `factual-news` \| `advertising` \| `sports` \| `other` | 50(4) | | `editorial_signoff` | Task | Named editor accepting responsibility | 50(4) | | `disclosure_gate` | Governance | Blocks delivery until certified | 50 | The critical design choice: **the per-asset conclusion is computed from lineage, not stored.** An asset is synthetic, hybrid, or traditional because of what its ancestry says, not because someone remembered to tick a box on it. Storing the conclusion would let it drift out of agreement with the work that produced it. ### Declaring AI involvement `ai_role` is declared on the task, and **AI-ness is never inferred from the executor type**. The schema says so explicitly. An `agent` executor might be doing deterministic file shuffling; a `saas_api` executor might be running a diffusion model. Only the explicit declaration is authoritative. ```json { "id": "dub_es_419", "type": "work", "label": "Spanish (LatAm) lip-sync dub", "executor": { "type": "saas_api" }, "ai_role": "generative", "performed_by": "agent_dubber", "used": ["master_audio_en", "reference_perf_v2"], "produced": ["dub_es_419_v1"] } ``` ```json { "id": "denoise_plate_0140", "type": "work", "executor": { "type": "local_app" }, "ai_role": "assistive", "performed_by": "a.rivera", "used": ["plate_0140_raw"], "produced": ["plate_0140_dn"] } ``` The second is exempt from marking, and the graph records *why* it is exempt rather than leaving the exemption as an undocumented assumption. ### Editorial sign-off Where the regulation expects a named human to take responsibility, `editorial_signoff` names one. The `editor` field is required within it: ```json { "id": "review_copy_final", "type": "review", "performed_by": "j.mbeki", "editorial_signoff": { "editor": "j.mbeki", "statement": "Reviewed for factual accuracy; accepts editorial responsibility.", "timestamp": "2026-08-19T14:22:00Z" } } ``` A participant with `role: "editor"` is the one eligible to provide it. ### Governance ceilings Run-level ceilings sit alongside the disclosure gate, which is what makes the gate more than advisory: ```json { "governance": { "disclosure_gate": true, "max_total_reroutes": 12, "budget_usd": 4800 } } ``` With `disclosure_gate` set, delivery-typed tasks are **blocked** until the compliance validator certifies the project and emits the disclosure manifest. Compliance stops being a pre-delivery scramble and becomes a condition of delivery. ## The baseline rule pack The specification defines a **baseline rule pack** — six rules a conformant compliance validator must implement. Rules are evaluated against a delivery-typed task and the lineage of the assets it delivers. | Rule | Check | Severity | |---|---|---| | **R1** | A task whose executor can involve AI (`agent`, `saas_api`, `comfyui_graph`, `comfyui_node`) leaves `ai_role` unset | Error | | **R2** | A delivered asset whose lineage contains a `generative` task carries no disclosure marking | Error | | **R3** | A delivered asset whose lineage contains both a `generative` task and an asset with `depicts_real_entity: true` carries no deepfake disclosure | Error | | **R4** | An AI-generated text asset delivered in a `factual-news` context has no `editorial_signoff` by a participant with `role: "editor"` in its lineage | Error | | **R5** | `governance.disclosure_gate` is true and a `delivery` task completed without a certified manifest asset in its `used[]` | Error | | **R6** | A disclosure-relevant value (`ai_role`, `depicts_real_entity`) rests on an `inferred` relationship with no `accepted_by` | Warning | Three of these deserve a note. **R1 is deliberately conservative.** Silence is not a declaration of innocence. An unset `ai_role` on a task that *could* involve AI is a finding, not a default of "no AI" — which is why declaring `ai_role: "none"` explicitly is an [authoring rule](/for-agents#authoring-rules) rather than optional politeness. **R3 is independent of `work_type`.** The deepfake provisions attach to depicting a real person, not to the genre. A synthetic likeness in a drama triggers R3 exactly as one in a news package does. **R6 is a warning rather than an error**, because an inferred disclosure input is not wrong — it is unconfirmed. A human accepting it clears the finding, which is the [acceptance record](/relationships#an-inferred-edge-end-to-end) doing its job. Certifying on an unaccepted inference is a defensible choice; doing it without knowing you did is not. ### Extending the pack The baseline is the floor, not the ceiling. Jurisdiction- and client-specific rules are added as a **versioned pack** layered over it, because obligations differ by territory and regulation keeps developing — binding every rule into the core would date the specification. Two requirements on any pack: - **It must version**, and a certification must record which pack version judged it. When regulation changes you need to know what standard a past delivery was held to. - **It may add and tighten, never remove.** A pack that disables a baseline rule is not conformant. See [Implementation considerations](/implementation-considerations#compliance-rule-sets) for how to structure one. ## The deliverable The output is a **signed, per-asset AI disclosure manifest** — the document a broadcaster can hand a regulator. It is itself an asset, produced by a task, performed by a responsible participant. Compliance has provenance too: the manifest can answer who generated it, from what graph state, and when. A manifest that could not account for its own origin would be a strange thing to hand a regulator. ## Where OWG sits Three layers, often conflated: 1. **Creation-time marking** — C2PA content credentials, applied at capture or generation. 2. **Production-time tracking** — the lineage OWG produces as a side effect of running the work. **This is the layer that was missing.** 3. **Publication-time labelling** — the EU icon set, applied at distribution. Layers 1 and 3 already existed. Without layer 2, an organization can mark at creation and label at publication and still be unable to say which of ten thousand delivered assets needs the label — because nothing tracked the chain in between. --- # Versioning and stability *Section: Ecosystem* > What v0.92 defines, how the specification versions, where it deliberately leaves room to extend, and what that means for building against it. This page tells you what you can rely on. It covers what v0.92 defines, how stable each part is, and where the specification deliberately leaves room — so you can build against it with a clear picture of what will and will not move. See the [changelog](/changelog) for the version history itself. ## What v0.92 defines v0.92 is v0.91 plus one addition — an optional `profiles` object on `context`, `asset`, and `relationships[]`, letting a profile carry structured, namespaced data directly on these registries rather than only on `task.tool`. Every v0.91 document remains valid; nothing was changed or removed. See [Profiles → Registry extension profiles](/profiles#registry-extension-profiles). v0.91 was v0.9 plus one addition — `task.fan_out`, for repeating a task (and its subgraph) over a list with bounded concurrency and a tolerated-failure threshold, rather than one instance's failure aborting every instance depending on it. See [Fan-out](/tasks#fan-out-v091) and the [failure-handling best practices](/failure-handling) for when to reach for it instead of generating tasks or using `failure_mode: CONTINUE`. The specification is complete for the workflows it describes. Its settled surface: **The model** - Five entity types — organizations, participants, infrastructure, assets, contexts — plus the tasks that connect them, with OMC's relationship vocabulary. - A W3C PROV backbone: tasks are activities, assets are entities, participants are agents. - Structural provenance edges (`used`, `produced`, `performed_by`, `ran_on`) as the authoritative lineage record. - Typed version relations: `revision`, `variant`, `derivation`, `representation`, `alternative`. - Three assurance levels, with `confidence` and `method` required on `inferred`. **The document** - One document class, with `owg_version` and `id` as the only required fields. - `params` for run-time parameter injection. - `governance` ceilings — re-route limits, spend limits, and the disclosure gate. - Closed objects throughout, with `metadata`, `x-` prefixes, and (v0.92) a namespaced `profiles` object as extension points. **The behaviour** - Executors as an attribute, spanning human, service, agent, application, and generative work. - Failure modes, compensation, retry with backoff, and gate routing with re-route ceilings. - Bounded-concurrency fan-out over a list, with a tolerated-failure threshold (v0.91). - Formal grammars for `$.` references and `when` expressions, with a defined run-state vocabulary. - Four ordered validation passes and a complete error-code set. - Subgraphs for nesting and for scoped hand-offs, with a specified turnover document shape. - Run documents recording per-attempt status, cost, and actual infrastructure. - Three testable conformance levels, and a baseline compliance rule pack. - Bindings to OMC, OpenUSD, BBC TAMS, OpenTimelineIO, C2PA, and SOM — by reference, never by absorption. ## Stability by area v0.92 is a draft version: the number reaches 1.0 when the specification has enough implementation experience behind it to promise compatibility. That is a deliberate sequencing choice — promising stability before anyone has built against a specification is how standards acquire mistakes they cannot remove. Meanwhile, this is how much movement to expect: | Area | Stability | |---|---| | Entity model, PROV mapping, version relations | **Stable.** Changing these would change what OWG is | | Provenance edges, assurance levels | **Stable** | | Document shape, required fields, `params`, `governance` | **Stable** | | Reference and `when` grammars | **Stable.** Small and complete by design | | Validation passes and error codes | **Stable.** Codes may be added; existing ones will not change meaning | | Enum values (executor types, states, task types) | **Additive.** Expect new values, not removed ones | | `identifiers[]` scopes, storage providers, relation predicates | **Open sets.** Designed to grow | | Standards bindings | **Additive.** New bindings follow the existing pattern | | Compliance rule identifiers | **Additive.** Fields are stable; the rule set grows | | `subgraph`, nesting limits, turnover documents | **Stable.** Format discriminator, parameter interface, depth limit, and turnover shape are specified | | Run documents | **Stable.** See [Run records](/runs) | | Conformance level definitions (L1/L2/L3) | **Stable.** Each is defined testably | | Compliance baseline rules R1–R6 | **Stable.** Packs may add and tighten, never remove | | JSON-LD context coverage | **Additive.** Complete for v0.9 fields; grows with the schema | | `fan_out` (v0.91) | **Additive, and new.** The two tolerance fields (`tolerated_failure_percentage`, `tolerated_failure_count`) are the part most likely to gain siblings — e.g. a per-tag tolerance — as real fan-outs surface cases this first cut does not cover | | `profiles` on `context`/`asset`/`relationships[]` (v0.92) | **Additive, and new — and not yet validator-enforced.** The slot is stable; whether and how the validator resolves a named profile's `registry_extensions` against it is still open. See [Profiles → Registry extension profiles](/profiles#registry-extension-profiles) | **Practical guidance:** pin the schema you validate against. Additive change is safe to ignore until you want it; pinning means you decide when to adopt it. ## Where the specification leaves room Three extension mechanisms exist so that the core can stay small without becoming limiting. Using them is the intended path, not a workaround. **Profiles** carry domain and vendor vocabulary. Two kinds: tool names, billing tiers, device types, and check identifiers for `task.tool`; and, since v0.92, structured data on `context`, `asset`, and `relationships[]` via the namespaced `profiles` object — used, for instance, by the `broadcast-newsroom` profile binding the Story Object Model. If you want a new value for `tool` or `billing.tier`, or a field meaningful only to one domain, it belongs in a profile, and profiles version independently of the core. See [Profiles](/profiles). **`metadata`** is a freeform object on the document and on every entity. Anything you need to carry that the specification does not define goes here, and it survives round-trip untouched. **`x-` prefixed keys** sit alongside specified fields for vendor extensions that want to be visible rather than nested in `metadata`. **Open sets** — `identifiers[]` scopes, storage providers, relation predicates, participant and organization roles, task types, asset types — are strings rather than enums precisely so you can use a value the specification has not anticipated without waiting for a release. What does **not** belong in an extension: anything the core already expresses. Duplicating a structural edge, or carrying a version relationship in `metadata` rather than in `version_relation`, produces a document that validates and interoperates poorly. ## Decisions left to implementations The specification defines the format and what it must record. A set of decisions belongs to the systems built on it — storage design, federation strategy, matching technique, acceptance policy, enforcement mechanism, and confidence calibration. That boundary is what makes the format portable: two implementations can differ substantially and still exchange documents losslessly. [Implementation considerations](/implementation-considerations) covers each of these decisions with a recommended approach and the reasoning behind it, and [Persistence requirements](/persistence) states the query capabilities an implementation must provide. ## Conformance Three levels, each with a testable definition in [Profiles](/profiles#conformance-levels): | Level | Needs | Tests | |---|---|---| | **L1** | The document | All four validation passes succeed | | **L2** | The document plus its resolved profiles | Every `tool` is declared, and its inputs and outputs satisfy the declaration | | **L3** | A [run document](/runs) | Every succeeded attempt records `performed_by`, `ran_on`, produced assets with identity and assurance, and a terminating authority chain | A definition cannot be L3-conformant — only an execution can. State which level you mean; "OWG-conformant" alone is ambiguous. The [conformance corpus](/conformance) exercises L1. It contains only documents expected to pass, so it demonstrates acceptance rather than correct rejection; write negative fixtures alongside it. ## Reading this documentation Where these pages describe behaviour, they describe **what the specification requires** — not what any particular implementation currently does. When you rely on a validator, engine, or store you did not write, establish its actual coverage before treating a clean result as a guarantee. --- # Changelog *Section: Ecosystem* > What changed in each version of the specification, and when. Every entry here corresponds to a schema artifact at its own permanent URL — see [Schema identity](/specification#schema-identity). A document that declares an older `owg_version` keeps validating against that version's schema; nothing here is retroactive. ## v0.92 — 2026-09-14 **Added:** an optional `profiles` object on `context`, `asset`, and `relationships[]` (additive) — a third profile kind alongside domain and vendor profiles, reserving one namespaced key per registry object so a profile can carry structured data on these registries, not only on `task.tool`. See [Profiles → Registry extension profiles](/profiles#registry-extension-profiles). **Added:** `broadcast-newsroom` profile (draft, v0.1) — the first profile to use the mechanism above, binding the [Story Object Model](https://storyobjectmodel.com/) (SOM) 1.0 onto OWG's context, asset, and relationship registries. Its `registry_extensions` are published at [/schemas/profiles/broadcast-newsroom/v0.1.json](/schemas/profiles/broadcast-newsroom/v0.1.json). **Why now:** SOM 1.0 publishes newsroom-specific structure, story lifecycle, evidential sourcing tiers, compliance-gated routing, that core has no reason to carry, by the same test that kept BBC TAMS's own domain detail out of core in v0.91: a field meaningless to a VFX or physical-production document doesn't belong where every implementer has to skip past it. The gap this closes is narrower than it sounds — most of SOM binds onto core unchanged (`delivery.media_available` already fits `asset.storage`; the `factual-news` disclosure-gate and editorial-signoff machinery already existed). What was missing was a place for the rest to live without widening core for everyone. **Compatibility:** fully additive. Every v0.91 document is a valid v0.92 document unchanged. `profiles` is a new optional key on three registries; nothing existing was renamed, removed, or given a new required field. **Not yet done:** the validator does not yet resolve a document's declared `profiles[]` against a registry-extension profile's schema and check the corresponding `.profiles.` subtrees. Until that lands, this mechanism is schema-valid but not conformance-checked past L1 — see [Profiles → Conformance levels](/profiles#conformance-levels). ## v0.91 — 2026-09-05 **Added:** `task.fan_out` — repeat a task (and its `subgraph`, if it has one) over a list, concurrently up to a stated `max_concurrency`, with a `tolerated_failure_percentage` or `tolerated_failure_count` so the task can be treated as succeeded despite some instances failing. See [Fan-out](/tasks#fan-out-v091), [Run records → Fan-out attempts](/runs#fan-out-attempts), and the [failure-handling best practices](/failure-handling). **Added:** validator rule `OWG_FANOUT_TOLERANCE_CONFLICT` — a `fan_out` declaring both tolerance fields at once is a validation error, not an ambiguity resolved silently at run time. See [Validation](/validation). **Why now:** the [existing guidance](/implementation-considerations#fanning-work-out-over-a-collection) for repeating work — generate one task per item — holds up well into the tens of items, and stays the right default there. It stops holding at hundreds: a document isn't meaningfully reviewed at that size, and "did enough of them succeed" needs an answer the engine can check, not one a person derives from reading every row. The motivating case was several hundred concurrent instances of the same generation subgraph, run live, where a single unrelated instance failing had no business taking the other 499 down with it. **Compatibility:** fully additive. Every v0.9 document is a valid v0.91 document unchanged — nothing was renamed, removed, or given new required fields. `fan_out` is v0.91-only: declaring it on a document whose `owg_version` is `"0.9"` is a validation error (the field is additive to the *schema*, not retroactively legal on documents targeting the version before it existed). ## v0.9 — draft The version this documentation describes prior to v0.91. See [Document structure](/specification), [Tasks and executors](/tasks), and [Run records](/runs) for its complete surface — this changelog begins tracking forward from here rather than reconstructing v0.9's own history retroactively. ---