Notifications
z4j fans events out to six channel types. Per-project channels are configured by admins and shared with the team. Personal channels are configured by each user for their own subscriptions. Both share the same six triggers, the same filtering primitives, and the same dispatch pipeline.
Triggers
Section titled “Triggers”| Trigger | When it fires |
|---|---|
task.failed | A task raised an exception or exited non-zero |
task.succeeded | A task completed without error |
task.retried | A task is being retried after a failure |
task.slow | A task ran longer than its latency budget |
agent.offline | An agent missed heartbeats and is now considered offline |
agent.online | An agent reconnected after being offline |
Every subscription pins a single trigger plus an optional filter set (see Filters below).
Channel types
Section titled “Channel types”z4j ships six channel types. All six are project-scoped (admin) and user-scoped (personal); the schema is identical.
Webhook (generic HTTPS)
Section titled “Webhook (generic HTTPS)”POSTs a JSON envelope to any URL you control. Use this when none of the named integrations fit.
| Field | Required | Notes |
|---|---|---|
url | yes | https://.... Loopback and RFC1918 ranges are rejected. |
headers | no | Up to 20 custom headers, 1024 bytes each. Reserved headers (host, cookie, proxy-authorization, …) are blocked. |
hmac_secret | no | Any string. The body is signed with HMAC-SHA256 and the digest is sent as X-Z4J-Signature: sha256=<hex>. |
Verify on your side:
import hmac, hashlib
def verify(body: bytes, header: str, secret: str) -> bool: expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, header)Email (SMTP)
Section titled “Email (SMTP)”Plain SMTP with optional STARTTLS / implicit TLS. No OAuth.
| Field | Required | Notes |
|---|---|---|
smtp_host | yes | Hostname only. |
smtp_port | yes | One of 25, 465, 587, 2525. Other ports are rejected. |
smtp_user | yes | Username for AUTH PLAIN / LOGIN. |
smtp_pass | yes | Password (masked on GET, preserve on empty PATCH). |
smtp_tls | no | true (default) negotiates STARTTLS on 587/2525 or implicit TLS on 465. false is plaintext, allowed only for 25. |
from_addr | no | Defaults to smtp_user. |
to_addrs | yes | List of recipient addresses. |
Provider notes:
- Gmail / Google Workspace: use an app password, host
smtp.gmail.com, port587. - Mailgun: host
smtp.mailgun.org, port587, user is your domain SMTP credential. - Brevo (ex-Sendinblue): host
smtp-relay.brevo.com, port587.
Incoming webhook with Block Kit formatting. To get the URL: Slack workspace -> Apps -> Incoming Webhooks -> add to a channel -> copy the URL (https://hooks.slack.com/services/T.../B.../...).
| Field | Required | Notes |
|---|---|---|
webhook_url | yes | The full https://hooks.slack.com/services/... URL. |
The dispatcher posts a message that includes trigger, project slug, task name (when relevant), and a link back to the dashboard.
Telegram
Section titled “Telegram”Bot API sendMessage. Two values to obtain:
- Bot token - DM @BotFather, run
/newbot, follow the prompts. Token format:123456789:AAH.... - Chat ID - either a signed integer (group chats are negative, e.g.
-100123456789) or@channel_handle. The bot must be added to the chat for messages to deliver.
| Field | Required | Notes |
|---|---|---|
bot_token | yes | Pattern \d+:[A-Za-z0-9_-]+. |
chat_id | yes | Signed integer or @handle. |
To find a chat ID, send any message to your bot then GET https://api.telegram.org/bot<TOKEN>/getUpdates and read result[].message.chat.id.
PagerDuty
Section titled “PagerDuty”Events API v2. Open a PagerDuty service, Integrations -> Add integration -> choose Events API v2, copy the Integration Key (32 chars).
| Field | Required | Notes |
|---|---|---|
integration_key | yes | 32-character routing key. |
severity_default | no | One of critical / error / warning / info. Default warning. |
severity_map | no | Per-trigger override, e.g. {"agent.offline": "critical", "task.failed": "error"}. |
z4j picks a sensible severity per trigger out of the box, so most operators only need to paste the integration key. Repeat firings for the same (project, trigger, task_id) collapse into a single incident via PagerDuty’s dedup key.
Discord
Section titled “Discord”Server Settings -> Integrations -> Webhooks -> New Webhook -> copy the URL (https://discord.com/api/webhooks/<id>/<token>).
| Field | Required | Notes |
|---|---|---|
webhook_url | yes | The full Discord webhook URL. |
z4j POSTs Slack-compatible payloads to Discord’s /slack endpoint, so the formatting renders cleanly. The dispatcher auto-appends /slack to the URL you paste.
Filters
Section titled “Filters”Every subscription can narrow the firehose with these filters:
| Filter | What it does |
|---|---|
priority_min / priority_max | Only fire for tasks whose priority falls in the range. |
task_name_pattern | fnmatch glob, e.g. billing.* or *.deliver. |
queue | Only this queue. |
cooldown_seconds | Drop events that arrive within N seconds of the previous matching event. |
muted_until | Hard mute the subscription until a timestamp. |
z4j evaluates filters before dispatch, so a muted subscription costs nothing.
Test before saving
Section titled “Test before saving”Two preflight endpoints let you verify credentials without committing them. The dashboard exposes both as Test buttons in the channel create / edit dialogs.
POST /api/v1/projects/{slug}/notifications/channels/test{ "type": "slack", "config": { "webhook_url": "https://hooks.slack.com/services/..." }}A canned z4j.test payload is dispatched through the real channel. Response:
{ "success": true, "status_code": 200, "error": null, "response_body": "ok"}For a saved channel, hit /channels/{id}/test instead. Saved-channel tests for project channels write a row to the delivery audit log with trigger=test.dispatch; preflight tests on unsaved configs do not.
Limits and dispatch behavior
Section titled “Limits and dispatch behavior”| Knob | Default |
|---|---|
| Outbound concurrency | 16 deliveries per event batch |
| HTTP timeout | 10s overall, 5s connect |
| Response body cap | 8 KiB (excess is discarded) |
| DNS cache TTL | 30s with a 5s resolve timeout |
| Config size cap | 16 KiB JSON per channel |
| Headers per webhook | 20 max, 1024 bytes per value |
| Rate limiting | per-channel test endpoints are throttled per IP |
Failures are written to notification_deliveries with the HTTP status, sanitized error message, and (capped) response body, so you can debug without pulling logs.
Security
Section titled “Security”- SSRF protection: all dispatchers validate the URL, resolve the hostname, and pin the IP at send time so a DNS rebind between create and dispatch cannot redirect to a private range.
- Header injection: webhook custom headers are validated for control characters and reserved names.
- Secret masking:
smtp_pass,hmac_secret,bot_token,integration_key, etc. are returned as••••••••fromGET. OnPATCH, leaving a sensitive field empty preserves the stored value, so you can rename a channel without re-entering credentials. - Audit trail: every dispatch is logged with channel name, channel type, trigger, status, and response code. The audit log is HMAC-chained.
Common patterns
Section titled “Common patterns”Page on agent offline, otherwise just chat
Section titled “Page on agent offline, otherwise just chat”Two channels, two subscriptions:
- PagerDuty channel scoped to
agent.offline(severitycritical). - Slack channel scoped to
task.failedwithcooldown_seconds: 300so a flood of failures from one bad deploy posts at most once every five minutes.
Per-user mute during oncall handoff
Section titled “Per-user mute during oncall handoff”Each user manages their own subscriptions under My account -> Notifications. Setting muted_until on a personal subscription stops their phone from buzzing while a teammate is on duty without affecting anyone else.
Slow-task budget alert
Section titled “Slow-task budget alert”Subscribe to task.slow with task_name_pattern: report.* to catch the long-running aggregations without alerting on every quick task that ticks the threshold.
API reference
Section titled “API reference”The full notification API (channels, subscriptions, defaults, deliveries, in-app inbox) is documented under API reference -> Tasks and adjacent endpoints. CRUD verbs follow the same shape as projects and memberships.