Notification rules
A [[notification_route]] block is one rule: when an event matches the
filter, it goes to the listed channels. An event can match several rules
at once — when that happens the router gathers up every channel named by
every match, throws out the duplicates, and sends one message per
channel. The order you write the rules in doesn’t matter; a channel
either matches or it doesn’t.
[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"], task = "backup-*" }notify = ["slack-ops", "tg-oncall", "inapp"]| Field | Required | What it does |
|---|---|---|
match.kind | no | Event kinds this rule applies to. Omit (or leave empty) to match all kinds. |
match.task | no | Shell-style glob on the task name. Omit to match every task. |
match.severity | no | Threshold: "info", "warn", "error". warn matches warn and error. |
Valid kind values:
| Kind | Fires when |
|---|---|
run.started | A run starts. Opt-in only — not on by default. |
run.succeeded | A run ends with success. |
run.failed | A run ends with a non-zero exit code. |
run.timeout | A run is killed for exceeding its timeout. |
run.stopped | A run is cancelled (manual stop, or on_overlap = "terminate"). |
run.crashed | A run is reconciled as crashed after the daemon was killed mid-flight. |
run.missed | A scheduled run was missed while the daemon was down (detected on restart). |
service.fatal | A service slot gave up after exhausting start_retries fast failures. |
log.disk_pressure | Free disk space dipped below min_free_space mid-run. |
notify.delivery_failed | A notifier exhausted its retries. Delivered only to the bell; cannot be used as a match.kind. |
The task glob uses standard shell-style patterns: * matches any
sequence except /, ? matches any single character except /, and
[abc] matches one of the listed characters. Globs are
case-sensitive (so Backup-* won’t match backup-db) and brace
expansion isn’t supported.
notify
Section titled “notify”You need at least one entry. Each one is either a [[notifier]] id
you’ve declared elsewhere in the file (e.g. "slack-ops"); the literal
"inapp", which is always there; or an inline target override of
the form "<id>:<target>" that borrows a parent notifier’s credentials
but posts somewhere else. "slack-ops:#deploys" reuses the slack-ops
credentials and sends to #deploys for this rule only. What <target>
looks like depends on the provider — see the
worked example.
A notifier id can’t contain : itself, since the colon is what marks
the separator. And the router dedups ids across every matching rule: if
two rules both fire on an event and both list slack-ops, the channel
hears about it once.
Per-task notifications
Section titled “Per-task notifications”If you only need to alert on one task, you can put the channel names
directly on the task itself with notify_on_failure /
notify_on_success:
[tasks.publish-feed]notify_on_failure = ["slack-ops"]notify_on_success = ["slack-ops"]See Per-task notifications for the full behaviour. Both forms can be used together; the router removes duplicate channels.
Delivery failures
Section titled “Delivery failures”When an outbound notifier burns through its retry budget, the daemon
raises a notify.delivery_failed event. To keep a dead channel from
generating yet more delivery attempts on itself, this one goes only to
the bell — it bypasses the route engine entirely and is delivered
directly to the in-app channel. A [[notification_route]] rule targeting
notify.delivery_failed will never fire, no matter what channel you point
it at. If you need a backup path, add a second notifier ID to the original
list instead — both channels get the same original event, and if one
delivery fails the other is unaffected.
Worked example
Section titled “Worked example”# Every failed/timeout/crashed run is sent to the team channel[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"] }notify = ["slack-ops"]
# Backups also send to the on-call channel[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"], task = "backup-*" }notify = ["tg-oncall"]A failure of backup-postgres sends one message to slack-ops, one
to tg-oncall, and adds one row to the bell (added automatically by
global_notifiers). A failure of metrics-export sends one message
to slack-ops and adds one row to the bell.
What the config loader rejects
Section titled “What the config loader rejects”The loader rejects routes that reference a notifier id not declared
in [[notifier]] (and not "inapp"); empty notify lists;
match.kind values that aren’t one of the valid kinds listed above;
match.task globs that can’t be parsed (e.g. unmatched [); inline
overrides whose parent isn’t declared, whose target is empty, or —
for Slack — doesn’t start with # or @; "inapp:something" (the
bell has no target); and [[notifier]] ids containing :.