Skip to content

Discord

The Discord driver posts color-coded embeds through a channel webhook — red for failures, green for successes, yellow for warnings. No bot to host, no token to refresh: a webhook URL is all Discord needs.

[[notifier]]
id = "discord-ops"
type = "discord"
webhook_url = "${RUNWISP_DISCORD_OPS_URL}"

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}.
template_pathnoPath to a Go-template file overriding the embedded message format.

In the server you want notifications in:

  1. Open Server Settings → Integrations → Webhooks and click New Webhook.
  2. Pick the destination channel (e.g. #ops-alerts).
  3. Set the name and avatar here too — that’s what messages post as.
  4. Click Copy Webhook URL. It looks like https://discord.com/api/webhooks/1234567890/XXXXXXXXXXXXXXXXXXXX.

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_DISCORD_OPS_URL=https://discord.com/api/webhooks/123.../XXX...
runwisp daemon
[[notifier]]
id = "discord-ops"
type = "discord"
webhook_url = "${RUNWISP_DISCORD_OPS_URL}"

The id is what other parts of runwisp.toml refer to. Pick something readable — discord-ops, discord-deploys both work. A webhook is bound to the channel it was created in; for a second channel, create a second webhook and a second [[notifier]].

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 = ["discord-ops"]
run = "..."

That single line is enough. The bell receives the same event by default, so if the Discord request fails you still see the failure in the bell. See Per-task notifications for the full list of options, including notify_on_success.

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 = ["discord-ops"]

Omit match.task and the rule matches every task. Add a match.task pattern for finer control:

# Backup failures also ping the on-call channel
[[notification_route]]
match = { kind = ["run.failed", "run.timeout", "run.crashed"], task = "backup-*" }
notify = ["discord-ops", "discord-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 red embed in your channel with the task name, end reason, and a preview of the captured stderr, plus a new row in the Web UI’s bell. If only the bell shows a row, Discord delivery failed — open the bell and look for a notify.delivery_failed event with the underlying reason.

The default template builds one embed per event. The title carries the task name and verb and links to the run (when [daemon] external_url is set), the description carries the event sentence, the trigger, and a code-block tail of captured stderr (failures and timeouts only), and the footer reads “from runwisp” (with the daemon’s fingerprint appended when set). The embed’s color strip tells you the outcome at a glance:

ColorWhen
Redrun.failed, run.timeout, crashes
Greenrun.succeeded
YellowWarnings like log.disk_pressure
BlurpleEverything else (e.g. run.started)

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
from runwisp · bright-falcon

No external_url? The title just isn’t a link. No log file? The code-block tail drops off. Both happen quietly, no broken layout. The payload also suppresses mentions, so a task that prints @everyone can’t ping your whole server.

Point template_path at a Go-template file to override the embedded message format. Copy discord.tmpl.json as your starting point — it has the embed shape and the color table. The template receives the full event struct and the same helpers as the other providers: statusEmoji, statusVerb, eventSentence, eventTrigger, humanTime, runURL, taskURL, outputTail, fingerprint, and more.

  • A missing or empty webhook_url.
  • A webhook_url that isn’t http:// or https://.
  • An id containing : (reserved for inline target overrides) or equal to "inapp" (reserved).

Discord notifiers do not support inline target overrides (like discord-ops:something) — a webhook is bound to one channel, so there’s no target to override.