> ## 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.

# Data views

> Save a telemetry query as a reusable, named view.

A data view is a named, reusable telemetry query you save once and run by name. It resolves to safe
SQL the gateway runs under your forced-organization predicate, so a view is organization-agnostic and only ever
reads your own rows. A data view (`/v1/telemetry/data-views`) is just a saved SQL `SELECT`.

Views are per-organization, run by name, and **re-validated on every run** — never cached as a compiled
plan. A view that references a column that no longer exists fails closed with an `invalid_argument`
error rather than returning a stale or wrong result.

There is no point-and-click query builder — you author the SQL with the API, CLI, or SDK shown below.

## Data views

Create or replace a data view by name (upsert). The stored SQL is an organization-agnostic `SELECT` — the
gateway AND-s your organization predicate in at execution. It's validated when you store it and again on
every run. `--sql` takes the SQL inline, from a file (`@view.sql`), or from stdin (`-`):

<CodeGroup>
  ```sh CLI theme={null}
  hiloop data-views create calls_by_branch \
    --description "Model calls per branch" \
    --sql "SELECT lineage_path, COUNT(*) AS calls FROM events WHERE signal = 'llm' GROUP BY lineage_path ORDER BY calls DESC"
  ```

  ```typescript TypeScript theme={null}
  import { telemetryViewServicePutDataView } from "@hiloopai/sdk";

  await telemetryViewServicePutDataView({
    client,
    path: { name: "calls_by_branch" },
    body: {
      description: "Model calls per branch",
      spec: {
        sql: "SELECT lineage_path, COUNT(*) AS calls FROM events WHERE signal = 'llm' GROUP BY lineage_path ORDER BY calls DESC",
      },
    },
  });
  ```
</CodeGroup>

Because the stored SQL is organization-agnostic and carries no `run_id` filter, the view is reusable across
every run — add a `WHERE run_id = '…'` if you want it pinned to one.

A view resolves as a **table**, so you read it back with an ordinary query:

```sh theme={null}
hiloop query --sql "SELECT * FROM calls_by_branch LIMIT 10"
```

(Views named with `-` or other non-identifier characters are still runnable by name via
`POST /v1/telemetry/data-views/{name}:run`.)

`hiloop data-views list` shows your views; `hiloop data-views delete <name>` removes one. Every
view is yours — there are no built-in views, only recipes you create (below).

## Derive views from captured payloads

Captured request/response **bodies** are stored out-of-line in a content-addressed blob store; an
event carries only a digest. A set of derivation functions lets a view reach the content and parse
it — entirely at query time, from the raw events alone:

| Function                                                                   | What it does                                                                                                                                |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `payload_text(digest)`                                                     | The payload body referenced by an event's `payload_digest`, as text                                                                         |
| `hiloop_sse_reassemble(body)`                                              | Reassembles a captured streamed (`text/event-stream`) LLM response into the provider's JSON document; returns non-streamed bodies unchanged |
| `hiloop_genai_input_messages(body)` / `hiloop_genai_output_messages(body)` | Provider request/response messages normalized to the OpenTelemetry GenAI message shape                                                      |
| `hiloop_json_get(body, path)`                                              | A scalar leaf at a dot path, as text                                                                                                        |
| `hiloop_json_get_json(body, path)`                                         | Any subtree at a dot path, as JSON text                                                                                                     |

The example recipes — `otel_genai_calls` (one OTel-GenAI-shaped row per LLM exchange),
`shell_transcript`, `metric_series`, `agent_tree`, `wandb_metrics` — are ordinary views you create
this way; nothing about them is built in. For instance:

```sh theme={null}
hiloop data-views create otel_genai_calls --sql @otel_genai_calls.sql
hiloop query --sql "SELECT gen_ai_request_model, input_tokens, output_tokens, duration_ms FROM otel_genai_calls WHERE run_id = '01K6Z…'"
```

## How saved SQL stays safe

A view's SQL runs through the same per-organization gateway as an [ad-hoc query](/observability/query-telemetry),
in a locked-down session:

* It sees only the `events` table and your own SQL views — no `information_schema`, only an
  allowlist of scalar functions.
* Every scan of `events` is automatically constrained to your organization from the request's verified
  identity — you can't widen it.
* DDL, DML, and multi-statement SQL are rejected; the planner is verified before execution.
* Each query gets a fresh session, an injected `LIMIT`, a memory ceiling, and a hard timeout.

To run a one-off query without saving it, `POST /v1/telemetry/sql` — the same safe path, just not
stored.

## Next

* [Query telemetry](/observability/query-telemetry) — the ad-hoc SQL a data view saves.
* [Annotations](/observability/annotations) — save a query over your eval annotations as a view.
