The notification system in django-forms-workflows provides flexible, event-driven email notifications throughout the lifecycle of a form submission. All notification configuration lives in a single model — NotificationRule — giving administrators a unified interface for controlling who receives which emails, when, and under what conditions.
Form Submission Lifecycle
─────────────────────────
Submitted ──► Stage 1 activates ──► Stage 1 completes ──► Stage 2 ──► Final Decision
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
submission approval_request stage_decision approval_request workflow_approved
_received workflow_denied
form_withdrawn
All notifications are dispatched as Celery tasks (asynchronous when a broker is available, synchronous fallback otherwise) and are configured through a single model:
| Model | Description |
|---|---|
NotificationRule |
One rule = one event + one set of recipient sources + optional conditions. Attach to a workflow, optionally scoped to a specific stage. |
Each rule answers three questions:
- When? — The
eventfield selects which lifecycle event triggers the notification. - Who? — Six additive recipient sources determine who receives the email.
- Under what conditions? — Optional
conditionsJSON evaluated againstform_data.
| Field | Type | Description |
|---|---|---|
workflow |
ForeignKey | The parent WorkflowDefinition (required) |
stage |
ForeignKey | Optional. Scopes recipient sources to a specific stage |
event |
Choice | Which lifecycle event triggers this rule |
conditions |
JSON | Optional conditions evaluated against form_data |
subject_template |
CharField | Custom subject line (supports {form_name}, {submission_id}, and any {field_name} token — see Answer Piping) |
notify_submitter |
Boolean | Include the form submitter |
email_field |
CharField | Form field slug whose value is an email address |
static_emails |
CharField | Comma-separated fixed email addresses |
notify_stage_assignees |
Boolean | Include dynamically-assigned approvers |
notify_stage_groups |
Boolean | Include all users in stage approval groups |
notify_groups |
M2M → Group | Additional groups to notify (independent of stages) |
The stage field controls the scope of stage-aware recipient sources:
stage value |
notify_stage_assignees behavior |
notify_stage_groups behavior |
|---|---|---|
| null (workflow-level) | Includes assignees from all stages | Includes users from all stages' groups |
| set (stage-level) | Includes only that stage's assignee | Includes only that stage's groups |
This gives you both "notify everyone" and "notify selectively" without separate models.
| Event Key | When It Fires | Description |
|---|---|---|
submission_received |
Form is first submitted | The initial submission event |
approval_request |
A workflow stage activates | A new approval task has been created |
stage_decision |
An individual stage completes | All tasks in a stage are resolved |
workflow_approved |
Final approval | The entire workflow is approved |
workflow_denied |
Final rejection | The submission is rejected |
form_withdrawn |
Submitter withdraws | The submitter cancels their submission |
| Event | Template |
|---|---|
submission_received |
emails/submission_notification.html |
approval_request |
emails/approval_request.html |
stage_decision |
emails/approval_notification.html |
workflow_approved |
emails/approval_notification.html |
workflow_denied |
emails/rejection_notification.html |
form_withdrawn |
emails/withdrawal_notification.html |
| Batched digest | emails/notification_digest.html |
Templates receive submission, submission_url, approval_url, hide_approval_history, and (where applicable) task in their context.
All recipient sources are combined additively and deduplicated. For a given rule:
notify_submitter→submission.submitter.emailemail_field→ The value ofform_data[slug](dynamic per submission)static_emails→ Comma-separated fixed addressesnotify_stage_assignees→ApprovalTask.assigned_to.emailfor dynamically-assigned usersnotify_stage_groups→ All users in the stage'sStageApprovalGroup→Group→User.emailnotify_groups→ All users in the explicitly-listed M2M groups
At least one recipient source must be configured per rule (validated by clean()).
"When approved or rejected, the submitter should get an email."
Create two NotificationRule records:
event=workflow_approved,notify_submitter=Trueevent=workflow_denied,notify_submitter=True
"The form has an 'advisor_email' field. Notify the advisor on submission and final approval."
Create two rules:
event=submission_received,email_field=advisor_emailevent=workflow_approved,email_field=advisor_email
"Stage 1 assigns a reviewer by full name. That reviewer should be notified when the workflow finishes."
Create one stage-scoped rule:
event=workflow_approved,stage=Stage 1,notify_stage_assignees=True
The reviewer's email comes from the Django User record resolved during dynamic assignment. No separate email field needed.
"Only notify the dean if the department is 'Graduate Studies' AND the request amount exceeds $5,000."
Create one rule:
event=workflow_approved,static_emails=dean@example.educonditions:{ "operator": "AND", "conditions": [ {"field": "department", "operator": "equals", "value": "Graduate Studies"}, {"field": "amount", "operator": "gt", "value": "5000"} ] }
"Three stages: Department Chair, HR Review, VP Approval. Only the Chair and VP should be notified on final decision — not HR."
Create two stage-scoped rules:
event=workflow_approved,stage=Department Chair,notify_stage_assignees=Trueevent=workflow_approved,stage=VP Approval,notify_stage_assignees=True
HR is excluded because there is no rule scoping to the HR stage. Compare this to a single workflow-level rule with notify_stage_assignees=True (no stage set), which would include all stages' assignees.
"The Finance Committee group reviews Stage 2. Everyone in that group should be emailed when the workflow is approved."
Create one stage-scoped rule:
event=workflow_approved,stage=Stage 2,notify_stage_groups=True
All users in Stage 2's approval groups who have an email address will receive the notification.
"The 'Executive Team' group should be notified whenever a high-value request is approved, regardless of which stages they're involved in."
Create one rule:
event=workflow_approved,notify_groups=[Executive Team]conditions:{"field": "amount", "operator": "gt", "value": "50000"}
"When Stage 1 (Manager Review) completes, notify the submitter so they know their form is progressing."
Create one rule:
event=stage_decision,stage=Manager Review,notify_submitter=True
"Instead of individual emails, send the registrar a daily summary."
- On the
WorkflowDefinition: setnotification_cadence=daily,notification_cadence_time=08:00 - Create a rule:
event=submission_received,static_emails=registrar@example.edu
All submissions are batched into a single digest email sent at 8:00 AM.
Workflows can opt into digest-style batching via WorkflowDefinition.notification_cadence:
| Cadence | Behavior |
|---|---|
immediate |
Each notification fires as a separate email right away (default) |
daily |
Queued and sent as a single digest once per day |
weekly |
Queued and sent once per week |
monthly |
Queued and sent once per month |
form_field_date |
Queued and sent on the date specified in a form field |
Additional settings: notification_cadence_day, notification_cadence_time, notification_cadence_form_field.
Queued notifications are stored in PendingNotification and dispatched by the send_batched_notifications periodic task.
The hide_approval_history flag on WorkflowDefinition controls whether notification emails include the full approval history (approver names and comments). When enabled, the submitter only sees the final decision.
| Backend | Setting | Use Case |
|---|---|---|
| Console | django.core.mail.backends.console.EmailBackend |
Development |
| SMTP | django.core.mail.backends.smtp.EmailBackend |
Traditional SMTP relay |
| Gmail API | django_forms_workflows.email_backends.GmailAPIBackend |
Google Workspace |
For Gmail API, also set DEFAULT_FROM_EMAIL, GMAIL_DELEGATED_USER, and GMAIL_SERVICE_ACCOUNT_KEY_BASE64.
- Check
EMAIL_BACKENDis not console in production - Check Celery is running (falls back to sync but verify)
- Check that at least one recipient resolves (no recipients = silently skipped)
- Check
notify_stage_assignees=Trueon the relevant rule - Check the stage has
assignee_form_fieldconfigured - Check the resolved
Userhas a populatedemailfield
- Check
notify_stage_groups=Trueornotify_groupsis set - Check users in the group have non-empty
emailfields
- Check field names in conditions match form field slugs exactly
- Valid operators:
equals,not_equals,gt,lt,gte,lte,contains,in
- Check
send_batched_notificationsis in the Celery beat schedule - Check
PendingNotificationrecords are being created
subject_template supports answer piping — you can embed submitted form field values directly into the email subject line using {field_name} tokens.
{field_name}
field_name must match the field slug on FormField exactly (case-sensitive).
| Token | Source |
|---|---|
{form_name} |
The form's display name |
{submission_id} |
The numeric ID of the submission |
{<any field slug>} |
The submitted value for that field |
| Scenario | Result |
|---|---|
| Known field, scalar value | Replaced with the submitted value |
| Known field, list value (e.g. checkboxes) | Values joined with ", " |
| Unknown field slug | Replaced with "" (empty string, fail-open) |
# Simple field piping
Subject: {form_name} — New request from {full_name}
# Multiple fields
Subject: #{submission_id}: {department} request approved — {position_title}
# List field
Subject: Courses selected: {course_selections}
In addition to subject piping, the full form_data dictionary is now passed to every notification email template as form_data. This allows HTML templates to reference field values directly:
<!-- emails/approval_request.html -->
<p>Request from: <strong>{{ form_data.full_name }}</strong></p>
<p>Department: {{ form_data.department }}</p>
<p>Amount requested: ${{ form_data.amount }}</p>NotificationRule replaces three older mechanisms:
| Legacy | Migrated To |
|---|---|
WorkflowNotification |
NotificationRule (stage=null) |
StageFormFieldNotification |
NotificationRule (stage=set) |
WorkflowStage.notify_assignee_on_final_decision |
NotificationRule with notify_stage_assignees=True |
Data migration 0075_migrate_to_notification_rules automatically converts all existing records. The legacy models are retained for backward compatibility but will be removed in a future release.