# Implementation considerations

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