Malloy Documentation
search

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 a reserved-route warning.

  • 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 route docs. 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:

  • tName

    • Sets the property tName to exist, with no value.

    • Example: # hidden.

  • tName=tVal

    • Sets tName to 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].

  • -tName removes the property tName.

  • tName: { p1=v1 p2=v2 }tName is a collection of sub-properties.

    • Example: # barchart: { bgColor=white fgColor=red }.

Advanced property syntax

  • tName=value { p1=v1 p2=v2 }tName has both a value and sub-properties.

  • tName=value — assign a new value to tName, 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 of tName are 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.