Skip to content

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"]
FieldRequiredWhat it does
match.kindnoEvent kinds this rule applies to. Omit (or leave empty) to match all kinds.
match.tasknoShell-style glob on the task name. Omit to match every task.
match.severitynoThreshold: "info", "warn", "error". warn matches warn and error.

Valid kind values:

KindFires when
run.startedA run starts. Opt-in only — not on by default.
run.succeededA run ends with success.
run.failedA run ends with a non-zero exit code.
run.timeoutA run is killed for exceeding its timeout.
run.stoppedA run is cancelled (manual stop, or on_overlap = "terminate").
run.crashedA run is reconciled as crashed after the daemon was killed mid-flight.
run.missedA scheduled run was missed while the daemon was down (detected on restart).
service.fatalA service slot gave up after exhausting start_retries fast failures.
log.disk_pressureFree disk space dipped below min_free_space mid-run.
notify.delivery_failedA 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.

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.

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.

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.

# 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.

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 :.