Per-task notifications
When it’s just one task you care about, skip the routing rules and name the channels right on the task. The setting sits next to the task it belongs to, so a glance at the file tells you which task pages which channel.
Every [tasks.*] and [services.*] block takes two fields:
notify_on_failure (run ended as failed, timeout, crashed,
log_overflow, or start_failed; also run.missed and
service.fatal) and
notify_on_success (run ended as succeeded). They work the same on
tasks and services. The field tables in
[tasks.*] and
[services.*] list the event
kinds that apply in each context.
Basic usage
Section titled “Basic usage”[tasks.nightly-db-backup]cron = "30 2 * * *"run = "/usr/local/bin/backup.sh"notify_on_failure = ["slack-ops"]That’s the whole setup. A failed backup lights up the slack-ops
channel, drops a row in the bell, and lands in the notification history.
The bell part is free: "inapp" sits in [notify] global_notifiers by
default, so it tags along on top of whatever channels you name. Don’t
want it? Set global_notifiers = [] to silence it (see
Global settings).
And if a notification rule happens to match the same event too, each
channel still gets exactly one message — duplicates are dropped.
Both fields together
Section titled “Both fields together”[services.payments-api]instances = 2run = "/usr/local/bin/payments-api"notify_on_failure = ["slack-ops", "tg-oncall"]notify_on_success = ["slack-ops"]A failure sends to two channels. A clean shutdown sends to one. The two lists are independent.
Missed-run alerts
Section titled “Missed-run alerts”There’s no notify_on_missed = [...] channel list, and that’s
deliberate. A missed scheduled run —
one the daemon was down for — is a failure, so it rides the exact same
channels you already named in notify_on_failure (plus the bell, via
global_notifiers). You don’t wire it up; it’s just there.
What you can do is turn it off for a task that’s expected to miss ticks — a laptop job that only runs while you’re at your desk, say:
[tasks.daytime-sync]cron = "*/30 9-17 * * 1-5"run = "/usr/local/bin/sync"notify_on_failure = ["slack-ops"]notify_on_missed = false # failures still page; missed ticks stay quietnotify_on_missed is a boolean, not a channel list. It defaults to
true, inherits from [defaults], and only silences the alert —
the missed run row is still recorded and browsable, so nothing
actually goes invisible. Use it instead of hollowing out
notify_on_failure, which would also drop your real failure alerts.
Inline target overrides
Section titled “Inline target overrides”Each entry in the list is one of three forms: a bare notifier id
("slack-ops"), the literal "inapp", or an inline override of
the form "<id>:<target>" that reuses the credentials of the named
notifier but sends to a different target. The override form is useful
when one workspace or one bot serves several destinations — one Slack
workspace covering several channels:
[[notifier]]id = "slack"type = "slack"webhook_url = "${RUNWISP_SLACK_URL}"# no `channel` set — each task picks one inline
[tasks.nightly-db-backup]notify_on_failure = ["slack:#ops"]
[tasks.weekly-deploy]notify_on_failure = ["slack:#deploys"]notify_on_success = ["slack:#deploys"]
[services.audit-stream]notify_on_failure = ["slack:#audit-alerts"]The form of <target> depends on the provider: for Slack it is a
channel (#name or @user); for Telegram it is the chat id.
[[notifier]]id = "tg"type = "telegram"bot_token = "${RUNWISP_TG_TOKEN}"chat_id = "-1001"
[tasks.payments-reconcile]notify_on_failure = ["tg:-1009998887"]Notifier ids cannot contain : — the colon separates the id from the
target, so id = "slack:foo" is rejected at load time. "inapp" has
no target and "inapp:anything" is rejected. Twenty tasks all writing
"slack:#ops" share one connection — #ops is not messaged twice
for the same event.
Use an explicit [[notifier]] block (with channel = "#…" set on
the block itself) when the same channel is used by enough tasks that
the override gets repetitive. Use the inline override when each task
has its own destination.
When to use a notification rule instead
Section titled “When to use a notification rule instead”A per-task field is for one task; a [[notification_route]]
block is for one rule covering many tasks.
Reach for a rule when you need a pattern across many tasks
(match.task = "backup-*"), a custom set of event kinds (the
per-task failure list covers failed, timeout, crashed,
log_overflow, start_failed, run.missed, and service.fatal), or
routing for kinds that have no task — for example
notify.delivery_failed. Per-task fields and rules mix freely: the
router collects every channel that matches and sends one message per
unique channel.
Worked examples
Section titled “Worked examples”Different settings per task
Section titled “Different settings per task”[[notifier]]id = "slack-ops"type = "slack"webhook_url = "${RUNWISP_SLACK_OPS_URL}"
[tasks.nightly-db-backup]cron = "30 2 * * *"run = "/usr/local/bin/backup.sh"notify_on_failure = ["slack-ops"]
[tasks.weekly-deploy]cron = "0 9 * * 1"run = "/usr/local/bin/deploy.sh"notify_on_failure = ["slack-ops"]notify_on_success = ["slack-ops"] # confirm Mondays went greenPer-task field plus a wildcard rule
Section titled “Per-task field plus a wildcard rule”[tasks.audit-log]cron = "0 0 * * *"run = "/usr/local/bin/audit"notify_on_failure = ["slack-ops"]
[tasks.process-event-queue]cron = "*/10 * * * *"run = "/usr/local/bin/process"notify_on_failure = ["slack-ops"]
# Backups also send to the on-call channel, in addition to the per-task setting[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"], task = "*-backup" }notify = ["tg-oncall"]A failed audit-log run sends one message to slack-ops and adds one row
to the bell. A failed *-backup run sends one message to slack-ops, one
to tg-oncall, and adds one row to the bell.
What the config loader rejects
Section titled “What the config loader rejects”The loader rejects unknown notifier ids in notify_on_failure /
notify_on_success (anything that isn’t declared in [[notifier]]
and isn’t "inapp"); inline overrides whose parent doesn’t exist,
whose target is empty, or — for Slack — doesn’t start with # or
@; and "inapp:something". An empty list (notify_on_failure = [])
is fine — it’s equivalent to omitting the field, and
[notify] global_notifiers still fires.