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" } } }