Blocks
The page is a tree of blocks. Layout blocks arrange the page; visualization
blocks query the view and render it. Every block is a single-key JSON
object: { "@block/<type>": <props> }. Many accept a shorthand (a bare string,
array, or child block) in place of the full props object.
The page is always wrapped in an @block/page. You can author the wrapper
explicitly, or just provide its content and let Noemata wrap it:
"page": { "@block/text": "Hello" }This page covers the core blocks. The installed frames under
@frames/@opentelemetry/ exercise the full set; .data/schemas/blocks.json
lists every block and field.
Layout
@block/grid— placed cells. Give itcolsand a list ofitems; each item setsx/y/cols/rowsand holds one child. Good for dashboards.@block/column/@block/row— stack children vertically / lay them out horizontally. Both accept the box options (width/height/padding) and the flex options (gap/alignItems).@block/rowaccepts a bare array shorthand.@block/stack— flex container withdirectionplus the same box and flex options. Use when you need explicit sizing or alignment.@block/box— single-child container for the box options (width/height/padding).@block/panel— a titled card.title, optionaldescription, one child, plus box options and agapthat separates the header from the body. The standard wrapper around a visualization.
{ "@block/grid": { "cols": 4, "default_rows": 1, "items": [ { "x": 0, "y": 0, "cols": 2, "rows": 2, "@block/panel": { "title": "Latency", "@block/plot": { "marks": [{ "line": { "y": "DurationP95" } }] } } } ] }}Plot
@block/plot renders charts from marks — there is no @block/line or
@block/bar. Each mark is a single-key object naming the geometry; its channels
(x, y, stroke, fill, …) map to view columns by name.
{ "@block/plot": { "marks": [{ "line": { "x": "t", "y": "DurationP95", "stroke": "ServiceName" } }] }}Common marks: line, areaY, barY / barX, dot, ruleY. Pass several to
overlay them ("marks": [{ "areaY": {…} }, { "line": {…} }]). Axis and color
scales are configured with x, y, and color props on the plot.
Plotting over time
A plot with no from queries the frame’s view automatically, bucketed by the
table’s timestamp over the active time range. So a metric
defined in the view renders as a time series just by naming it:
{ "@block/plot": { "marks": [{ "line": { "y": "DurationP95" } }] } }For an explicit time-bucketed query, bind the plot’s from to an
@expr/timeseries_query (see Querying data).
Table
@block/table renders rows. Bind from to a data expression and list columns
as { "title": …, "column": … } (omit columns to show every field):
{ "@block/table": { "from": { "@expr/query": { "from": "otel_traces", "select": ["ServiceName", { "Requests": "count()" }, { "ErrorRate": "ErrorRate" }], "group_by": ["ServiceName"], "order_by": { "Requests": "desc" }, "limit": 20 } }, "columns": [ { "title": "Service", "column": "ServiceName" }, { "title": "Requests", "column": "Requests" }, { "title": "Error rate", "column": "ErrorRate" } ] }}Stat
@block/stat is a KPI card: a title and a scalar value, with optional
format, a sparkline series, and a baseline for delta-vs-previous.
{ "@block/stat": { "title": "Request rate", "format": { "rate": "seconds" }, "value": { "@expr/query": { "from": "otel_traces", "select": [{ "Rps": "count()" }], "where": "IsEntrySpan" } } }}Text
@block/text— plain text. Shorthand: a bare string. Object form addssizeandcolor(muted,error, …).@block/md— renders a Markdown string to HTML.
Querying data
Data-bearing blocks take a from that is a data expression. The structured
@expr/query builds SQL declaratively against the view:
{ "@expr/query": { "from": "otel_traces", "select": ["ServiceName", { "Requests": "count()" }], "where": "IsEntrySpan", "group_by": ["ServiceName"], "order_by": { "Requests": "desc" }, "limit": 10 }}select items are bare column names or { alias: expression } objects, where the
expression may reference any view scalar by name. Use @expr/timeseries_query
(same shape) when you want one row per time bucket over the active range.
To correlate two relations, give from an as alias and add a join. The join is
a top-level key named for its kind — inner_join, left_join, right_join,
full_join, or cross_join — so the query reads like SQL. It carries its own
relation from (a table or a nested query) with an optional as, and a condition
that is either on (a predicate) or using (shared column names); a cross_join
takes no condition. A query may carry at most one join key (as is only valid
alongside one); for more, nest a query in from or drop to raw SQL.
{ "@expr/query": { "from": "otel_traces", "as": "t", "left_join": { "from": "service_owners", "as": "o", "on": "t.ServiceName = o.ServiceName" }, "select": ["t.ServiceName", { "Owner": "o.Team" }] }}For a per-second rate, divide a count() (or sum()) by _time_grain — the
time each value covers, in milliseconds, e.g.
{ "Rps": "count() / nullif(_time_grain / 1000.0, 0)" }. _time_grain resolves to
the bucket width inside a timeseries and to the whole window in a scalar query, so
the same expression reads correctly as both a per-bucket chart series and a
window-mean stat card — the headline equals the mean of its own chart by
construction. The bundled Throughput / Rate / HistRate view scalars are
defined this way. (Formatting a bare count() as { "rate": "seconds" } without
dividing reports the raw count as a per-second rate — wrong by the window’s
length.)
Two fixed denominators are injected alongside it for when you need one regardless
of context: _time_interval (the bucket width — only in a timeseries) and
_time_range (the selected window’s total duration).
Sharing a query across blocks
@block/context binds named values that descendant blocks read by name, so
several blocks share one query instead of each re-running it:
{ "@block/context": { "extend": { "totals": { "@expr/query": { "from": "otel_traces", "select": ["Throughput", "DurationP95", "EntryErrorRate"], "where": "IsEntrySpan" } } }, "@block/row": [ { "@block/stat": { "title": "Throughput", "value": { "@expr/get_context": "totals.Throughput" } } }, { "@block/stat": { "title": "p95", "value": { "@expr/get_context": "totals.DurationP95" } } } ] }}Controls
Pages can carry interactive filters that all queries on the page respect:
@block/field_filter— a dropdown that filters by a view column ({ "field": "DeploymentEnvironment", "placeholder": "Environment" }).@block/query_bar— a free-text query bar.
See the installed @frames/@opentelemetry/services frame for these in context,
then continue to the Cookbook.