# Write OCP from your tool

> Where a producer hooks in, how to map your tool's records onto nodes and attempts, and the choices every producer makes.

A producer turns what your tool knows about a run into one OCP document. There are three places to do that:

| Producer | Hooks in | Example |
|---|---|---|
| The orchestrator itself | Writes the document as it runs, and finishes it when the run ends. | loopmath records the runs it is told about. |
| An exporter | Listens to a harness's hooks, event bus or OpenTelemetry output. | A Claude Code hook script, a CrewAI event listener. |
| A converter | Reads a native log after the fact. | A reader for Codex rollout files. |

[Implementations](https://orchestrationcontextprotocol.io/docs/implementations) lists where each common agent tool can be hooked, and what it can fill.

## Map your records

**1. Decide what a node and an attempt are**

A **node** is a unit of work: a task, a subagent role, a step in a graph. An **attempt** is one try at a node: a session, a subagent run, one pass of a step. A retry is a new attempt at the same node, never an edit of the old one, and its `cause` says why it happened.

**2. Choose stable ids**

Ids are scoped to your producer. Derive them from your tool's own ids, so writing the same run twice gives the same ids. A common pattern is `<node>.a<n>` for attempts.

**3. Record the model and effort as your tool knows them**

Always write `model.raw`, the label exactly as your tool had it. Add `model.id` only when you know the provider's canonical id. Reasoning effort goes in `effort`, never in the model name.

**4. Record cost per attempt, per stream**

Write each token stream separately. OCP's `input_tokens` is **uncached** input: if your source counts cache reads inside input, as Codex, the OpenTelemetry GenAI conventions, LangChain and the OpenAI Agents SDK do, subtract them. Set `basis` to `measured` only when the counts come from usage records of that attempt; if you split a session or daily total across attempts, write `allocated`. Dollars are optional; if you write `usd`, write the `tariff` you priced it with.

**5. Record outcomes with their evidence**

When an attempt ends, write its `status` and an `outcome` whose `result` matches it. Say how the result is known with `evidence`: a check that ran is `verified`, a reviewer's verdict `reported`. `via` names the node that judged, and `receipt` holds a short label. See [Evidence](https://orchestrationcontextprotocol.io/docs/learn/evidence).

**6. Add edges with their tier**

If your tool knows the order or lineage of work, add edges: `dep` for "must finish first", `spawn` when one attempt started another, `artifact` when one read what another wrote. Every edge carries a `tier`.

**7. Declare capabilities**

`producer.capabilities` says which elements you can write. A `false` promises the element is absent, so a reader can tell "not recorded" from "did not happen".

## A minimal producer

Standard library only. It writes a two-node run with a review, and passes `loopmath ocp validate` with no findings.

```python emit.py
import json
from datetime import datetime, timezone

def now():
    return datetime.now(timezone.utc).isoformat(timespec="seconds")

def attempt(node, n, *, harness, model, effort, usage, status, evidence=None, receipt=None, cause=None):
    a = {
        "id": f"{node}.a{n}",
        "node": node,
        "harness": harness,
        "model": {"raw": model},
        "effort": effort,
        "status": status,
        "cost": {
            # OCP input_tokens is uncached input. Subtract if your source counts cache reads inside input.
            "input_tokens": usage["input"] - usage.get("cache_read", 0),
            "cached_input_tokens": usage.get("cache_read", 0),
            "output_tokens": usage["output"],
            "basis": "measured",
        },
        "outcome": {"result": status, "evidence": evidence or "asserted"},
    }
    if receipt:
        a["outcome"]["receipt"] = receipt
    if cause:
        a["cause"] = cause
    return a

doc = {
    "ocp": "0.3",
    "producer": {
        "name": "my-orchestrator",
        "version": "1.0.0",
        "emitted_at": now(),
        "capabilities": {"edges_dep": True, "cost_tokens": True, "outcome_evidence": True, "cost_usd": False},
    },
    "privacy": {"profile": "metadata_only"},
    "run": {"id": "run_7f3a", "started_at": now()},
    "nodes": [{"id": "implement", "kind": "impl"}, {"id": "review", "kind": "review"}],
    "edges": [{"from": "implement", "to": "review", "kind": "dep", "tier": "verified"}],
    "attempts": [
        attempt("implement", 1, harness="claude-code", model="claude-opus-5-5", effort="high",
                usage={"input": 330000, "cache_read": 310000, "output": 16000}, status="done"),
        attempt("review", 1, harness="codex", model="gpt-5.6-sol", effort="medium",
                usage={"input": 90000, "cache_read": 60000, "output": 4000}, status="done",
                evidence="reported", receipt="approved"),
    ],
}

with open("run.ocp.json", "w", encoding="utf-8") as f:
    json.dump(doc, f, indent=2, ensure_ascii=False)
```

## Checklist

- [ ] `privacy.profile` is set, and under `metadata_only` no field holds prompt, completion or transcript text.
- [ ] Every free-text field is a short label. The schema caps label fields at 500 characters; strings it does not cap, such as `task.features` values, setting `options` and `ext` payloads, hold no transcript text either.
- [ ] Timestamps carry an explicit offset.
- [ ] Your own data sits in `ext` under a namespace you control, such as `com.example.`. See [Extensions](https://orchestrationcontextprotocol.io/specification/0.3/extensions).
- [ ] Documents validate in CI. See [Validate](https://orchestrationcontextprotocol.io/docs/develop/validate).

## Going further: the observation

The 0.3 observation fields make a run comparable with other runs: the [task](https://orchestrationcontextprotocol.io/specification/0.3/task), the [configuration](https://orchestrationcontextprotocol.io/specification/0.3/configuration) that ran with its content-derived id, the [signals](https://orchestrationcontextprotocol.io/specification/0.3/signals) the result was judged on, and the acceptance rule. They are optional. Fill `run.task` and `run.signals` when your tool knows the task and runs checks; fill `run.configuration` when your tool has a declared workflow, and compute its id with the [canonical algorithm](https://orchestrationcontextprotocol.io/specification/0.3/configuration#configuration-id).
