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/nameresolves a template defined in thetemplatesmap 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/nameresolves a template from a dedicated*.templates.jsonbundle, 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/frameto 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 asblockor 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, afrombound and an optionalto(defaulting tonow). 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/frameinjects but without rendering that frame’s page. A bare string is shorthand for{ "id": … }; use the object form to passparams:{ "@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
- Blocks and expressions — what a page is built from, and what templates wrap.
- Views — the data half of a frame.
- Block reference — the
@block/use/@expr/useschema.