Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Template engine

The shared template engine that command, webhook, teams, and post triggers use to substitute notification fields and environment variables into their templated inputs.

Syntax

Two expression forms inside {{ ... }}:

{{ notification.<dotted.path> }}    -- substitutes a notification field
{{ env.<NAME> }}                    -- substitutes a process environment variable

Literal {{ is escaped as \{{. Everything outside {{ ... }} is passed through verbatim.

Notification paths

The notification namespace walks the notification’s serialised JSON tree. Empty path means the whole notification:

ExpressionResolves to
{{ notification }}The whole notification as compact JSON: {"event_type":"mars","sequence":42,...}
{{ notification.event_type }}The event_type string, unquoted: mars
{{ notification.sequence }}The sequence as a numeric-string: 42
{{ notification.identifier }}The identifier object as compact JSON: {"class":"od","date":"20260601",...}
{{ notification.identifier.class }}A specific identifier value, unquoted: od
{{ notification.payload }}The payload as compact JSON: {"seed":"x"} (or null)
{{ notification.payload.seed }}A specific payload field, unquoted: x

Paths can go arbitrarily deep into nested objects. Array indexing is not supported (the engine walks object keys only).

Env paths

url: "{{ env.WEBHOOK_URL }}"
body_template: '{"token": "{{ env.SECRET }}"}'

{{ env.<NAME> }} reads std::env::var(NAME). Two failure modes:

  • Variable not set: TemplateErrorKind::EnvNotSet. Operator must export NAME=value before running.
  • Variable set but not UTF-8: TemplateErrorKind::EnvNotUnicode. Rare; usually means a misconfigured deployment.

Both surface as TriggerError::Template at first dispatch (the constructor is infallible; errors are deferred to render time).

Value rendering rules

How each JSON value type renders into the template’s output:

JSON typeRendered asExample
StringThe string contents, unquotedod (NOT "od")
NumberDecimal representation, unquoted42, 3.14, -7
Booleantrue or falsetrue
NullLiteral four-character string nullnull
ObjectCompact JSON, including the surrounding braces{"class":"od","date":"20260601"}
ArrayCompact JSON["a","b","c"]

These are the values as rendered for a JSON body or a header. When the text is a shell command or a URL, each notification value is also neutralised for that use; see Where the text goes.

The string-unquoted rule is the load-bearing one for safe embedding in JSON bodies. Compare:

"value": "{{ notification.identifier.class }}"
                ↓ renders ↓
"value": "od"                              ← valid JSON; the surrounding quotes ARE the JSON string delimiters

vs. trying to embed an object value inside a JSON string:

"value": "{{ notification.identifier }}"
                ↓ renders ↓
"value": "{"class":"od","date":"20260601"}"   ← INVALID JSON; inner quotes break the string

For object/array values, embed them outside a JSON string field:

"identifier": {{ notification.identifier }}
                ↓ renders ↓
"identifier": {"class":"od","date":"20260601"}   ← valid JSON; object is a JSON value, not a string

Where the text goes

Notification values are written by the publisher, not by you. Each place a rendered template is used has its own idea of which characters are special, so the engine neutralises every {{ notification.* }} value for that place:

Rendered text isNotification values are
a command: stringquoted for the shell context they land in: wrapped in single quotes when bare, ' escaped inside single quotes, and backslash, $, backtick and " escaped inside double quotes. The shell reads the value as one literal argument. A placeholder after a here-document, arithmetic expansion or backticks is refused with ValueAfterUnsupportedShellSyntax; see the Command trigger page.
a webhook url:percent-encoded (letters, digits, -, _ and ~ kept; . is encoded too so .. cannot fold the path), so a value cannot add or remove a path segment or add a query parameter. A notification placeholder in the scheme or authority, where the value would be the host, is refused with ValueInUrlAuthority. Put the host in the template or in an {{ env.* }} value and notification values in the path, query or fragment.
a header value or a bodyinserted as written. You supply the encoding around the value, for example the quotes of a JSON string.

{{ env.* }} values are your own and are inserted as written everywhere.

Common patterns

Pass-through scalar identifier values

body_template: '{"who": "{{ notification.identifier.class }}", "what": "{{ notification.event_type }}"}'

Renders to: {"who": "od", "what": "mars"}

Pass-through whole identifier as nested object

body_template: '{"id": {{ notification.identifier }}, "seq": {{ notification.sequence }}}'

Renders to: {"id": {"class":"od","date":"20260601",...}, "seq": 42}

Pass-through whole notification

body_template: '{{ notification }}'

Renders to: {"event_type":"mars","sequence":42,"identifier":{...},"payload":{...}}

This is the default body when body_template: is omitted on the webhook trigger. The echo trigger emits the same shape in pipe mode.

Secret-bearing URL

url: "{{ env.WEBHOOK_URL }}"

The URL never appears in YAML, in logs, or in error messages. Set the env var in your deployment:

export WEBHOOK_URL='https://hooks.example.com/notify?token=xxx'

Conditional content via env tagging

title_template: "[{{ env.DEPLOYMENT_TIER }}] aviso {{ notification.event_type }}"

{{ env.DEPLOYMENT_TIER }} can be prod, staging, dev, etc., setting different prefixes per deployment.

Error categories

Template errors fall into one of seven TemplateErrorKind values, surfaced via TriggerError::Template { context, field, kind }:

KindWhenExample template
MissingA {{ notification.<path> }} resolved to nothing{{ notification.identifier.nonexistent }} when the notification has no nonexistent field
EnvNotSetA {{ env.<NAME> }} variable is not in the process environment{{ env.UNDEFINED_VAR }} when UNDEFINED_VAR is not exported
EnvNotUnicodeA {{ env.<NAME> }} variable’s value is not valid UTF-8(rare; usually a misconfigured deployment)
BadSyntaxTemplate parse failure: unclosed {{, empty path segment, unknown namespace{{ unclosed, {{ notification..empty }}, {{ unknown.foo }}
ValueInUrlAuthorityA {{ notification.<path> }} in the scheme or authority of a webhook URLhttps://{{ notification.payload.host }}/hook
ValueAfterUnsupportedShellSyntaxA {{ notification.<path> }} in a command after a here-document, $(( )), backticks or case, directly after a $, inside ${ }, after a redirection operator, as the command name, or as an argument to eval, trap, . or sh -c; field names the reasoncat <<EOF ... EOF; run {{ notification.sequence }}
NotificationEncodeNotification could not be serialised to JSONPractically unreachable

NotificationEncode occurs when resolving a path. It is practically unreachable for well-typed notifications. The variant points the diagnosis at the notification rather than a missing-path template bug.

All seven are terminal under fail_fast: true (the default): retrying with the same notification and environment will produce the same template error.

The context carried on TriggerError::Template is the safe static label ("webhook url", "command", "teams title", etc.) of where the failure occurred. field carries the JSON path / env-var name / parse-failure category (whichever applies to the kind). Neither echoes the raw template, which may carry secrets.

What the template engine does NOT support

  • Conditionals - no {% if %} / {% else %}. Use Rust code outside aviso if you need branching.
  • Loops - no {% for %} over object keys or array elements. Templates render specific paths; the teams and post triggers iterate identifier fields at dispatch time via Rust code (not via the template engine).
  • Filters / pipes - no {{ value | uppercase }} or {{ value | json }}. Escaping for the shell and for URLs is applied by the engine according to where the text goes, so no filter is needed for that. Operators wanting other transformations should do them in the receiver.
  • Macros / includes - templates are flat strings; no recursion.

This is intentional: a more featureful template engine adds attack surface and complexity for marginal value. Operators wanting full programmability should use the command trigger (which runs arbitrary shell code) or process the notification downstream.