Profiles
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:
{
"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:
{
"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_byis required on any participant acting as anagentexecutor. An autonomous agent with no accountable principal is not usable under that profile — see Participants and 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 theassurancevocabulary.
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:
{
"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[]:
{
"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 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.<id> 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 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:
- If the task has a
tool, some active profile declares it. - The declaring profile lists the task's
executor.typein itsexecutor_types. - Every input the profile marks
requiredis present in the task'sinputs. - Every input present satisfies the profile's declared type and enum constraints.
- Every key in the task's
outputsappears in the profile'sproduceslist for that tool, unless it is ametadataorx-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, for every attempt with status: "succeeded":
performed_byis present and resolves to a participant in the definition.ran_onis present and resolves to an infrastructure entry.- Every asset in the definition's
produced[]for that task appears in the attempt'sproduced. - Every produced asset carries an
identityand anassurance. - Any participant acting as an
agenthas anoperated_bychain terminating at ahumanororganization.
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.
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
toolas 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.