Slack
The Slack driver posts through Slack’s Incoming Webhooks. Each
webhook URL has one default channel baked in, but an optional channel
override lets a single webhook fan out to several destinations.
Fields
Section titled “Fields”[[notifier]]id = "slack-ops"type = "slack"webhook_url = "${RUNWISP_SLACK_OPS_URL}"channel = "#ops" # optional overrideid, type, and webhook_url are required. Use
${...} substitution to pull the URL
from an env var or a file instead of writing it inline —
storing the secret.
| Key | Required | What it does |
|---|---|---|
webhook_url | yes | The webhook URL — inline, ${VAR}, or ${file:path}. |
channel | no | Override the webhook’s default. Must start with # (channel) or @ (user). |
template_path | no | Path to a Go-template file overriding the embedded message format. |
1. Create the webhook in Slack
Section titled “1. Create the webhook in Slack”In your Slack workspace:
- Open api.slack.com/apps and create a new app (or pick an existing one) for the workspace you want notifications in.
- Under Incoming Webhooks, toggle the feature on.
- Click Add New Webhook to Workspace, pick the destination
channel (e.g.
#ops), and authorise. - Copy the URL. It looks like
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX.
Treat the URL as a secret — anyone with it can post to your channel.
2. Store the URL
Section titled “2. Store the URL”Pick where the URL lives and reference it with
${...} substitution.
The simplest option for any deployment — Docker, systemd, bare metal. Set the variable in whatever already manages your environment.
export RUNWISP_SLACK_OPS_URL=https://hooks.slack.com/services/T00.../B00.../XXX...runwisp daemon[[notifier]]id = "slack-ops"type = "slack"webhook_url = "${RUNWISP_SLACK_OPS_URL}"channel = "#ops"Useful when a secrets manager (Vault agent, sops, Docker
secrets at /run/secrets/...) writes the URL to a known
path for you. Relative paths resolve next to runwisp.toml;
~/ works too.
mkdir -p ~/.config/runwispchmod 0700 ~/.config/runwispprintf '%s\n' 'https://hooks.slack.com/services/...' > ~/.config/runwisp/slack-ops.urlchmod 0600 ~/.config/runwisp/slack-ops.url[[notifier]]id = "slack-ops"type = "slack"webhook_url = "${file:~/.config/runwisp/slack-ops.url}"channel = "#ops"The notifier accepts the literal URL directly. Avoid it — config files are often committed to git or shared in chat, and an inline URL will leak.
[[notifier]]id = "slack-ops"type = "slack"webhook_url = "https://hooks.slack.com/services/T.../B.../X..."channel = "#ops"The id is what other parts of runwisp.toml refer to. Pick
something readable — slack-ops, slack-deploys, slack-marketing
all work. Without channel, the webhook posts to whatever channel
was selected when it was created.
3. Route failures to it
Section titled “3. Route failures to it”There are two places this channel id can appear. Pick whichever reads better in your file.
On one task
Section titled “On one task”[tasks.backup-postgres]cron = "30 2 * * *"notify_on_failure = ["slack-ops"]run = "..."That single line is enough. The bell receives the same event by default,
so if the Slack request fails you still see the failure in the bell. See
Per-task notifications for the full list of
options, including notify_on_success and inline channel overrides.
In a notification rule (one rule covering many tasks)
Section titled “In a notification rule (one rule covering many tasks)”[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"] }notify = ["slack-ops"]Omit match.task and the rule matches every task. Add a match.task
pattern for finer control:
# Backup failures also send to the on-call channel[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"], task = "backup-*" }notify = ["slack-ops", "tg-oncall"]The router removes duplicates — if a backup failure matches both a generic rule and a backup-specific rule, the channel receives one message.
4. Test it
Section titled “4. Test it”Trigger a task you know will fail:
runwisp exec smoke-test # whatever you have that exits non-zeroWithin a few seconds you should see a message in #ops with the task
name, end reason, and a preview of the captured output. If only the bell
shows a row, Slack
delivery failed — open the bell and look for a
notify.delivery_failed event with the underlying reason.
What a message looks like
Section titled “What a message looks like”The default Slack template builds one message per event as Block Kit
JSON. You get a header with the task name and verb, a section with the
event sentence and what triggered it, a code-block tail (for
run.failed and run.timeout), an action button that jumps to the run (when
[daemon] external_url is set),
and a footer reading “from runwisp” (with the daemon’s fingerprint
appended when set). Every event kind uses that same shape — only the
emoji, verb, and sentence change. Rendered
in a channel, a failure looks like:
❌ backup-postgres failed
Exited with code 1 after 0.3s.Manually triggered via API · 14 May, 17:11.
Error: connection refused dial tcp 127.0.0.1:5432: connect: connection refused
[ View full run ]
from runwisp · bright-falconNo external_url? The action button drops off. No log file? The
code-block tail drops off. Both happen quietly, no broken layout.
The helpers statusEmoji, statusVerb, humanTime, humanDuration,
runDuration, triggerPhrase, eventSentence, eventTrigger,
linkLabel, runURL, taskURL, outputTail, and fingerprint are
available to custom templates if you point template_path at a
Go-template file of your own.
Customising the message
Section titled “Customising the message”Point template_path at a Go-template file to override the embedded
message format. Copy
slack.tmpl.json
as your starting point — it has the full per-kind sentence table and
the Block Kit JSON shape. The template receives the full event struct:
task name, run id, exit code, end reason, timestamp, captured tail.
What the loader rejects
Section titled “What the loader rejects”channelthat doesn’t start with#(channel) or@(user).- A missing or empty
webhook_url. - An
idcontaining:(reserved for inline target overrides) or equal to"inapp"(reserved).