Ofelia supports sending webhook notifications when jobs complete. You can configure multiple named webhooks and assign them to specific jobs, allowing flexible notification routing.
[global]
; Global webhook settings (optional)
webhook-allow-remote-presets = false
[webhook "slack-alerts"]
preset = slack
id = T00000000/B00000000000
secret = XXXXXXXXXXXXXXXXXXXXXXXX
trigger = error
[job-exec "backup-database"]
schedule = @daily
container = postgres
command = pg_dump -U postgres mydb > /backup/db.sql
webhooks = slack-alertsservices:
ofelia:
image: mcuadros/ofelia:latest
labels:
# Define a webhook
ofelia.webhook.slack-alerts.preset: slack
ofelia.webhook.slack-alerts.id: "T00000000/B00000000000"
ofelia.webhook.slack-alerts.secret: "XXXXXXXXXXXXXXXXXXXXXXXX"
ofelia.webhook.slack-alerts.trigger: error
# Assign webhook to a job
ofelia.job-exec.backup.schedule: "@daily"
ofelia.job-exec.backup.container: postgres
ofelia.job-exec.backup.command: "pg_dump -U postgres mydb > /backup/db.sql"
ofelia.job-exec.backup.webhooks: slack-alertsOfelia includes presets for popular notification services:
| Preset | Service | Required Variables |
|---|---|---|
slack |
Slack Incoming Webhooks | id, secret |
discord |
Discord Webhooks | id, secret |
teams |
Microsoft Teams | url |
matrix |
Matrix (via hookshot bridge) | url |
ntfy |
ntfy.sh (public topics) | id (topic) |
ntfy-token |
ntfy.sh (with Bearer auth) | id (topic), secret (access token) |
pushover |
Pushover | id (user key), secret (API token) |
pagerduty |
PagerDuty Events API v2 | secret (routing key) |
gotify |
Gotify | url, secret (app token) |
| Option | Type | Description |
|---|---|---|
preset |
string | Preset name (bundled or remote) |
url |
string | Custom webhook URL (overrides preset URL) |
id |
string | Service-specific identifier |
secret |
string | Service-specific secret/token |
link |
string | Optional URL to include in notification (e.g., link to logs) |
link-text |
string | Display text for link (default: "View Details") |
trigger |
string | When to send: always, error, success, skipped (default: error) |
timeout |
duration | HTTP request timeout (default: 30s) |
retry-count |
int | Number of retries on failure (default: 3) |
retry-delay |
duration | Delay between retries (default: 5s) |
Assign webhooks to jobs using the webhooks option:
[job-exec "my-job"]
schedule = @hourly
container = myapp
command = /run-task.sh
webhooks = slack-alerts, discord-notifyMultiple webhooks can be assigned (comma-separated).
[global]
; Allow fetching presets from remote URLs
webhook-allow-remote-presets = false
; Cache TTL for remote presets
webhook-preset-cache-ttl = 24h[webhook "slack-alerts"]
preset = slack
id = T00000000/B00000000000
secret = XXXXXXXXXXXXXXXXXXXXXXXX
trigger = errorThe id is your workspace/channel identifier and secret is the webhook token from your Slack Incoming Webhook URL:
https://hooks.slack.com/services/{id}/{secret}
[webhook "discord-notify"]
preset = discord
id = 1234567890123456789
secret = abcdefghijklmnopqrstuvwxyz1234567890ABCDEF
trigger = alwaysFrom your Discord webhook URL: https://discord.com/api/webhooks/{id}/{secret}
[webhook "teams-alerts"]
preset = teams
url = https://outlook.office.com/webhook/your-webhook-url
trigger = error[webhook "matrix-alerts"]
preset = matrix
url = https://matrix.example.com/hookshot/webhooks/webhook/your-webhook-id
trigger = error
link = https://logs.example.com/ofelia
link-text = View LogsThe Matrix preset works with the matrix-hookshot bridge. Create a webhook in your Matrix room and use the full webhook URL.
The optional link and link-text fields add a clickable link to your notifications, useful for linking to log dashboards or job details.
For public topics on ntfy.sh (no authentication):
[webhook "ntfy-notify"]
preset = ntfy
id = my-topic-name
trigger = alwaysFor private topics or self-hosted ntfy with access tokens:
[webhook "ntfy-private"]
preset = ntfy-token
id = my-private-topic
secret = tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
trigger = alwaysFor self-hosted ntfy with custom URL and authentication:
[webhook "ntfy-self-hosted"]
preset = ntfy-token
url = https://ntfy.example.com/my-topic
secret = tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
trigger = alwaysNote: Use
ntfyfor public topics without authentication, andntfy-tokenwhen Bearer token authentication is required (self-hosted instances with access control or private topics on ntfy.sh).
[webhook "pushover-alerts"]
preset = pushover
id = user-key-here
secret = api-token-here
trigger = error[webhook "pagerduty-oncall"]
preset = pagerduty
secret = routing-key-here
trigger = error[webhook "gotify-notify"]
preset = gotify
url = https://gotify.example.com
secret = app-token-here
trigger = alwaysYou can configure webhooks without a preset by providing a URL directly:
[webhook "custom-hook"]
url = https://api.example.com/webhook
trigger = always
timeout = 10s
retry-count = 2This sends a JSON payload with job execution data to the specified URL.
Security Warning: Remote presets execute templates that could potentially exfiltrate data. Only enable this feature if you trust the preset sources.
Enable remote presets in global settings:
[global]
webhook-allow-remote-presets = true
webhook-preset-cache-ttl = 24hReference presets from GitHub using shorthand notation:
[webhook "custom-service"]
; Loads from github.com/user/repo/blob/main/presets/custom.yaml
preset = gh:user/repo/presets/custom.yaml[webhook "custom-service"]
preset = https://raw.githubusercontent.com/user/repo/main/presets/custom.yamlWebhook body templates have access to the following data:
| Variable | Type | Description |
|---|---|---|
.Job.Name |
string | Job name |
.Job.Command |
string | Executed command |
.Job.Schedule |
string | Cron schedule |
.Job.Container |
string | Container name (if applicable) |
| Variable | Type | Description |
|---|---|---|
.Execution.Status |
string | successful, failed, or skipped |
.Execution.Failed |
bool | Whether execution failed |
.Execution.Skipped |
bool | Whether execution was skipped |
.Execution.Error |
string | Error message (if failed) |
.Execution.Duration |
duration | Execution duration |
.Execution.StartTime |
time.Time | When execution started |
.Execution.EndTime |
time.Time | When execution ended |
| Variable | Type | Description |
|---|---|---|
.Host.Hostname |
string | Machine hostname |
.Host.Timestamp |
time.Time | Current timestamp |
| Variable | Type | Description |
|---|---|---|
.Ofelia.Version |
string | Ofelia version |
| Variable | Type | Description |
|---|---|---|
.Preset.ID |
string | Configured ID value |
.Preset.Secret |
string | Configured secret value |
.Preset.URL |
string | Configured URL value |
.Preset.Link |
string | Configured link URL (empty if not set) |
.Preset.LinkText |
string | Configured link text (defaults to "View Details") |
Templates support these helper functions:
| Function | Description | Example |
|---|---|---|
json |
JSON-escape a string | {{json .Execution.Error}} |
truncate |
Limit string length | {{truncate 100 .Execution.Error}} |
isoTime |
Format time as ISO 8601 | {{isoTime .Host.Timestamp}} |
unixTime |
Format time as Unix timestamp | {{unixTime .Host.Timestamp}} |
formatDuration |
Format duration as string | {{formatDuration .Execution.Duration}} |
Custom presets use YAML format:
name: my-service
description: "My custom notification service"
version: "1.0.0"
url_scheme: "https://api.myservice.com/notify/{id}"
method: POST
headers:
Content-Type: "application/json"
Authorization: "Bearer {secret}"
variables:
id:
description: "Service ID"
required: true
secret:
description: "API token"
required: true
sensitive: true
body: |
{
"title": "Job {{.Job.Name}} {{.Execution.Status}}",
"message": "{{if .Execution.Failed}}Error: {{.Execution.Error}}{{else}}Completed in {{.Execution.Duration}}{{end}}",
"timestamp": "{{isoTime .Host.Timestamp}}"
}| Field | Type | Description |
|---|---|---|
name |
string | Preset identifier |
description |
string | Human-readable description |
version |
string | Preset version |
url_scheme |
string | URL template with {id}, {secret} placeholders |
method |
string | HTTP method (GET, POST, etc.) |
headers |
map | HTTP headers (supports {secret} placeholder) |
variables |
map | Variable definitions with validation |
body |
string | Go template for request body |
Ofelia includes Server-Side Request Forgery (SSRF) protection that blocks webhooks to:
- Localhost and loopback addresses (
127.0.0.1,::1,localhost) - Private networks (
10.x.x.x,172.16-31.x.x,192.168.x.x) - Link-local addresses (
169.254.x.x) - Cloud metadata endpoints (
169.254.169.254,metadata.google.internal) - Internal hostnames (
.local,.internal,.corp)
- Keep secrets secure: Use environment variables or secret management for webhook credentials
- Use HTTPS: Always use HTTPS URLs for production webhooks
- Limit remote presets: Keep
webhook-allow-remote-presets = falseunless necessary - Audit presets: Review remote preset sources before enabling them
If you're using the deprecated slack-webhook option, migrate to the new webhook system:
[job-exec "my-job"]
schedule = @hourly
container = myapp
command = /run-task.sh
slack-webhook = https://hooks.slack.com/services/TXXXX/BXXXX/your-secret-here
slack-only-on-error = true[webhook "slack"]
preset = slack
id = T00000000/B00000000000
secret = XXXXXXXXXXXXXXXXXXXXXXXX
trigger = error
[job-exec "my-job"]
schedule = @hourly
container = myapp
command = /run-task.sh
webhooks = slackThe deprecated slack-webhook option will continue to work but will show a deprecation warning. It will be removed in a future version.
- Check the
triggersetting matches your expected condition - Verify the webhook is assigned to the job with
webhooks = webhook-name - Check Ofelia logs for webhook errors
- Verify
idandsecretvalues are correct - Check if the service requires additional authentication headers
- Try using a custom
urlto bypass preset URL construction
Increase the timeout for slow services:
[webhook "slow-service"]
preset = slack
timeout = 60s
retry-count = 5
retry-delay = 10sIf you need to send webhooks to internal services, consider:
- Using a webhook relay service
- Running Ofelia with custom SSRF allowlists (requires code modification)
- Setting up a proxy that forwards to internal services