Subgraphs and turnovers
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.
{
"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
paramsare satisfied by the parent task'sinputs. A child parameter withrequired: trueand 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.
{
"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 and TAMS timeranges: 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, 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.
{
"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: 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.
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.
{
"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.
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 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 |
{
"$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:
- Validate the returned document before merging. It is untrusted input.
- Match entities by id first, then by
identifiers[]for anything the vendor's own system named. - Never strip an identifier the vendor added. It is how the next round matches.
- The returned artifact is a
revisionwhosepredecessoris 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 and Enforcing what the document expresses.