Webhook
The webhook driver POSTs a JSON body to any HTTP endpoint you control. It’s the catch-all for services that accept incoming webhooks but don’t have a dedicated provider — PagerDuty, n8n, a custom server, anything that speaks HTTP. (For Discord, use the native provider instead — it sends embeds Discord actually renders.)
Fields
Section titled “Fields”[[notifier]]id = "my-hook"type = "webhook"url = "${WEBHOOK_URL}"
[notifier.headers]Authorization = "Bearer ${WEBHOOK_TOKEN}"id, type, and url are required.
| Key | Required | What it does |
|---|---|---|
url | yes | The endpoint URL — inline, ${VAR}, or ${file:path}. |
headers | no | Extra HTTP headers sent with every request (e.g. auth tokens). |
template_path | no | Path to a Go-template file overriding the embedded JSON payload. |
Use ${...} substitution in both url
and header values to pull secrets from env vars or files —
storing the secret.
1. Store the URL
Section titled “1. Store the URL”Pick where the URL lives and reference it with
${...} substitution.
The simplest option. Set the variable in whatever already manages your environment.
export WEBHOOK_URL=https://example.com/hooks/abc123export WEBHOOK_TOKEN=my-secret-tokenrunwisp daemon[[notifier]]id = "my-hook"type = "webhook"url = "${WEBHOOK_URL}"
[notifier.headers]Authorization = "Bearer ${WEBHOOK_TOKEN}"Useful when a secrets manager writes the URL to a known path.
mkdir -p ~/.config/runwispchmod 0700 ~/.config/runwispprintf '%s\n' 'https://example.com/hooks/abc123' > ~/.config/runwisp/webhook.urlchmod 0600 ~/.config/runwisp/webhook.url[[notifier]]id = "my-hook"type = "webhook"url = "${file:~/.config/runwisp/webhook.url}"The notifier accepts the literal URL directly. Avoid it — config files are often committed to git.
[[notifier]]id = "my-hook"type = "webhook"url = "https://example.com/hooks/abc123"2. Route failures to it
Section titled “2. Route failures to it”On one task
Section titled “On one task”[tasks.backup-postgres]cron = "30 2 * * *"notify_on_failure = ["my-hook"]run = "..."In a notification rule
Section titled “In a notification rule”[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"] }notify = ["my-hook"]See Per-task notifications and Notification rules for the full options.
3. Test it
Section titled “3. Test it”Trigger a task you know will fail:
runwisp exec smoke-testYour endpoint should receive a POST with Content-Type: application/json
within a few seconds. If only the in-app bell shows, delivery failed —
look for a notify.delivery_failed event in the bell for the reason.
What the payload looks like
Section titled “What the payload looks like”The default template produces a flat, machine-readable JSON object. Every field uses a stable key that’s safe to parse downstream.
{ "kind": "run.failed", "severity": "error", "timestamp": "2025-01-15T02:30:45Z", "task": "backup-postgres", "title": "backup-postgres failed", "message": "Exited with code 1 after 3m 4s.", "trigger": "Scheduled run", "run": { "id": "01JXYZ...", "exit_code": 1, "triggered_by": "cron", "url": "https://runwisp.example.com/tasks/backup-postgres/01JXYZ...", "duration": "3m 4s" }, "output_tail": "Error: connection refused\ndial tcp ...", "reason": "exit 1", "source": "runwisp (bright-falcon)"}run, output_tail, and reason are conditional — they appear only
when applicable. run.url and run.duration are also conditional
within the run object.
Customising the payload
Section titled “Customising the payload”Point template_path at a Go-template file. Copy
webhook.tmpl.json
as your starting point. The template receives the full event struct and
the same helpers available to other providers: statusEmoji,
statusVerb, eventSentence, eventTrigger, runURL, outputTail,
fingerprint, and more.
What the loader rejects
Section titled “What the loader rejects”- A missing or empty
url. - A
urlthat isn’thttp://orhttps://. - An empty key in
headers. - An
idcontaining:(reserved for inline target overrides) or equal to"inapp"(reserved).
Webhook notifiers do not support inline target overrides (like
my-hook:something) — there’s no natural “target” to override.