Open Workflow GraphPre-release
Implementing

Implementation considerations

Decisions the specification deliberately leaves to implementers, with recommended approaches and the trade-offs behind them.

View as markdownMachine-readable source for agents and scripted implementers

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

{
  "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 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 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) 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() and read the 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 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 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 is for.

Compliance rule sets

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

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

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 contains only documents expected to pass, so it cannot tell you whether your validator rejects correctly. Cover one case per error code.

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.