Annotations are text strings attached to objects when a Malloy file compiles. They carry metadata — display hints for the renderer, compiler flags, documentation strings, application-specific configuration. The compiler stores annotations verbatim and hands them back to whichever application asks; it does not interpret them.
Annotations start with # (attached to the object declared below) or ## (attached to the entire model) and run to end-of-line:
##! experimental.parameters # bar_chart view: how_many is things -> { aggregate: total_count is count() }
Block annotations
For annotations that span multiple lines, use the block form. Object block annotations are bracketed by #| and |#; model block annotations by ##| and |##:
source: flights is duckdb.table('flights.parquet') extend { #| bar_chart size=xl color=blue |# dimension: carrier_name is carrier }
The closing |# (or |##) must be at the same indentation level as the opener. Body lines are then dedented by the longest leading-whitespace prefix they share — so the block above is equivalent to writing three separate annotations # bar_chart, # size=xl, # color=blue. Pasting flush-left content works too: a body line with no leading whitespace forces the common prefix to zero and nothing gets stripped.
Block annotations can carry a route just like single-line ones:
#|(docs) This is multi-line documentation. It can be as long as it needs to be. |# source: my_source is duckdb.table('my_table')
Annotation distribution
An annotation that appears before a definition list is distributed to each member of the list. Each member can also carry its own annotations:
// Every measure renders as currency, but only `pct` also renders as percent # currency measure: max_x is max(x) # percent pct is avg(x) / all(avg(x)) min_x is min(x)
Prefix and route
Everything from the marker (#/##/#|/##|) up to the first whitespace is the annotation's prefix. The prefix resolves to a route — a namespace key. The compiler validates the shape of the prefix and routes the annotation; it never parses what comes after.
#(docs) Hello is an annotation with prefix #(docs) and content Hello. It's routed to docs. An application that has claimed the docs route reads the content; everything else ignores it.
A non-empty prefix must match one of two shapes:
Punctuation —
#followed by punctuation characters:#!,##!,#@,#",#:. These are reserved for Malloy's internal use; the compiler knows the full set. An unrecognized punctuation prefix (#%,#~) emits areserved-routewarning.Bracketed name —
#followed by a bracket pair ((),<>,[], or{}) with any non-matching-close characters inside. The route is the literal text inside the brackets. Bracket pair doesn't matter:#(docs),#<docs>,#[docs],#{docs}all resolve to the same routedocs. Content is opaque, so#(bar-chart),#(my.app), and#(https://example.com/ns)all work as their literal strings.
If the prefix is just # followed by whitespace (# tag, ## tag), the route is the empty string — the default, used by the renderer.
Anything else (a bare word #NO_UI, an unclosed bracket #(docs, trailing junk after the close #((X)))) emits a malformed-route warning. The warning is non-fatal; the annotation is still stored.
Routes Malloy itself claims
| route | written as | who reads it |
|---|---|---|
'' (empty) |
# tag / ## tag |
the renderer |
! |
##! flag |
the compiler (compiler flags) |
@ |
#@ directive |
the foundation persistence layer |
" |
#" markdown |
the explorer (description strings, rendered as markdown) |
App routes use the bracketed form: #(myApp) .... The route name is whatever the app claims. The Malloy documentation site is the informal registry for claimed routes — claim yours there if you publish an app that reads annotations.
Annotations whose route uses the empty form (# tag) or one of Malloy's punctuation-route forms speak the small property language described below — Malloy's tag language. This is what the renderer parses for display hints, what the compiler parses for ##! flags, and what most apps will parse for their own routes (unless they choose otherwise — see "Bring your own payload" below).
A quick tour:
tNameSets the property
tNameto exist, with no value.Example:
# hidden.
tName=tValSets
tNameto a value.Example:
# color=red. Quote values that need spaces:# name="John J. Johnson". Use backticks for property names that need quoting:`my long property name`=red.Lists:
tName=[val1, val2].
-tNameremoves the propertytName.tName: { p1=v1 p2=v2 }—tNameis a collection of sub-properties.Example:
# barchart: { bgColor=white fgColor=red }.
Advanced property syntax
tName=value { p1=v1 p2=v2 }—tNamehas both a value and sub-properties.tName=value— assign a new value totName, deleting any existing sub-properties.tName=value {...}— assign a new value but keep existing sub-properties.tName: { p1=v1 p2=v2 }— replace sub-properties, delete any existing value.tName { p1=v1 p2=v2 }— merge sub-properties, preserve value.tName=...{ p1=v1 p2=v2 }— assign new sub-properties, keep value.tName.p1=value— set one nested property; other properties and the value oftNameare preserved.tName.p1=value { pp1=v1 pp2=v2 }— nested properties can themselves have properties.
For the renderer's catalog of meaningful tag names, see the Render Tags documentation.
If your app's annotations aren't shaped like tags — for example, you want to embed JSON or markdown — claim a bracketed route and parse the content yourself. Malloy hands you the raw content plus source-location offsets so your parser's error messages can point back to the model file:
// In the Malloy API for (const note of field.annotations.forRoute('myApp')) { const content = note.rawText.slice(note.contentIndex); try { handleConfig(JSON.parse(content)); } catch (e) { reportError(e.message, note.at); // your error squigglies land in the model } }
If your app does use the tag language on its route, the shorter call is:
const tag = field.annotations.parseAsTag('myApp').tag; if (tag.has('hidden')) hide(field);
The compiler stores annotation text verbatim and routes by prefix. It does not validate content. A route is a claim — by claiming (myApp), you take responsibility for what #(myApp) ... annotations mean in your part of the world. Other apps using other routes don't collide with you, and the renderer's default route stays out of your way unless you write # ... deliberately.