Skip to content

Composition & reuse

These blocks are how a frame stops being a single flat page: embed other frames, pull in reusable pieces, and shape the context that flows down the tree. They’re the structural glue behind pages & templates.

@block/page

The root of a frame’s page — a padded container with an optional title, and the place to set the page’s default time range. Most frames start here and nest everything else inside it.

{ "@block/page": { "title": "Dashboard", "padding": "lg", "block": { "@block/text": "Body" } } }

@block/frame

Embeds another frame by id. Without a child it drops the referenced frame’s whole page in place — compose a landing page out of existing dashboards. With a child, you borrow that frame’s view and render your own blocks against it — reuse a semantic layer without copying its queries. Ids can carry {Param} placeholders filled from params.

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

params values are plain, decoded strings — a value containing a / or a space is escaped for you when the concrete id is built, so an operation named GET /api/products still addresses one path segment.

When you already hold the concrete id — the one in the URL, with its placeholders filled and its segments percent-encoded — reference it as instance_id instead. It is used verbatim, so a segment that already carries a %2F is not escaped a second time:

{
"@block/frame": {
"instance_id": "services/checkout/operations/oteldemo.CheckoutService%2FPlaceOrder"
}
}

Use id + params when authoring by hand, instance_id when copying an id out of the address bar (or when a tool hands you one, as “copy block” and a notebook’s frame fence do). The two are mutually exclusive.

@block/use

Instantiates a named block template — a piece of page you defined once and want to stamp out repeatedly with different params (a service row, a metric tile). Reach for it whenever you find yourself copy-pasting a block.

{ "@block/use": { "ref": "#/block/service_row", "params": { "service_name": "checkout" } } }

@block/context

Opens a scope that its descendants read from. Use it to declare shared state once (in extend) so several controls and queries below can bind to the same keys, to set a time range for just one subtree, or to inject another frame’s view. It’s the seam where you wire a section of the page together.

{
"@block/context": {
"extend": { "greeting": { "@expr/literal": "hello" } },
"block": { "@block/text": { "text": { "@expr/get_context": "greeting" } } }
}
}

The keys in extend resolve against the scope the same block opens, so a query there runs over the window that block’s timerange sets and against the frame its frame resolves — put them on one block rather than nesting a second @block/context.

{
"@block/context": {
"timerange": { "from": { "value": -7, "unit": "days" } },
"extend": { "rows": { "@expr/query": "SELECT count() FROM traces" } },
"block": { "@block/text": { "text": { "@expr/handlebars": "{{rows}}" } } }
}
}

@block/filter_context

Establishes a filter scope, optionally bound to a table. The filter controls (@block/filter_bar, @block/search, @block/field_filter) inside it write filters here, and the queries inside it read them — so a filter bar and a table become connected just by sharing a filter context.

{ "@block/filter_context": { "table": "traces", "block": { "@block/text": "scoped" } } }