events table. This guide covers the common shapes. For the concepts behind it, see
the query engine; for the column list, see the
event model; for the exact request/response schemas, see the
API reference.
Every query is a single SELECT over events. The gateway forces an organization predicate from your
verified identity, so you never write one — your SQL only ever sees your own rows. Only SELECT
runs; there’s a row cap and a timeout.
Annotations read per project. Run-scoped events span your organization, but annotation rows are
project-scoped on read: selecting a project — the --project flag, then HILOOP_PROJECT, then
the context’s project — returns only that project’s annotations, both run-scoped and run-less
(cross-run knowledge written with annotations add --project). The SQL itself can also select a project
in-band with a project_id = '<id>' (or project_id IN ('<id>', …)) filter, which surfaces that
project’s run-less annotations. Without any selection the query runs over run-scoped data and
returns no project-scoped annotations.
The smallest query
Return the model calls in a run:POST /v1/telemetry/sql taking {"sql": "<SELECT …>"}, plus
an optional "project_id" naming the project scope (the CLI fills it from your selected project),
and returning {"rows": [ … ], "columns": [ … ]} — a list of plain JSON object rows with null
columns omitted per row and 64-bit integer columns encoded as decimal strings, plus the declared
column names in projection order (so a column that is NULL in every row is still visible). From
the SDKs, POST to it directly; the typed view services (data views)
wrap the saved-query path. The CLI’s pragmatic scoping flags (--run-id, --signal, --limit,
--since, --until) build a SELECT over a compact default column
set — event id, time, signal, name, run identity, principal, and payload size; pass
--fields <col,col,…> to choose columns or --fields '*' for every column. For anything richer,
pass the SQL yourself with --sql (inline, @file, or - for stdin):
Filter, group, and aggregate
It’s just SQL —WHERE, GROUP BY, aggregates (COUNT, SUM, AVG, MIN, MAX,
approx_percentile_cont), and ORDER BY all work. Count model calls grouped by lineage path:
Scope to one lineage path
Every event carries thelineage_path of the run that produced it, and logical child runs share
their parent’s path as a prefix. A subtree is therefore an ordinary prefix predicate on that
column — the path itself, plus everything below it:
run_id predicate to span the whole subtree rather than one run’s slice of it. Logical
lineage does not imply runtime filesystem fork.
Compare two runs
To ask what one run did that another did not, use an anti-join on signal, name, and attributes. The events unique to run A relative to run B:AND a.signal = 'llm' to compare one
signal. To compare logical subtrees, replace each run_id equality with the lineage_path prefix
match from Scope to one lineage path.
Query custom attributes
Arbitrary attributes an agent emits live inattributes_json. Reach into them with
hiloop_json_get(attributes_json, '$.path.to.key'):
Filter failed requests
Ask the platform what happened
Work in a sandbox also recordsruntime lifecycle events — platform
metadata that flows independently of workload capture. “Why did my sandbox take so long to start”
is a query over operation.started’s queue_wait_ms:
exec.start/exec.end pair a buffered
sandbox execution records in its ambient run — every exec.start gets exactly one
exec.end, so a failed command never reads as still-running:
Tokens per model
Model, token counts, and message content live in the captured request/response bodies, exactly as they crossed the wire. Rather than re-parsing provider JSON in every query, create theotel_genai_calls data view once — one OpenTelemetry-GenAI-shaped row
per LLM exchange, derived entirely from the raw events — and query it like a table:
GROUP BY over it:
WHERE run_id = '…' to scope it to one run. The token counts come from each provider
response’s own usage block — streamed responses are reassembled at query time — so they match what
the provider actually reported. hiloop stores what crossed the wire and providers report tokens,
not prices; to turn token sums into dollars, multiply by your own rates in the SELECT.
The same derivation functions (payload_text, hiloop_sse_reassemble, hiloop_json_get, …) are
available in any query or view, so anything else in a captured body — messages, stop reasons, tool
definitions — is one expression away. See
deriving views from captured payloads.
Output
The CLI prints a table by default; pass--output json for the raw response body. Wide values are
truncated in table mode (tune with --max-cell-width, or 0 to disable); JSON is always full.
Next
- Save a query you run often as a reusable data view.
- Compare two runs with the SQL recipe above.
- Run the same queries from your code with the SDKs.