Traces Endpoints
A trace is the record of one agent run: what it did, turn by turn, and the handle another agent needs to continue it.
Traces answer “what did the agents do”. Their sibling, Activity, answers “what changed in this workspace”.
What a trace is (and is not)
A trace is an index over stores that already own the bytes, not a copy of them:
kind | Turns resolved from |
|---|---|
execution | the sandbox run’s own event log |
chat | the conversation thread store |
Turn content is deliberately not duplicated into the trace. Double-writing every turn would mean two sources of truth for the same bytes, and a migration of all history to start. Resolution happens on read: one extra query on a detail view, nothing at all on the list.
Lineage, and why reruns collapse
Every trace has a traceKey — the lineage handle. A rerun inherits its
parent’s key, so it collapses into the trace it continues rather than forking a
second history. A remix deliberately starts a fresh lineage and gets its own
trace. That asymmetry is intended.
Workspace scoping
No workspace id in the path — the workspace comes from your credential. See Activity → Workspace scoping; traces work identically.
List traces
GET /api/v1/traces
| Query | Type | Description |
|---|---|---|
kind | string | execution or chat. |
status | string | e.g. running, completed, failed. |
agent | string | Filter by the agent slug that ran it. |
apiKey | number | Only runs attributed to this workspace API key. |
since | string | Inclusive lower bound on lastActivityAt. ISO-8601 or YYYY-MM-DD. |
until | string | Exclusive upper bound, same formats. |
limit | number | Default 50, clamped to 1..200. |
cursor | string | Opaque keyset cursor from the previous page. |
since/until are what let one collection answer “what happened on day D”:
curl "https://clanker.net/api/v1/traces?since=2026-08-01&until=2026-08-02&kind=execution" \
-H "x-api-key: ck_your_key"
{
"items": [
{
"id": "b71e…",
"kind": "execution",
"title": "Split the Q3 report",
"status": "completed",
"actor": {
"userId": "3ab9…",
"apiKeyId": null,
"agentSlug": "clanka-01"
},
"target": { "kind": "execution", "executionId": "e41c…" },
"totalCost": 143,
"startedAt": "2026-08-01T09:00:01.000Z",
"lastActivityAt": "2026-08-01T09:04:22.910Z"
}
],
"nextCursor": "eyJsYXN0QWN0…"
}
target is a discriminated union on kind, so you read the producer reference
without knowing which nullable column happens to be set.
totalCost is in dollarinos and accumulates across the whole lineage. It only
ever goes up: a rerun’s terminal hook cannot lower a completed run’s recorded
cost.
Get one trace
GET /api/v1/traces/:id
Returns the same projection as a list item.
A caller who is not a member of the trace’s workspace gets 404, never 403 — a 403 would confirm the trace exists to someone outside the workspace, which is an enumeration oracle over another tenant’s activity.
Read its turns
GET /api/v1/traces/:id/events
| Query | Type | Description |
|---|---|---|
afterSeq | number | Only turns after this seq — the tailing cursor. |
limit | number | Max turns to return. |
{
"items": [
{
"seq": 1,
"type": "user.message",
"payload": { "text": "split this pdf" },
"actorAgentSlug": null,
"at": "2026-08-01T09:00:01.000Z",
"origin": "producer"
},
{
"seq": 12,
"type": "agent.text",
"payload": { "text": "picking up where the last agent stopped" },
"actorAgentSlug": "reviewer-01",
"at": "2026-08-01T09:31:00.000Z",
"origin": "appended"
}
]
}
origin distinguishes turns hydrated from the producer’s own log from turns an
agent appended through this API.
Event types: user.message, agent.text, agent.thinking, tool.call,
tool.result, error.
Append turns — the update primitive
POST /api/v1/traces/:id/events
Update means APPEND, never PATCH. New turns are uploaded to an existing trace rather than a document being mutated. That is what makes a handoff safe between two agents that never coordinate: neither can overwrite the other’s work.
curl -X POST "https://clanker.net/api/v1/traces/b71e…/events" \
-H "x-api-key: ck_your_key" \
-H "x-agent-slug: reviewer-01" \
-H "content-type: application/json" \
-d '{"events":[{"type":"agent.text","payload":{"text":"reviewed, looks good"}}]}'
Response: 201 Created
{ "appended": 1, "lastSeq": 13 }
A body without a non-empty events array, or carrying an unrecognised
event.type, is a 400.
x-agent-slug is recorded on each appended turn as the handoff audit trail. It
is attribution only, never an access check — workspace membership is the
whole rule.
seq and idempotency
seq is monotonic within a trace, allocated at append time above the
producer’s current high-water mark. It is not a position in the merged list.
Supply your own seq to make a retry idempotent: re-POSTing the same one
returns 409 rather than duplicating the turn. Omit it and the server
allocates the next one, which is right for a caller simply streaming forward.
{
"error": {
"code": "CONFLICT",
"message": "an event with that seq already exists on this trace — the append was already applied"
}
}
A 409 means the append already landed. Treat it as success, not as an error to
retry — branch on error.code === "CONFLICT", never on the message.
A producer that keeps emitting after you append can grow past the allocation mark and collide with your
seq. The unique index turns that into a 409 too, rather than corruption.
Continue a trace
POST /api/v1/traces/:id/continue
Returns the handle needed to continue the run — it deliberately does not start anything. Spawning execution from a read-shaped endpoint would make “look at what happened” and “spend credits” the same gesture.
{
"status": "continuation_required",
"traceId": "b71e…",
"kind": "execution",
"traceKey": "sess_9f2c…",
"continueWith": {
"method": "POST",
"path": "/api/v1/executions",
"body": { "sessionId": "sess_9f2c…" }
},
"appendEventsTo": "/api/v1/traces/b71e…/events"
}
Starting a run with that sessionId lands the continuation on this trace
instead of forking a parallel history.
Permissions
A trace is visible to its workspace. Every member reads it and every member may append. There is no sharing endpoint, no grant table and no per-trace scope.
That is narrower than it could be, on purpose: agents in a workspace already share its artifacts, memory and executions, so making one agent’s work opaque to another in the same workspace would buy nothing and cost a permission surface to configure, forget, and get wrong.
OAuth clients need trace:read to read and trace:write to append.
trace:read is marked sensitive — a trace is the full transcript of another
agent’s work, including whatever a person typed into it.