Bindings & tokens

A banner shows data that arrives with each notification. A binding is how a template says which data goes where: a string with {token} placeholders, written in a component's binding property. This page explains what a token is, where its value comes from, how values are printed, and what happens when a value is missing. It is for anyone who writes template JSON or designs a banner in the Designer.

Concepts#

A token is a name between braces, such as {title} or {customer.name}. When Herald draws a banner it replaces each token with the value of the field of that name. A binding can be one token, several tokens with text around them, or plain text:

JSON
{"type": "text", "binding": "{count} new from {sender}"}

Everything a notification sends is flattened into one dictionary of fields. A token reads one field. Nothing else connects a notification to a banner, so a field a template never names is never shown.

A field is absent when the notification does not send it or sends a blank value. A component whose tokens are all absent is empty, and an empty component can collapse. See Empty values.

Syntax#

RuleExampleResult
A token is letters, digits, _, . and - between braces.{customer.name}The field customer.name.
Braces that do not enclose such a name are plain text.{}, { a b }, {"json": 1}Left exactly as written.
Substituted text is never scanned again.A value {x}Stays as typed.
There are no expressions, filters or default values.Format values before you send them.

Where values come from#

Each source adds fields. An earlier source wins over a later one when two define the same name.

OrderSourceFields
1The notification's own keystitle, subtitle, body, image, url, app, id, priority, sound, group
2The app's manifestappName
3metadataEvery scalar and list in it, and keys a sender put at the top level of the request. See Metadata.
4The template's extraextra.<key>, always text.
5DeliverydeliveredAt, the delivery time as an ISO 8601 string.
6The banner's stackstack.count, present only while two or more notifications are stacked. See Stacking.

Blank values are left out, so a field that is an empty string is the same as a field that is missing.

image is also present whenever the banner has a picture, for example a preview that was handed one.

Built-in tokens#

These tokens exist without any manifest. Whether they have a value depends on what the notification sent.

TokenTypeDescription
{title}textThe headline.
{subtitle}textThe second line.
{body}textThe main text.
{image}imageThe notification's picture.
{url}urlThe notification's link.
{app}textThe app id, for example example.bidbot.
{appName}textThe app's display name from its manifest.
{id}textThe notification id.
{priority}textlow, normal, high or urgent, when sent.
{sound}textThe sound the notification asked for.
{group}textThe stacking key.
{deliveredAt}dateWhen Herald received the notification.
{stack.count}numberHow many notifications the banner stands for. Absent while the banner is alone.
{extra.key}textA value from the template's extra.

Metadata#

metadata carries any data your app wants a template to use. Herald flattens it into fields:

  • A string, number or boolean becomes a field with the key's name.
  • An object becomes dotted names, up to three levels of nesting: customer.name, customer.address.city.
  • An array of strings, numbers or booleans becomes a list.
  • null, and arrays that hold objects, are skipped.
  • A key that is already taken by an earlier source is skipped.

A key at the top level of a notification request that is not a notification field, such as a manifest field called count, is moved into metadata before any of this. These two requests give the same fields. When a key is in both places, the top-level one wins.

JSON
{"app": "example.bidbot", "title": "Bid accepted", "count": 2}
JSON
{"app": "example.bidbot", "title": "Bid accepted", "metadata": {"count": 2}}

A request with nested data and the fields it produces:

JSON
{
  "app": "example.bidbot",
  "title": "Bid accepted",
  "amount": 4200,
  "metadata": {
    "client": {"name": "Acme", "address": {"city": "Oslo"}},
    "tags": ["rfp", "q3"],
    "note": null
  }
}
FieldValueNote
titleBid acceptedA notification key.
amount4200A top-level key, moved into metadata.
client.nameAcmeFlattened.
client.address.cityOsloFlattened.
tagsrfp, q3A list prints joined by a comma and a space.
notenonenull is skipped, so the field is absent.

Where a token comes from in the Designer#

The Designer files every token it offers under where its value comes from. The sample shown beside a token is what the preview fills it with.

