Frame, view & page schema
A frame is the top-level authored document: a *.frame.json file that pairs a view (what the data means) with a page (how to display it). This page is the schema reference for the shape you write; for how to think in views and pages, see Views and Pages & templates.
Every authored field is snake_case. Editors autocomplete and validate frames from the generated JSON Schema at .data/schemas/frame.json (referenced by the $schema field the CLI writes into installed frames), so this page covers the shape — reach for editor autocomplete for the exhaustive set of nested fields.
Frame
Here is a whole, minimal frame — a view over a table plus a page that reads a metric from it by name:
{ "title": "Services", "view": { "tables": { "traces": { "table": "otel_traces", "timestamp": "Timestamp", "with": { "ErrorRate": "countIf(lower(StatusCode) = 'error') / nullif(count(), 0)" } } } }, "page": { "@block/stat": { "title": "Error rate", "value": { "@expr/query": "SELECT ErrorRate FROM traces" }, "format": "percent" } }}The top-level fields of a *.frame.json:
| Field | Required | Purpose |
|---|---|---|
title | yes | Display title. Supports Handlebars, e.g. "{{ServiceName}}", for parameterized frames. |
view | yes | The data half — tables, imports, and named expressions. See View. |
page | yes | The display half — a tree of blocks, or null for a data-only frame. See Page. |
params | no | Declared parameters for a parameterized frame. See Parameters. |
settings | no | How the route renders — today the default time window. See Settings. |
templates | no | Reusable blocks and expressions, split into blocks and expressions. See Pages & templates. |
$schema | no | Path to the generated frame JSON Schema; auto-written into installed frames. |
page is required as a key so the absence of a page is an explicit choice: null marks a data-only frame that exists to be embedded or borrowed by others and doesn’t appear in frame search.
Settings
settings holds how a route renders, as opposed to what it means (view) or what it accepts (params). Today it has one field:
| Field | Purpose |
|---|---|
default_timerange | The window the route opens at — { from, to? }, relative to “now”, so it rolls. |
{ "title": "OpenTelemetry", "settings": { "default_timerange": { "from": "now-24h" } }, "view": { "tables": {} }, "page": { "@block/text": "…" }}Settings are inherited down the route. A frame at @opentelemetry covers every route beneath it — @opentelemetry/services/checkout, @opentelemetry/traces/{TraceId}, and any *.page.json or *.md file that lives under them — so a pack declares its window once at the root. A deeper frame that declares its own wins for its own subtree, and a *.page.json may declare settings too (applied over the frame chain it borrows its view from). Merging is per key, nearest-wins.
default_timerange is only the initial value: it seeds the URL-synced from/to, so a link that already carries a window wins, the picker stays writable, and following a link from one frame to another keeps the window you were looking at. With nothing declared anywhere in the chain, the window is the last 15 minutes.
View
The view says where rows come from and what they mean. It has one required field, tables, plus optional imports, view-wide where, and with:
{ "view": { "tables": { "traces": { /* one table definition, keyed by its alias */ } }, "imports": { "@opentelemetry/views/combined": { /* one import, keyed by frame name */ } }, "where": "IsServerSpan", "with": { "ErrorRate": "ErrorCount / nullif(RequestCount, 0)" } }}tables— direct database tables, keyed by the CTE alias blocks reference (required; use{}when a frame only imports). See Table.imports— other frames’ views spliced in, keyed by frame name. See Import.where— a SQL predicate applied to every source, scoping the whole frame.with— named scalar expressions (derived columns and metrics) every block can select by name, applied across all sources.
There is no view-level select, group_by, or scalars — selecting and grouping happen in the blocks’ @expr/query; the view only names data. A scalar in with is either a bare SQL string or the object form { "expression", "title?", "format?" } when it needs presentation metadata.
Table
Each entry in tables is keyed by its alias — the handle blocks reference in their from. All fields are optional:
table— what this alias reads from, resolved alias-first: a name matching another alias in the view (this frame’s own or one an import contributed) derives from it, inheriting that alias’swhere,timestamp, and scalars and refining them ("api_requests": { "table": "events", "where": "…" }— see Derived tables); any other name is a physical database table ("traces": { "table": "otel_traces" }). Defaults to the alias key, but prefer a semantic alias distinct from the physical name — the shipped packs all use one, so a physical name likeotel_tracesalways means the raw table.where— a predicate for this source alone, combined with the view-levelwhereand, on a derived table, with every ancestor’s.with— scalars scoped to this source. On a derived table these are added to the inherited ones; a repeated name replaces the inherited definition for this alias only.timestamp— the column used for time bucketing. Inherited from the parent by a derived table.disable_auto_scope— settrueto opt this table out of automatic route-parameter scoping when it doesn’t carry the parameter’s column.
Alias keys must start with a letter or underscore and may contain letters, digits, _, -, and . (so system.parts is a legal defaulted alias).
Import
Each entry in imports is keyed by the imported frame’s name. An import builds on an installed semantic layer — { "imports": { "@opentelemetry/views/combined": {} }, "tables": {} } — inheriting that view’s scalars and scoping and AND-composing your where onto it. An import accepts:
tables— a list of the imported frame’s aliases to splice in; omit to import all. Derivations resolve before the filter applies, so selecting a derived alias without its parent still carries the parent’s scoping and scalars — the parent is simply not exposed.params— route parameters bound on the imported frame, keyed by column:{ "params": { "ServiceName": "checkout" } }.where/with— a predicate / scalars folded into every table spliced from this import.disable_auto_scope— propagate the route-parameter opt-out to every spliced table.
Page
The page is a tree of blocks, each node a single-key object like { "@block/stack": { … } }, typically rooted at @block/page. Any block is accepted: an @block/page renders as-is, and any other block is wrapped in @block/page automatically. page may also be null for a data-only frame.
{ "title": "Latency", "view": { "imports": { "@opentelemetry/views/traces": {} }, "tables": {} }, "page": { "@block/stat": { "title": "p95 latency", "value": { "@expr/query": "SELECT DurationP95 FROM traces" }, "format": "duration" } }}A block queries the view rather than the raw table, so the frame’s scope, parameters, and the page’s time range fold into every query automatically:
{ "@expr/query": { "from": "traces", "select": ["RequestCount", "ErrorRate"], "where": "IsServerSpan" }}Parameters
Parameters let one frame definition serve many entities — a single services/{ServiceName}/index.frame.json that renders for any service. A {ParamName} segment in the file path binds that parameter; you declare each one in the frame’s params:
{ "params": [{ "type": "string", "column": "ServiceName", "required": true }]}Each parameter names a column (the field it filters on, also its URL query key), a type ("string"), and optional required. Noemata automatically adds column = value to every query, so the whole view narrows to that entity. The value arrives from the URL or from a parent that embeds the frame.
Standalone views and pages
The same building blocks exist as standalone files:
*.view.json— a view with no page, referenced by other frames viaimports.*.page.json— a page on its own; it borrows the nearest frame’s view for its data model.
See also
- Views and Pages & templates — how to author the two halves.
- Authoring frames — the authoring guide.
- Frames — the concept behind the schema.