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:

kindTurns resolved from
executionthe sandbox run’s own event log
chatthe 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
QueryTypeDescription
kindstringexecution or chat.
statusstringe.g. running, completed, failed.
agentstringFilter by the agent slug that ran it.
apiKeynumberOnly runs attributed to this workspace API key.
sincestringInclusive lower bound on lastActivityAt. ISO-8601 or YYYY-MM-DD.
untilstringExclusive upper bound, same formats.
limitnumberDefault 50, clamped to 1..200.
cursorstringOpaque 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
QueryTypeDescription
afterSeqnumberOnly turns after this seq — the tailing cursor.
limitnumberMax 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.