Authoring Frames
A route models a thing you observe (a service, an endpoint, a namespace), and you author it as two files sitting side by side:
- a frame,
*.frame.json— the view: what the data means, which tables it reads, and the named columns and metrics built on top of them; - a page,
*.page.json— how to show it: a tree of blocks, fed by expressions, rendered over that view.
The view holds the meaning, defined once; the page holds the presentation. Because blocks query the view by name, the two stay in step — and the current time range, active filters, and the frame’s parameters flow into every query without manual wiring. A frame with no page beside it renders nothing: it exists to be imported or borrowed.
services/index.frame.json builds its view on an installed base view rather than a raw table:
{ "view": { "imports": { "@opentelemetry/views/combined": {} }, "tables": {} }}services/index.page.json renders one block that asks that view for a value:
{ "title": "Services", "page": { "@block/stat": { "title": "Active services", "value": { "@expr/query": "SELECT uniqExact(ServiceName) FROM traces" } } }}From here, each piece gets its own depth:
In this section
- Views — the semantic layer: tables, scoping, scalars and metrics, and parameters that scope a frame to one entity.
- Blocks — the
@block/*components a page is built from, organized by what you reach for them to do. - Expressions — the
@expr/*values that feed blocks: queries, shared state, and transforms. - Pages & templates — composing blocks into a page, and packaging reusable pieces you instantiate by name.
For the exact file format — every field of the view, page, and parameters — see the frame schema reference.