> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiloop.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# The query engine

> How hiloop makes agent telemetry queryable through constrained SQL.

Capturing telemetry is half the job. The other half is getting answers out of it. hiloop runs a
query engine over the [event model](/concepts/event-model) so you can interrogate a thousand
parallel experiments the way you'd interrogate a database — because that's exactly what you do: you
write **SQL** against a single `events` table.

The engine reads across recent events held in memory and historical events on object storage, and
presents them as one queryable surface. You don't manage that split; you ask a question and get
rows back.

## One table, plain SQL

Every signal — a model call, a tool invocation, a line of stdout, an OpenTelemetry span, an
[annotation](/observability/annotations) — lands in the same denormalized `events` table. There are
no joins to get right and no bespoke query language to learn. You `SELECT` over the
[canonical event columns](/concepts/event-model) and the engine returns rows.

```sql theme={null}
SELECT signal, COUNT(*) AS n
FROM events
GROUP BY signal
ORDER BY n DESC
```

The addressable columns are exactly the canonical event schema (`run_id`, `lineage_path`, `signal`,
`name`, `ts_wall_ns`, `http_status_code`, `attributes_json`, `payload_digest`, …). An unknown
column is rejected before the query runs, with an error that names the offending identifier and
lists what's available. Model-shaped fields (model, token counts, messages) are not columns — they
derive at query time from the captured attributes and payload bodies.

## The query gateway keeps SQL safe

You send raw SQL, but you never reach another organization's data or escape into the database. Every query
runs through a per-organization gateway that:

* exposes only the `events` table and your own SQL data views — no `information_schema`, and only an
  allowlist of scalar functions;
* automatically constrains every scan of `events` to your own organization, derived from the request's
  verified identity — you can't widen it, so the SQL you write is organization-agnostic;
* rejects DDL, DML, and multi-statement SQL — only a single `SELECT` runs;
* caps each query with an injected row limit, a memory ceiling, and a hard timeout.

So a query is both expressive (anything `SELECT` can say) and contained (it can only ever read your
own rows).

## Ad-hoc queries

Send a `SELECT` to `POST /v1/telemetry/sql`:

```sh theme={null}
hiloop api /v1/telemetry/sql -X post -d '{
  "sql": "SELECT signal, COUNT(*) AS n FROM events GROUP BY signal ORDER BY n DESC"
}'
```

The response is a list of plain JSON object rows — `null` columns omitted, and 64-bit integer
columns (counts, `ts_wall_ns`) encoded as decimal strings so values above 2^53 never lose
precision:

```json theme={null}
{ "rows": [ { "signal": "llm", "n": "412" }, { "signal": "tool", "n": "1180" } ] }
```

The CLI wraps this with `hiloop query`, and the SDKs call it from your code. See the
[querying guide](/guides/querying-telemetry) for runnable CLI and SDK examples.

## Querying custom attributes

Arbitrary attributes an agent emits land in `attributes_json`. Reach into them with the
`hiloop_json_get(attributes_json, '$.path.to.key')` function — it walks a dot-separated path (the
leading `$.` is optional) and returns the addressed value as text:

```sql theme={null}
SELECT hiloop_json_get(attributes_json, '$.gen_ai.request.temperature') AS temperature,
       COUNT(*) AS n
FROM events
WHERE signal = 'llm'
GROUP BY temperature
```

## Derive, don't materialize

Everything model-shaped derives from the raw capture at query time. Payload bodies resolve from the
content-addressed store with `payload_text`, streamed responses reassemble with
`hiloop_sse_reassemble`, and provider JSON parses with the JSON-path functions — so a
[data view](/observability/data-views) like `otel_genai_calls` turns raw exchanges into
OpenTelemetry-GenAI-shaped rows with nothing precomputed at ingest. The standard token curves a
dashboard needs are a `GROUP BY` over that view — plain SQL, no special endpoint.

The public query surface is constrained SQL. Use the generated [API reference](/api-reference) for
exact schemas.
