Skip to content

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.

[[notifier]]
id = "slack-ops"
type = "slack"
webhook_url = "${RUNWISP_SLACK_OPS_URL}"
channel = "#ops" # optional override

id, 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.

KeyRequiredWhat it does
webhook_urlyesThe webhook URL — inline, ${VAR}, or ${file:path}.
channelnoOverride the webhook’s default. Must start with # (channel) or @ (user).
template_pathnoPath to a Go-template file overriding the embedded message format.

In your Slack workspace:

  1. Open api.slack.com/apps and create a new app (or pick an existing one) for the workspace you want notifications in.
  2. Under Incoming Webhooks, toggle the feature on.
  3. Click Add New Webhook to Workspace, pick the destination channel (e.g. #ops), and authorise.
  4. 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.

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.

Terminal window
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"

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.

There are two places this channel id can appear. Pick whichever reads better in your file.

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

Trigger a task you know will fail:

Terminal window
runwisp exec smoke-test # whatever you have that exits non-zero

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

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-falcon

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

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.

  • channel that doesn’t start with # (channel) or @ (user).
  • A missing or empty webhook_url.
  • An id containing : (reserved for inline target overrides) or equal to "inapp" (reserved).