When to use text
A text note is the explanatory layer of a dashboard — section headings, narrative, instructions, caveats, legends: the words that make a wall of charts make sense. The common note is pure prose with no query. Optionally, when it helps a sentence, a note can weave a live value from a query into the text — but that is a secondary feature, not the point.
This is not a kpi: a kpi is a metric tile with fixed headline/delta slots and card chrome; a text note is free prose. If the answer is a single number, use kpi; if you need a number per category, use bar; for a table, use grid. Use text when the content is language.
Static vs dynamic
A static note has only a body — Markdown prose, no query, no data fetch. A dynamic note adds a single-row query and a mapping, and interpolates values into the prose.
format— body format. v1 shipsmarkdown(the only value;plain/htmlare reserved).body— required. The Markdown source. Supported subset: headings, paragraphs, ordered / unordered lists, bold, italic, inlinecode, fenced code blocks, and[links](url).
Tokens and mapping
A dynamic note interpolates scalar values from the single result row of its query. Each token in the body is bound to a column through mapping:
- Token syntax:
{{ field }}, or with a number format{{ field | "$#,##0" }}. mapping— an object of token name → result column. Every token's name must be a key ofmapping, and everymappingkey must be used by a token; otherwise the note is rejected at validation.- Tokens are scalars only. Braces that do not match the token grammar are treated as literal text.
mapping:
total_revenue: revenue_total # token "total_revenue" -> column "revenue_total"
growth_pct: mom_growth
body: |
Revenue this month is **{{ total_revenue | "$#,##0" }}**, up
**{{ growth_pct | "+0.0%" }}** versus last.
Number-format patterns use the same grammar as every other viz — see the viz-types overview.
query is optional
text is the one visualization type where query is optional. A note with no query is a static note: it renders its prose immediately, with no execution. Add a single-row query only when the note interpolates a value. Every other viz type requires a query.
Safety
The note is safe by construction:
- Interpolated values are HTML-escaped — a value containing markup renders as literal text, never as elements.
- The rendered Markdown is sanitized — only the supported subset survives; scripts and unknown tags are stripped.
- Link hrefs are allow-listed to
http,httpsand relative URLs; any other scheme (e.g.javascript:,data:) renders as inert text.
Cross-filter behavior
A text note participates as a consumer only, exactly like kpi:
- Does not emit. A note has no dimension to click — it adds no pills.
- Does react. A dynamic note's query re-runs with the active dashboard filters and pills, and its interpolated values recompute. A static note has nothing to re-run.
Worked examples
A static note — pure prose, no query (the common case):
id: ops_intro
title: Operations overview
type: text
format: markdown
body: |
## Operations overview
These panels track fulfilment health. **Amber** cells flag SLAs
at risk. Figures exclude cancelled orders.
published: true
A dynamic note — a live value woven into a sentence:
id: revenue_note
title: Revenue summary
type: text
format: markdown
query: "models/sales.malloy::headline" # one row, many columns
mapping:
total_revenue: revenue_total
growth_pct: mom_growth
body: |
## Revenue summary
Total this month is **{{ total_revenue | "$#,##0" }}**, up
**{{ growth_pct | "+0.0%" }}** versus last month.
published: true
Common pitfalls
- A token renders as a placeholder. The mapped column is missing or null in the result row, or the query failed — the rest of the prose still renders. Check the column name in
mapping. - Validation rejects the note. A token whose name is not in
mapping, or amappingkey referenced by no token, is rejected. Keep tokens and mapping in lock-step. - mapping without a query. Tokens need a source — a
mappingwith noqueryis invalid. - Expecting a table. Tokens are scalars; only the first row's named columns are read. A many-row query is not rendered as a list.
- A link does nothing. Only http / https / relative hrefs become links; other schemes render as plain text by design.