--- title: Query API description: The q/1 query contract — thirteen live shapes with their arguments, limits and trust classes, plus the three ways to call them: HTTP, the daemon proxy, and MCP. keywords: agent telemetry query API, clawmetry local API, DuckDB query shapes, agent data API eyebrow: Data & storage --- # Query API The read surface is a declared contract called **`q/1`**. It is not a loose collection of endpoints: every shape, its arguments, its defaults and its limits are registered, and the HTTP API, the daemon proxy, the MCP server and the cloud relay all serve the same registry. That is why an agent asking over MCP and a script calling `curl` get identical rows. ## Evolution rule Inside `q/1`, evolution is **additive only**. New shapes and new optional arguments may appear. Renaming or removing a shape, an argument, or a response field requires a new contract version. ## Live shapes | Shape | Arguments | Returns | |---|---|---| | `sessions` | `agent_id`, `since`, `until`, `limit` (100, max 2000) | One row per session with start/end, event count, cost | | `events` | `session_id`, `agent_id`, `event_type`, `since`, `until`, `limit` (200, max 5000) | Raw event rows, newest first | | `transcript` | `session_id` **required**, `limit` (500, max 5000) | Events scoped to one session | | `aggregates` | `agent_id`, `since`, `until` | Per-day rollup of events, tokens and cost | | `health` | — | Store health: engine, size, ring depth, flush age | | `spans` | `trace_id`, `session_id`, `agent_type`, `since`, `until`, `limit` (200, max 2000) | OTel span rows | | `traces` | `session_id`, `agent_type`, `since`, `until`, `limit` (100, max 1000) | One row per trace with aggregate span stats | | `external_calls` | `session_id`, `since`, `until`, `limit` (200, max 2000) | Non-LLM API calls captured by the interceptor | | `models` | `runtime`, `since`, `until`, `limit` (1000, max 10000) | Per-model daily token and cost rollup | | `runtimes` | `since`, `until`, `limit` (1000, max 10000) | Per-runtime daily activity and cost rollup | | `rollup_sessions` | `runtime`, `limit` (200, max 2000) | Materialised per-session summary: title, status, totals, stuck flag | | `search` | `q` **required**, `model`, `status`, `since`, `until`, `limit` (50, max 500) | Full-text search over session titles and eval reasons | | `agent_graph` | `since`, `until`, `limit` (500, max 2000) | Spawn graph: nodes plus spawn edges | Arguments are **strictly coerced**: anything not in a shape's allowed-key set is dropped, and limits are clamped to their range rather than erroring. An agent asking for a million events gets the cap. ## Trust classes Every shape is classified, and the classification governs what may leave the machine in plaintext: | Class | Meaning | |---|---| | `plaintext` | Aggregate counters or metadata the server may see in cleartext | | `e2e` | Session and content-bearing payloads. These only ever leave the machine AES-256-GCM encrypted, and must never appear on a plaintext push list. | `aggregates`, `health`, `models`, `runtimes` and `agent_graph` are plaintext. Everything content-bearing — `sessions`, `events`, `transcript`, `spans`, `traces`, `external_calls`, `rollup_sessions`, `search` — is `e2e`. → [End-to-end encryption](/docs/cloud/encryption/) ## Calling it — HTTP Convenience routes: ```bash curl -s 'localhost:8900/api/local/sessions?limit=20' | jq '.rows' curl -s 'localhost:8900/api/local/events?event_type=error' | jq '.rows' curl -s 'localhost:8900/api/local/transcript/' | jq '.rows' curl -s localhost:8900/api/local/aggregates | jq '.rows' curl -s localhost:8900/api/local/health | jq curl -s 'localhost:8900/api/local/spans?session_id=' | jq '.rows' curl -s 'localhost:8900/api/local/traces?limit=20' | jq '.rows' curl -s 'localhost:8900/api/local/external-calls' | jq '.rows' curl -s 'localhost:8900/api/local/search?q=migration' | jq '.rows' curl -s 'localhost:8900/api/local/agent-graph' | jq ``` The generic form, which reaches every shape: ```bash curl -s localhost:8900/api/local/query -X POST \ -H 'content-type: application/json' \ -d '{"shape":"rollup_sessions","args":{"runtime":"claude_code","limit":50}}' | jq ``` An unknown shape returns `400` with the list of allowed shapes, which makes the API self-describing: ```bash curl -s localhost:8900/api/local/query -X POST \ -H 'content-type: application/json' -d '{"shape":"nope"}' | jq '.allowed_shapes' ``` ## Calling it — the daemon directly The dashboard is a convenience. The daemon serves the same contract: ```bash PORT=$(jq -r .port ~/.clawmetry/local_query.json) TOK=$(jq -r .token ~/.clawmetry/local_query.json) curl -s "http://127.0.0.1:$PORT/api/local/query" \ -H "Authorization: Bearer $TOK" \ -H 'content-type: application/json' \ -d '{"shape":"sessions","args":{"limit":5}}' | jq ``` This works with no dashboard running at all, which makes it the right path for a monitoring script. ## Calling it — MCP Five of these shapes are exposed as MCP tools so an agent can query them directly. → [MCP tool reference](/docs/mcp/tools/) ## Planned shapes The registry also declares shapes that are targets rather than served surfaces: `usage`, `session`, `brain`, `approvals` and `glance`. A planned shape is a declared intent; shipping one flips its registry entry, and a drift test enforces both directions so the documentation cannot claim a method the code does not serve. ## Practical recipes ```bash # yesterday's errors, grouped by tool curl -s 'localhost:8900/api/local/events?event_type=tool_result&limit=5000' \ | jq '[.rows[] | select(.data.is_error) | .tool_name] | group_by(.) | map({tool: .[0], n: length}) | sort_by(-.n)' # the ten most expensive sessions this month curl -s localhost:8900/api/local/query -X POST -H 'content-type: application/json' \ -d '{"shape":"rollup_sessions","args":{"limit":2000}}' \ | jq '[.rows[] | select(.cost_usd != null)] | sort_by(-.cost_usd) | .[:10] | map({title, cost_usd, runtime})' # per-runtime cost for the last 30 days curl -s localhost:8900/api/local/runtimes | jq \ '[.rows[]] | group_by(.runtime) | map({runtime: .[0].runtime, cost: (map(.cost_usd) | add)})' ```