Docs / Build Workflow

Visualization — text

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 ships markdown (the only value; plain / html are reserved).
  • body — required. The Markdown source. Supported subset: headings, paragraphs, ordered / unordered lists, bold, italic, inline code, 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 of mapping, and every mapping key 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, https and 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 a mapping key referenced by no token, is rejected. Keep tokens and mapping in lock-step.
  • mapping without a query. Tokens need a source — a mapping with no query is 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.