Skip to content

Pages & templates

The page is the visual half of a frame: a tree of blocks, fed by expressions, rendered over the frame’s view. Templates are how you avoid repeating yourself — reusable blocks and expressions you instantiate by name, inside one page or across many.

Pages

A page is a block tree rooted at @block/page. Layout blocks (@block/stack, @block/grid, @block/box, @block/tabs) arrange the visualization and input blocks inside them, and the whole tree is wrapped automatically so page-wide behaviour — a shared time range, a synchronized crosshair, active filters — reaches every block without per-block wiring.

Because blocks query the frame’s view by name, the page stays in step with its data model: change the view’s scoping or add a metric, and the blocks that reference it follow. A single frame can also carry more than its main page — standalone pages present extra views over the same frame’s data.

Templates

When the same piece of a page shows up more than once — a status badge, a formatted label, a small chart — you don’t copy it. You define it once as a template and instantiate it wherever you need it with @block/use (for blocks) or @expr/use (for expressions).

Defining a template

Templates live under a templates map, split by kind into blocks and expressions. Names only need to be unique within their kind, so a block and an expression may share a name. There are two forms:

  • Reference — a plain alias to a fixed block or expression, instantiated as-is each time:

    {
    "templates": {
    "blocks": {
    "ok_badge": { "reference": { "@block/badge": { "title": "OK" } } }
    }
    }
    }
  • Partial — a parameterized template. It declares a JSON Schema for the parameters it accepts, and the template reads them from context:

    {
    "templates": {
    "blocks": {
    "greeting": {
    "partial": {
    "parameters": {
    "type": "object",
    "properties": { "name": { "type": "string" } },
    "required": ["name"]
    },
    "template": { "@block/text": { "text": { "@expr/get_context": "name" } } }
    }
    }
    }
    }
    }

Using a template

Reference a template by name with @block/use (or @expr/use). A bare string is the shorthand for a no-parameter reference; the full form passes params:

{ "@block/use": "#/block/ok_badge" }
{ "@block/use": { "ref": "#/block/greeting", "params": { "name": "checkout" } } }

For a partial, the params are validated against the template’s schema, then bound into the template’s context — which is why the template above reads name with @expr/get_context. Instantiating a template is, in effect, wrapping it in a @block/context that supplies those parameters.

Where templates live

The ref string says where to look:

  • Inline#/block/name resolves a template defined in the templates map of the file it’s written in. In a frame that’s the frame’s own map; inside a shared *.templates.json, a template body’s #/ refs resolve against that file’s own map, never against whichever frame happens to render it.

  • Shared file@frames/path/to/file.templates.json#/block/name resolves a template from a dedicated *.templates.json bundle, reusable across many frames.

  • Packs — integrations ship template bundles this way. The OpenTelemetry pack, for example, provides shared badge templates that any OTel dashboard can drop into a table column:

    { "@block/use": "@frames/@opentelemetry/badges.templates.json#/block/severity_badge" }

Templates vs. embedding a frame

Templates and @block/frame both reuse authored content, but at different scales:

  • Reach for a template (@block/use / @expr/use) to reuse a UI component — a badge, a label, a chart — without re-declaring it.
  • Reach for @block/frame to embed a whole frame, borrowing its view and data model, not just a snippet of page. Its child renders against that frame; supply it either as block or inline as a @block/… key. With no child, the referenced frame’s own page is transcluded.

Seeding context: time ranges and borrowed views

@block/context creates a scope for everything beneath it. Besides the extend map (named bindings descendants read via @expr/get_context), it takes two shortcuts:

  • timerange — seed the time window for the subtree, a from bound and an optional to (defaulting to now). Descendant queries pick it up automatically:

    {
    "@block/context": {
    "timerange": { "from": "now-7d" },
    "block": { "@block/use": "#/block/error_chart" }
    }
    }
  • frame — borrow another frame’s view for the subtree (its _frameRuntime, params, and filter scope), the same binding @block/frame injects but without rendering that frame’s page. A bare string is shorthand for { "id": … }; use the object form to pass params:

    { "@block/context": { "frame": "services", "@block/table": { } } }
    { "@block/context": { "frame": { "id": "services/{ServiceName}", "params": { "ServiceName": "checkout" } }, "@block/table": { } } }

A bound (from / to) is either a relative offset object ({ "value": -15, "unit": "minutes" }), date math (now, now-2d, now-1d/d — offsets plus an optional /unit rounding), or an ISO-8601 timestamp (2026-05-30T00:00:00Z). The same bound syntax applies wherever a time window is authored (e.g. a frame’s settings.default_timerange).

Next