Skip to content

Notifications

Notifications are a consumer of domain events, never a concern of the workflow engine. When the engine records a fact it emits an event; the notification router subscribes to those events and hands them to providers, which build and deliver messages.

Workflow Event ──► Notification Router ──► Provider ──► Email / Webhook

This keeps the engine decoupled: it neither knows about email addresses nor third-party services. Adding a channel is purely a routing concern.

Providers

A provider implements a single method:

from workflow_kit.notifications import NotificationProvider

class MyProvider(NotificationProvider):
    def send(self, event) -> bool:
        ...
        return True

send receives the raw event and returns True on success. Providers that cannot deliver should raise a NotificationError.

Email (EmailProvider)

Uses Django's email infrastructure (django.core.mail) through the modern MAILERS configuration introduced in Django 6.1, so it works with the console mailer in development, the SMTP mailer in production and the locmem mailer in tests. The mailer's backend is chosen by the host project's MAILERS setting; the package never touches EMAIL_BACKEND. No third-party dependency is required.

from workflow_kit.notifications import EmailProvider

EmailProvider(
    recipients=lambda event: ["owner@example.com"],   # or a static list
)

Subject and body can be overridden with callables that receive the event:

EmailProvider(
    recipients=["owner@example.com"],
    subject_fn=lambda event: f"Approval for {event.object.display}",
    body_fn=lambda event: f"Workflow {event.workflow} changed state.",
)

Webhook (WebhookProvider)

Posts a structured JSON payload to WORKFLOW_KIT["WEBHOOK_URL"]. Every request is signed with an HMAC-SHA256 signature over timestamp.event_id.body, sent in the X-Domain-Signature header together with X-Domain-Event, X-Domain-Event-Id and X-Domain-Timestamp, so receivers can authenticate the sender and detect tampering or replay.

The payload contains only the public event fields — never the full business object, credentials or internal state.

Routing events to providers

A NotificationRouter subscribes to the event dispatcher and forwards matching events:

from workflow_kit.notifications import NotificationRouter

router = NotificationRouter()
router.register(email_provider, event_type="workflow.completed")
router.register(webhook_provider, event_type="*")
router.install()
  • install() subscribes every registered (event_type, provider) pair.
  • uninstall() removes the subscriptions.
  • clear() unsubscribes and drops all registrations.
  • Provider failures are isolated and logged — they never break the workflow.

Configuration

Notifications are configured through the WORKFLOW_KIT dict in Django settings. Defaults are provided, so basic usage needs no configuration. Email delivery uses the host project's MAILERS setting (Django >= 6.1); the mailer alias is selected with EMAIL_MAILER:

MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {"host": "smtp.example.com"},
    },
}

WORKFLOW_KIT = {
    "NOTIFICATIONS_ENABLED": True,
    "EMAIL_NOTIFICATIONS_ENABLED": True,
    "EMAIL_MAILER": "default",          # which MAILERS alias to use
    "EMAIL_FROM": "no-reply@example.com",   # falls back to DEFAULT_FROM_EMAIL
    "EMAIL_SUBJECT_PREFIX": "[Workflow] ",
    "WEBHOOK_NOTIFICATIONS_ENABLED": False,
    "WEBHOOK_URL": "https://hooks.example.com/workflow",
    "WEBHOOK_SECRET": "choose-a-secret",
}

Webhooks are disabled by default. WEBHOOK_SECRET signs every request; when empty no signature header is added (not recommended for sensitive payloads).

Delivery guarantees

Notification delivery follows the event system's semantics: synchronous, best-effort, in-process. There is no retry policy, queue or dead-letter mechanism yet, and the package does not claim guaranteed delivery. A failing handler is logged and swallowed, so notifications never corrupt workflow state. For production-grade delivery (retries, idempotency, exactly-once), run delivery in an application-level worker (e.g. Celery) consuming the same events.

Demo

The Invoice Approval example wires an [EVENT] console logger and an email notification on workflow completion. With the console mailer (MAILERS) it prints one email; with SMTP configured it delivers the real message. This wiring lives in the demo (invoices/events.py), not in the package.

See also

  • Events: the stream notifications consume.