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.