Combinators & templates
Combinators combine values from other values: chain a transform pipeline, pick a value by case, merge several reactive inputs, or build up an object. Reach for one when a single query or state key isn’t quite the shape a block wants. See the expression reference for the exact inputs.
@expr/pipeline
Runs a source expression through a sequence of transform operators, each receiving the previous result. Reach for it whenever you’d otherwise nest transforms inside transforms — it reads top-to-bottom instead of inside-out.
{ "@expr/pipeline": [ { "@expr/query": "SELECT StatusCode, Duration FROM spans" }, { "@expr/filter": { "where": "StatusCode > 0" } }, { "@expr/derive": { "duration_ms": "Duration / 1000000" } } ]}@expr/set_context in a pipeline
A pipeline step that publishes named entries into scope for the rest of the chain, passing the flow through unchanged. An operator-shaped entry (an @expr/filter, say) is applied to the incoming value; a complete expression evaluates on its own; { "@expr/pipeline": [] } captures the flow verbatim. Downstream steps read the names with @expr/get_context. Names may not start with _, which the runtime reserves for its own scope entries (_timerange, _filter), and a @expr/pipeline entry holds operators only (a standalone chain is written as its head expression with the operators nested in it). Pure scoping in every host, workflow steps included: nothing is persisted; durable cross-run state is @expr/workflow_state.
@expr/query as a pipeline step
A from-less structured query in step position runs SQL over the incoming value: the flow binds as the driving relation, so select/where/group_by read its columns, and a join key (full_join, left_join, …) joins another table against it — a view alias or an inline expression, with as aliasing the flow for the on predicate. SQL text and { "sql": … } bodies stay complete queries with their own FROM and never consume the flow.
{ "@expr/pipeline": [ { "@expr/query": { "from": "services", "select": { "as": { "ServiceName": "ServiceName", "value": "ErrorRate" } } } }, { "@expr/query": { "as": "c", "full_join": { "from": "baselines", "as": "p", "on": "c.ServiceName = p.ServiceName" }, "select": { "as": { "ServiceName": "coalesce(c.ServiceName, p.ServiceName)", "delta": "c.value - p.value" } } } } ]}@expr/case
Picks a value by matching a context value against a set of cases (with a default) — a lookup table. Use it to map a status to a colour, a mode to a label, an enum to a human string, without a chain of conditionals.
{ "@expr/case": { "status": { "ok": "green", "error": "red", "default": "gray" } } }@expr/color
Reads a source table, maps one of its values to a semantic colour tone, and appends the tone as a new column. Use it to colour a cell by its outcome, severity or status class (a named palette), or by where a number falls on a scale. Every source column passes through unchanged.
from is a table source (a sibling @block/context binding, an @expr/query, or any other table-producing expression). value is the SQL value to map: a bare column name ("Outcome") or any SQL fragment ("lower(SeverityText)", so a value that arrives mixed-case can be normalised before the lookup runs). color is the mapping, either { "palette": … } or { "scale": … }. as names the appended column (default color).
Chain it in a pipeline, once per column, then bind each appended column on its cell:
{ "@block/table": { "from": { "@expr/pipeline": [ { "@expr/query": { "from": "spans", "select": ["Outcome", "Status"] } }, { "@expr/color": { "value": "Outcome", "color": { "palette": "outcome" }, "as": "outcome_bg" } }, { "@expr/color": { "value": "Status", "color": { "palette": "http_status" }, "as": "status_bg" } } ] }, "columns": [ { "title": "Outcome", "value": "Outcome", "background_color": { "@expr/get": "outcome_bg" } }, { "title": "Status", "value": "Status", "background_color": { "@expr/get": "status_bg" } } ] }}Supported palettes: outcome (OTel StatusCode — Unset, Ok, Error), severity (OTel log severities), http_status (1xx–5xx), annotation_severity (critical, warning, ok, info, neutral). The signal and cache chart palettes have no tone equivalent and are not supported.
A value the palette does not define falls back to unknown — a BackgroundColor tone (default 'default', i.e. no tint). The long palette form pairs the name with an explicit fallback:
{ "@expr/color": { "from": "spans", "value": "lower(SeverityText)", "color": { "palette": { "name": "severity", "unknown": "muted" } }, "as": "severity_bg" }}Numeric scales
A scale buckets a numeric value into tones. It takes the same type, domain, scheme, range, reverse and unknown fields as a @block/plot colour scale, but scheme, range and unknown hold tones instead of CSS colours. The output is still a BackgroundColor tone, so a scale steps across tones rather than shades of one hue.
{ "@expr/color": { "from": "instances", "value": "HeapShare", "color": { "scale": { "type": "threshold", "domain": [0.75, 0.9] } }, "as": "heap_bg" }}type: "threshold"readsdomainas ascending boundaries. n boundaries give n + 1 steps, and a value equal to a boundary lands in the step above it, so the example tintsHeapSharebelow 0.75default, 0.75 up to 0.9warning, and 0.9 or aboveerror.type: "quantize"splits[min, max]into equal-width steps. Without adomainit reads the extent from the rows, and a column whose values are all equal takes the first tone. Values outside the domain take the first or last tone. A quantize scale without adomainalways runs on the database.type: "quantile"takes nodomain. It splits the rows into equal-count steps by rank, and rows with equal values share a step.quantizeandquantiletake one step per tone, or a bucket count through{ "quantize": { "n": 4 } }/{ "quantile": { "n": 4 } }.
The tones come from scheme or range, from the low end of the scale to the high end:
schemenames a tone ramp:alert(default,warning,error; the default) orhealth(success,warning,error). The ramp is sampled evenly when the step count differs from its length.rangelists one tone per step instead, e.g.["default", "info"].reverse: trueflips the order, for measures where low values are bad.unknownis the tone for a null value (defaultdefault).domainaccepts an expression, e.g.{ "@expr/get": "heap_limits" }, so the boundaries can follow a setting or page state.
@expr/combine_latest and @expr/concat
Merge several reactive inputs. @expr/combine_latest emits an array of the latest values of its inputs, re-emitting whenever any one changes — use it to feed a block something that depends on several state keys or queries at once. @expr/concat flattens several array-valued expressions into one list — handy for stitching together option lists or rows from more than one source.
{ "@expr/combine_latest": [{ "@expr/get_context": "userName" }, { "@expr/get_context": "userRole" }]}@expr/scalar
Coerces a table down to a single value. Most scalar-typed props reject a query outright — a query yields a table, and a title is text — so when you genuinely want a query result in one, wrap it: @expr/scalar takes the sole cell of a one-row result (and errors on more rows than one). A few blocks do this for you where it’s the obvious intent, like @block/stat’s value.
{ "@expr/scalar": { "@expr/query": { "from": "traces", "select": { "as": { "Services": "uniqExact(ServiceName)" } } } }}@expr/object, @expr/spread and @expr/get
Small value helpers. @expr/get reads a value out of context and walks a dot path into it ("totals.TotalCost") — pull a single field out of a one-row query, or a nested property out of a state object.
{ "@expr/get": "totals.TotalCost" }@expr/object builds a one-property object from a { key, value } pair. Both positions take an expression, so the key can be computed too.
{ "@expr/object": { "key": "env", "value": { "@expr/get_context": "env" } } }{ "@expr/object": { "key": { "@expr/get_context": "field" }, "value": 1 } }A many-property object is a merge of pairs, which is what @expr/spread is for.
@expr/spread merges objects left to right — literal ones, expressions resolving to one, or @expr/object pairs — so a later part’s keys win. Reach for it to assemble a map from pieces computed separately: a base set of values plus a conditional override.
{ "@expr/spread": [ { "service": "checkout" }, { "@expr/object": { "key": "env", "value": { "@expr/get_context": "env" } } } ]}Both are checked against the position they sit in: where a slot’s type says what its values are, each value is validated against it rather than accepted as anything.
@expr/resolve
Deep-substitutes any embedded @expr/* expressions inside a JSON-like value, leaving the surrounding shape intact. Reach for it to template a structured object — say, a block’s config — where only some fields are dynamic.
{ "@expr/resolve": { "title": "Dashboard", "user": { "@expr/get_context": "userName" }, "tags": ["static", { "@expr/get_context": "currentTag" }] }}