This Django app provides a flexible notification delivery system for sending alerts and messages to various platforms.
It abstracts the complexity of multiple notification backends (email, Slack, PagerDuty, etc.) behind a simple, unified interface. Drivers handle platform-specific logic and configuration.
See Architecture for how this app fits in the pipeline (NOTIFY stage).
NotificationChannel— Persistent configuration for notification channels (managed via Django Admin)NotificationSeverity— Severity levels for notifications (critical,warning,info,success)
Drivers live in apps/notify/drivers/ and are responsible for:
- validating driver configuration (
validate_config()) - sending notifications to their backend (
send()) - normalizing results into a common format
Built-in drivers:
email— SMTP email notificationsslack— Slack workspace integration via webhookspagerduty— PagerDuty incident creationgeneric— flexible fallback for custom integrations
Send a test notification to verify driver or channel configuration.
Default mode is interactive — a wizard guides you through channel selection, message options, and lets you retry or switch channels without restarting the command.
# Launch the interactive wizard (default)
uv run python manage.py test_notify$ manage.py test_notify
=== Test Notification Wizard ===
Select a channel:
1. ops-slack (slack)
2. oncall-email (email)
3. Configure a new driver manually
Enter choice: 1
Title [Test Alert]:
Message [This is a test notification from the notify app.]:
Severity (critical/warning/info/success) [info]: warning
Sending test notification via ops-slack (provider=slack)...
Notification sent successfully!
Message ID: abc-123
What next?
1. Retry with different message options
2. Switch to a different channel
3. Done — exit
Enter choice: 3
Key behaviors:
- Channel discovery — lists active
NotificationChannelrecords from the database - Configure new — prompts for driver type and driver-specific config fields
- Defaults in brackets — press Enter to accept, type to override
- Adjust loop — retry with different title/message/severity or switch channels
Use --non-interactive for CI/scripting or when you prefer CLI flags:
# Test using first active DB channel
uv run python manage.py test_notify --non-interactive
# Test a specific driver with flags
uv run python manage.py test_notify slack --non-interactive \
--webhook-url https://hooks.slack.com/services/T.../B.../XXX
# Test a named DB channel
uv run python manage.py test_notify ops-slack --non-interactive
# Custom message
uv run python manage.py test_notify slack --non-interactive \
--webhook-url https://hooks.slack.com/services/T.../B.../XXX \
--title "Deploy Alert" \
--message "Deployment started" \
--severity warning
# JSON config (advanced)
uv run python manage.py test_notify slack --non-interactive \
--json-config '{"webhook_url": "https://hooks.slack.com/...", "channel": "#alerts"}'| Flag | Type | Default | Description |
|---|---|---|---|
--non-interactive |
flag | — | Skip wizard, use CLI flags |
driver (positional) |
str | first active channel | Driver name or DB channel name |
--title |
str | Test Alert |
Notification title |
--message |
str | default test message | Notification body |
--severity |
choice | info |
critical, warning, info, or success |
--channel |
str | default |
Destination channel/recipient |
--json-config |
str | — | Full driver config as JSON string |
--smtp-host |
str | — | SMTP host (email driver) |
--smtp-port |
int | 587 |
SMTP port (email driver) |
--from-address |
str | — | Sender address (email driver) |
--use-tls |
flag | — | Enable TLS for SMTP (email driver) |
--webhook-url |
str | — | Webhook URL (slack driver) |
--integration-key |
str | — | Integration key (pagerduty driver) |
--endpoint |
str | — | API endpoint (generic driver) |
--api-key |
str | — | API key (generic driver) |
Notification channels are managed via the NotificationChannel model in Django Admin. Each channel has an is_active flag — set it to False to disable a channel without deleting its configuration.
Pipeline definitions control which drivers are used per-pipeline via the drivers config in notify nodes.
The app exposes REST endpoints for sending notifications:
POST /notify/send/— Send notification (specify driver in payload)POST /notify/send/<driver>/— Send notification via specific driverPOST /notify/batch/— Send multiple notifications in one requestGET /notify/drivers/— List available driversGET /notify/drivers/<driver>/— Get specific driver info
Access the admin interface at /admin/notify/ to manage notification channels:
- NotificationChannel — Create, edit, and disable notification channels
- Configure driver type and settings
- Store webhook URLs, API keys (as references), and other driver-specific config
- Enable/disable channels without deleting them
NotificationChannel— persistent channel configuration (driver + config)NotificationMessage— standardized message format with title, message, severity, and metadataBaseNotifyDriver— abstract base for all notification drivers
The recommended approach is to configure channels via Django Admin:
- Navigate to
/admin/notify/notificationchannel/ - Add a new channel (e.g., "ops-slack")
- Select the driver type (e.g., "slack")
- Configure driver-specific settings in the JSON config field
- Set
is_active=Trueto enable
Example channel config for Slack:
{
"webhook_url": "https://hooks.slack.com/services/T.../B.../XXX",
"channel": "#alerts",
"username": "UserName",
"icon_emoji": ":rotating_light:",
"timeout": 30
}{
"smtp_host": "smtp.example.com",
"smtp_port": 587,
"from_address": "alerts@example.com",
"to_addresses": ["ops@example.com"],
"use_tls": true,
"use_ssl": false,
"username": "user@example.com",
"password": "app-password",
"timeout": 30
}{
"integration_key": "your-pagerduty-integration-key",
"dedup_key": "optional-deduplication-key",
"event_action": "trigger",
"client": "Server Maintenance",
"client_url": "https://your-dashboard.com",
"timeout": 30
}{
"endpoint": "https://api.example.com/notify",
"method": "POST",
"headers": {
"Authorization": "Bearer your-api-key",
"X-Custom-Header": "value"
},
"timeout": 30,
"payload_template": {
"alert": "{title}",
"body": "{message}",
"level": "{severity}"
}
}# Interactive wizard (recommended) — discovers channels, lets you retry
python manage.py test_notify
# Non-interactive (CI/scripting)
python manage.py test_notify slack --non-interactive \
--webhook-url https://hooks.slack.com/services/T00000000/B00000000/XXXXXXX
python manage.py test_notify email --non-interactive \
--smtp-host smtp.gmail.com \
--from-address alerts@example.comfrom apps.notify.drivers.base import NotificationMessage
message = NotificationMessage(
title="CPU Alert",
message="CPU usage exceeded 90% threshold",
severity="critical",
channel="devops-alerts",
tags={"environment": "production", "service": "api"},
context={"current_usage": 95.2, "threshold": 90},
)from apps.notify.drivers.slack import SlackNotifyDriver
driver = SlackNotifyDriver()
config = {
"webhook_url": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXX",
"channel": "#alerts"
}
result = driver.send(message, config)
print(result)
# {
# "success": True,
# "message_id": "slack_...",
# "metadata": {"channel": "#alerts", ...}
# }Selection priority when invoking notifications from the orchestration pipeline or management commands
- If the pipeline payload's
notify_drivermatches aNotificationChannel.namein the database (and that channel isis_active=True), the channel's storeddriverandconfigwill be used (this allows choosing named channels configured via Admin). - If no
notify_driveris provided in the payload, the orchestration layer will select the first activeNotificationChannelordered by name and use its driver and config. - If neither of the above applies, the orchestration pipeline treats
notify_driveras a driver key (for example,slack,email,generic) and uses the providednotify_configfrom the payload or default behavior.
This ordering lets you prefer centrally-managed channels (via Admin) while still allowing ad-hoc driver usage from scripts and management commands.
from apps.notify.drivers.email import EmailNotifyDriver
driver = EmailNotifyDriver()
config = {
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"from_address": "alerts@example.com",
"use_tls": True,
"username": "user@example.com",
"password": "app-password",
}
result = driver.send(message, config)from apps.notify.drivers.pagerduty import PagerDutyNotifyDriver
driver = PagerDutyNotifyDriver()
config = {
"integration_key": "your-pagerduty-integration-key",
}
result = driver.send(message, config)- Create a new file in
apps/notify/drivers/(e.g.,myservice.py) - Subclass
BaseNotifyDriver - Implement
validate_config()andsend()methods - Add to
apps/notify/drivers/__init__.py
Example:
from apps.notify.drivers.base import BaseNotifyDriver, NotificationMessage
class MyServiceNotifyDriver(BaseNotifyDriver):
name = "myservice"
def validate_config(self, config):
return "api_key" in config
def send(self, message, config):
# Your implementation here
return {
"success": True,
"message_id": "...",
"metadata": {...}
}from apps.notify.models import NotificationChannel
from apps.notify.drivers.slack import SlackNotifyDriver
from apps.notify.drivers.base import NotificationMessage
# Get an active channel by name
channel = NotificationChannel.objects.get(name="ops-slack", is_active=True)
# Create message
message = NotificationMessage(
title="CPU Alert",
message="CPU usage exceeded 90%",
severity="critical",
)
# Get driver and send
driver = SlackNotifyDriver() # Or use a registry to get driver by channel.driver
result = driver.send(message, channel.config)- Drivers are stateless and thread-safe
- Channel configuration is stored in
NotificationChannel, passed to drivers at send time - All drivers normalize to the same result format
- Failures return structured error information (captured by orchestrator)
- Each driver independently handles retries, rate-limiting, etc.
- No separate notification logs — the orchestration layer tracks all delivery attempts
# Send via Slack
curl -X POST http://localhost:8000/notify/send/slack/ \
-H "Content-Type: application/json" \
-d '{
"title": "High CPU Alert",
"message": "CPU usage exceeded 90%",
"severity": "critical",
"channel": "#alerts",
"config": {
"webhook_url": "https://hooks.slack.com/services/T.../B.../XXX"
}
}'
# Send via Email
curl -X POST http://localhost:8000/notify/send/email/ \
-H "Content-Type: application/json" \
-d '{
"title": "Database Alert",
"message": "Connection pool exhausted",
"severity": "warning",
"config": {
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"from_address": "alerts@example.com",
"to_addresses": ["ops@example.com"],
"use_tls": true,
"username": "user@example.com",
"password": "app-password"
}
}'
# Send via PagerDuty
curl -X POST http://localhost:8000/notify/send/pagerduty/ \
-H "Content-Type: application/json" \
-d '{
"title": "Service Down",
"message": "API server is not responding",
"severity": "critical",
"tags": {"service": "api", "environment": "production"},
"config": {
"integration_key": "your-pagerduty-integration-key"
}
}'curl -X POST http://localhost:8000/notify/batch/ \
-H "Content-Type: application/json" \
-d '{
"notifications": [
{
"driver": "slack",
"title": "Alert 1",
"message": "First notification",
"severity": "warning",
"config": {"webhook_url": "https://hooks.slack.com/..."}
},
{
"driver": "email",
"title": "Alert 2",
"message": "Second notification",
"severity": "info",
"config": {"smtp_host": "smtp.example.com", "from_address": "alerts@example.com"}
}
]
}'# List all drivers
curl http://localhost:8000/notify/drivers/
# Get specific driver info
curl http://localhost:8000/notify/drivers/slack/Success response:
{
"status": "success",
"driver": "slack",
"message_id": "slack_1a2b3c4d",
"metadata": {
"channel": "#alerts",
"severity": "critical"
}
}Error response:
{
"status": "error",
"driver": "slack",
"message": "Invalid Slack configuration (valid webhook_url required)"
}Batch response:
{
"status": "partial",
"total": 3,
"success_count": 2,
"error_count": 1,
"results": [
{"index": 0, "success": true, "driver": "slack", "message_id": "..."},
{"index": 1, "success": true, "driver": "email", "message_id": "..."},
{"index": 2, "success": false, "driver": "pagerduty", "error": "..."}
]
}