Group in the menuSourceWhat it isSample
From the issuer app (manifest field)The manifestA field the app's manifest declares, with a type and a sample. The app promises to send it.The manifest's sample.
From the notification (payload field)The notificationA built-in token, or any other key a real notification carried or you typed as a custom token. Nobody promises it, so it is absent when the notification does not send it.The last real value, or a stand-in.
Set here (fixed value)The templateAn extra.<key> value you wrote in the template. The same for every notification.The value itself.

The palette on the left lists the same tokens in smaller groups: Issuer fields, Standard, Seen in notifications, Extra (yours) and Custom.

A fixed picture is the same idea without a token. An image component whose binding is a plain file path shows that file for every notification, and is never empty while the file exists. See Image source.

Formatting#

Values are printed as plain text. Components that need a number or a date read the text.

ValuePrinted as
TextItself.
NumberWithout a trailing .0: 3 and 3.5.
Booleantrue or false.
ListThe items joined with a comma and a space.

When the app has a manifest, a field declared number or bool that arrives as text is converted.

Declared typeTextBecomes
number"2"The number 2.
booltrue, yes or 1True.
boolfalse, no or 0False.

How components read their value:

ComponentReads the value as
progressA fraction. 0.4, 40 and 40% all mean 40 percent. A number above 1 is a percentage. The result is held between 0 and 1.
timestampA date: ISO 8601 text, or seconds or milliseconds since 1970. With relative it prints 3 min. ago. Otherwise it prints the time for today and the date and time for another day.
imageA file path, an https URL or a data: URI.

Empty values#

A component is empty when its binding has at least one token and every token is absent. A binding with no token is literal text, and it is empty only when it is blank.

In a binding with several tokens, absent tokens become empty text, literal text stays, and the result is trimmed. Literal text never keeps a component alive.

BindingFields presentResult
{title}noneEmpty.
{sender}: {subject}subject only: Invoice 4021
{sender} {subject}subject onlyInvoice 4021
{count} newnoneEmpty, not new.
InboxnoneInbox, because it has no token.

Write bindings so that a missing part still reads well, or split them into two components that collapse independently. A multi-line text drops a line whose tokens are all absent, and is empty when no line has content.

What each component counts as its binding is on its page under empty values: Components. issuerIcon and spacer never read data and are never empty. A rive cell is empty only when it names no animation. An action label that comes out empty shows the action's id. The full rules for collapsing are in Collapse semantics.

Template default text#

A template's default title, subtitle, body and url use the same token syntax, but they are filled when the notification arrives, from a smaller set of values:

  1. metadata, including nested names such as {customer.name}.
  2. The notification's own title, subtitle, body, app, id and group.

A token with no value becomes empty text. Only text that comes from the template is filled. A notification's own text may contain braces, and they are left alone. In a url, each substituted value is percent-encoded.

Sample values#

The Designer, POST /v1/preview with "data": "sample" and the MCP tool render_preview fill fields from the sample values in the app's manifest. A declared field with no sample gets a stand-in:

Field typeStand-in
textThe key made readable: receivedAt becomes Received at. title, subtitle and body get Notification title, A short subtitle and Body text goes here.
number3
dateThe current time.
urlhttps://example.com
booltrue
listOne, Two, Three
imageNothing, because there is no picture to invent.

Without a manifest, only a generic title, subtitle and body exist.

Where tokens may be used#

PlaceTokensNote
binding of text, badge, progress, timestamp and imageyes
A text's lines, in a run's tokenyes
inputBindings values of riveyesA single token keeps its number or boolean type.
An action's label and inputyes
The url of an action the template addsyesValues are percent-encoded. A leading {url} whose value is an openable link is kept whole.
The url of an action the issuer sendsnoIt is used as written.
A shell commandnoData reaches the command on standard input and in HERALD_* environment variables. See Actions.
Template defaults title, subtitle, body, urlyesSee Template default text.
symbol.colors and symbol.variableValueyesSee Symbols.
stateMachine, artboard, asset, pathnoAlways literal.

Declared fields and warnings#

The manifest tells the Designer and the validator which tokens an app sends, with a type and a sample. See Manifests. With a manifest, the validator warns about a token the manifest does not declare:

text
token {sender} is not declared in the manifest (it only resolves if the issuer sends it)

The warning does not block saving, because a notification can still send the field in metadata. The validator also warns when {extra.key} has no matching key in the template's extra.

Edit this page on GitHub

Esc
Getting started
Guides