Runtimes
Custom runtime ingest API#
Push events from any runtime into ClawMetry without writing to a supported runtime's filesystem layout. Designed for in-house agents, eval harnesses and web agents that already produce structured run and step records.
Tier: Pro (entitlement key custom_runtime_ingest).
Endpoints#
| Method | Path | Description |
|---|---|---|
GET | /api/v1/runtimes | List runtimes ClawMetry knows about. Free. |
POST | /api/v1/runs | Open a run; returns a run_id. |
POST | /api/v1/runs/<id>/events | Append one event or many. |
POST | /api/v1/runs/<id>/end | Mark the run ended. Optional. |
GET | /api/v1/runs/<id> | Read back: was the run persisted? |
Authentication#
Two modes, selected by whether a token is configured:
- Localhost only (default). With
CLAWMETRY_INGEST_TOKENunset, only
loopback requests are accepted. Zero configuration, local only.
- Token header. Set
CLAWMETRY_INGEST_TOKEN=<secret>on the dashboard;
clients then send X-ClawMetry-Token: <secret>. The comparison is
constant-time.
A non-loopback request without a matching token gets 401 unauthorized.
Quickstart#
# 1. Open a run
curl -s http://localhost:8900/api/v1/runs \
-H 'content-type: application/json' \
-d '{"runtime": "my_engine", "metadata": {"build": "abc123"}}'
# -> {"ok": true, "run_id": "run_a1b2c3d4…", "runtime": "my_engine"}
# 2. Push an event
curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4/events \
-H 'content-type: application/json' \
-d '{"event": {
"id": "evt_1",
"ts": '"$(date +%s)"',
"event_type": "model.completed",
"model": "claude-sonnet-5",
"data": {"input_tokens": 1240, "output_tokens": 312}
}}'
# -> {"ok": true, "accepted": 1, "ids": ["evt_1"]}
# 3. Close it
curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4/end \
-H 'content-type: application/json' -d '{}'Event payload#
| Field | Required | Notes |
|---|---|---|
id | no | Dedupe key. The server fills evt_<hex> if absent. Re-ingesting the same id is a no-op. |
ts | no | Epoch seconds (float). Server uses now if absent. |
event_type | no | Free-form. Conventional: session.started, prompt.submitted, model.completed, tool.invoked, tool.result, error. |
session_id | no | Defaults to the run_id. |
tool_name | no | When the event represents a tool call. |
model | no | LLM model id — this is what makes cost derivable. |
role | no | user, assistant, system, tool. |
data | no | Opaque dict. The daemon adds data.extra.runtime. |
Token counts belong in data as input_tokens, output_tokens and, where
you have them, cache_read_tokens / cache_write_tokens. Supplying model
alongside them is what lets ClawMetry derive cost — without a model id you get
tokens and an honest unknown for dollars.
Batching#
{"events": [
{"id": "evt_1", "event_type": "prompt.submitted", "role": "user"},
{"id": "evt_2", "event_type": "tool.invoked", "tool_name": "search"},
{"id": "evt_3", "event_type": "model.completed", "model": "gpt-5.4",
"data": {"input_tokens": 900, "output_tokens": 120}}
]}Idempotency#
Event ids are the dedupe key, and re-posting an id already stored is a no-op rather than an error. This is deliberate: a harness that retries a failed push, or replays a run from a log, must not double-count. Generate stable ids from something in your own domain — a step index, a request id — rather than a random value per attempt.
A worked example#
import os, time, uuid, requests
BASE = os.environ.get("CLAWMETRY_URL", "http://localhost:8900")
HDRS = {"content-type": "application/json"}
if os.environ.get("CLAWMETRY_INGEST_TOKEN"):
HDRS["X-ClawMetry-Token"] = os.environ["CLAWMETRY_INGEST_TOKEN"]
run = requests.post(f"{BASE}/api/v1/runs", json={
"runtime": "research_agent",
"metadata": {"repo": "acme/api", "trigger": "nightly"},
}, headers=HDRS).json()
rid = run["run_id"]
def emit(step, **event):
event.setdefault("id", f"{rid}-{step}")
event.setdefault("ts", time.time())
requests.post(f"{BASE}/api/v1/runs/{rid}/events",
json={"event": event}, headers=HDRS)
emit(0, event_type="prompt.submitted", role="user",
data={"text": "summarise last night's failures"})
emit(1, event_type="tool.invoked", tool_name="query_ci",
data={"arguments": {"since": "24h"}})
emit(2, event_type="model.completed", model="claude-sonnet-5", role="assistant",
data={"input_tokens": 4210, "output_tokens": 380})
requests.post(f"{BASE}/api/v1/runs/{rid}/end", json={}, headers=HDRS)Those events land in the same store as every other runtime, appear in Conversations and Cost, and are visible to the MCP server — so your coding agent can ask about your production agent's night.
Reading it back#
curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4 | jq
curl -s 'http://localhost:8900/api/local/events?session_id=run_a1b2c3d4' | jq '.rows'If a run reads back but nothing appears in the dashboard, check that the daemon is running — the ingest endpoint accepts the write, but the rollups the UI reads are built by the daemon.