Status Update Emails
Audience: Engineers configuring status-update triggers and sweep cadence; operations staff managing email drafts.
What you will accomplish: Understand the 8-step status-update workflow — from change detection to outcome recording — including dual triggers, QC gates, and idempotency.
Prerequisites: Read Workflow Engine first.
Estimated time: ~8 minutes.
Detect matter status changes, draft context-aware update emails, and route them for one-click human approval before sending. Never let a client wonder where their case stands.
Trigger paths
Two paths converge on the same workflow run, deduplicated by change_id:
- Webhook:
matter.status_changedevent from the case management system - Sweep: Periodic
sweep_matters()compares last-known status vs current case system state
Both derive the same change_id for the same transition → exactly one run per actual status change.
Workflow steps
Step 1: detect_change
Check if the new status is in the trigger allow-list. If not, skip silently — no notification sent, no run created.
Allow-list filterStep 2: compute_delta
Persist the last-known status for this matter so future sweeps can detect changes. Write is idempotent (same change_id = no-op).
State store — write (confirm)Step 3: resolve_recipients
Derive email recipients from matter.participants. Default = client contact only. Missing email → blocking gap.
Recipient resolutionStep 4: draft_email
Compose a context-aware, on-brand status update using the status_update_email prompt template. Prompt version recorded on the run for traceability.
email.draft — draft onlyStep 5: qc_recipient_integrity
Run QC verification on the draft: recipient ∈ matter participants, attachments present, no privilege violations. fail → blocks gate.
qc.verify — read (gate input)Step 6: GATE:human_review
Park run in awaiting_approval. Attorney or paralegal reviews the draft email. Approve → send. Reject → terminal, no send.
Required role: attorney or paralegal — TTL: 24hStep 7: send_email
Only after gate approval: send the approved draft. Exactly-once delivery via status_update:{change_id} idempotency key.
email.send — gated (human)Step 8: record_outcome
Mark change_id as delivered. Final audit record written. Future sweeps will not re-trigger for this change.
State store — write (confirm)
Idempotency
change_id=sha256(matter_id:from_status:to_status:discriminator)[:16]- Run-level idem key =
change_id - Send idem key =
status_update:{change_id} deliveredflag set only AFTER send + audit write (crash-safe)
QC gate
The QC check before the gate runs three checks on the draft:
| Check | Failure action |
|---|---|
recipient_integrity | fail → gate cannot approve |
attachment_integrity | warn → surfaced in gate UI, approver decides |
privilege | fail → gate cannot approve (non-overridable) |
Any fail blocks the gate. The approver cannot override a QC fail — they must fix the underlying issue first.
Connector error handling
| Error | Action |
|---|---|
auth / fatal | Park run; change_id not marked delivered |
transient / rate-limit | Retry with backoff; same idem_key reused |
not-found (draft missing at send) | Park blocked; no silent re-create |
ASSUMPTION (confirm)
- S-001 — Trigger allow-list (which status values notify)
- S-002 — Recipient role policy beyond the client (cc attorney / G-28?)
- S-003 — Sweep cadence and webhook-primary/sweep-safety-net relationship
- S-004 — Whether status reverts suppress a repeat notification
- S-005 — Whether stale draft re-validated for freshness at send gate
- S-006 — Per-matter opt-out flag for automated emails
See docs/ASSUMPTIONS.md (opens in a new tab) for full tracking.
Example: sweep configuration
The sweep task is configured as a Celery beat schedule:
# celery_app.py
beat_schedule = {
"status-update-sweep": {
"task": "cam.core.workflows.status_update.sweep_matters",
"schedule": crontab(minute="*/15"), # Every 15 minutes
},
}Verification
After a status change and gate approval:
workflow.status(run_id)showssucceeded- Audit log shows
detect_change,compute_delta,draft_email,qc.verify,workflow.approve,send_email,record_outcomein sequence - The email connector shows exactly one
sendcall forstatus_update:{change_id} sweep_matters()does not re-trigger for the samechange_id
Next steps
- Client Intake Workflow — The first workflow that triggers status updates
- Document Generation — Attachments for status emails
- QC Verification — How the gate checks work
Last updated: 2026-06-01