Skip to content

Message Variables

A message block's string values may embed {{variable}} tokens, expanded at send time with facts the Processor knows without asking the database (the outage message fires precisely when the database is gone):

"message": {"channel": "CKT02UTD2",
            "text": ":arrow_forward: pg_relay started on {{host}} at {{time_local:HH24:MI}} (instance {{instance}})"}
Variable Value Example
{{time}} Send time, RFC 3339 UTC (formattable) 2026-08-25T12:04:38Z
{{time_local}} Send time in the Processor host's zone (formattable) 2026-08-25T22:04:38+10:00
{{host}} The Processor host's name db01
{{os}} The Processor's operating system linux
{{pid}} The Processor's process id 13288
{{instance}} The instance slot (1–64); 0 when none is held yet — a startup outage fires before a slot exists 1
{{event}} Which notification this is: outage, recovery, start, or stop (test under --test-notify for a start/stop contract) — lets one template serve several contracts outage
{{database}} PGDATABASE from the environment (empty if unset) relay_dev
{{dbhost}} PGHOST from the environment (empty if unset) localhost
{{open}} The literal characters {{ — emits a token for a downstream system's own templating {{
{{close}} The literal characters }} — cosmetic partner to {{open}} (a bare }} already passes verbatim) }}

Three more are valid only in an outage contract's messages (using them in a start/stop contract refuses startup):

Variable Value
{{outage_started}} The episode's start, RFC 3339 UTC (formattable) — the same instant that seeds the email Message-ID
{{outage_seconds}} Whole seconds since the episode started: in outage_message roughly the threshold; in recovery_message the full outage duration
{{outage_duration}} The same, human-formatted: 45s, 1m 34s, 2h 3m 4s

{{time}} is the send-attempt time — an outage delivery that retries re-stamps it — while {{outage_started}} is stable for the whole episode. Only tokens shaped exactly like {{lowercase_name}} (optionally with a :format) are touched; anything else — {{ spaced }}, uppercase, single braces — passes through verbatim, so message bodies aimed at systems with their own templating are never mangled. When a downstream token does collide with that shape, write it through the escape variables: {{open}}incident_url}} (or the symmetric {{open}}incident_url{{close}}) lands on the wire as the literal {{incident_url}}. Expansion is a single pass, so the braces it emits are never re-expanded — {{open}}time}} reliably delivers {{time}}, not a timestamp. An unknown variable name refuses startup (notify_config_invalid), naming the token and listing the known set: a typo is caught while a human is watching.

Date/time formatting: to_char patterns

The three timestamp variables accept a format after a colon, in the pattern language you already write every day — PostgreSQL's to_char():

{{time:YYYY-MM-DD HH24:MI:SS}}          → 2026-08-25 12:04:38
{{time_local:DD Mon YYYY, HH12:MI am}}  → 25 Aug 2026, 10:04 pm
{{outage_started:HH24:MI TZ}}           → 11:59 UTC

The supported subset: YYYY YY MM Mon/MON/mon Month/MONTH/month DD Dy/DY/dy Day/DAY/day HH24 HH12 HH (= HH12, as in to_char) MI SS MS US AM/PM/am/pm TZ/tz OF. Non-letter characters are literals. Literal letters must be double-quoted exactly as in to_char itself (YYYY-MM-DD"T"HH24:MI:SS). An unquoted letter run that isn't a token refuses startup, naming the run and listing the supported tokens — so {{time:YYYY-QQ}} fails at parse instead of rendering garbage at 3 a.m. Two documented divergences from Postgres: Month/Day render without blank padding (as if FM were applied), and OF always carries minutes (+10:00). A format on a non-timestamp variable ({{host:DD}}) is likewise a parse error.

--test-notify prints the fully rendered message, so it doubles as a template previewer — iterate on a format against the real provider without touching the daemon.


Back to Lifecycle Notifications.