Now domain-agnostic — configure any practice with Domain Packs (immigration is the reference pack)
Workflows
Status Update

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:

  1. Webhook: matter.status_changed event from the case management system
  2. 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

  1. 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 filter
  2. Step 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)
  3. Step 3: resolve_recipients

    Derive email recipients from matter.participants. Default = client contact only. Missing email → blocking gap.

    Recipient resolution
  4. Step 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 only
  5. Step 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)
  6. 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: 24h
  7. Step 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)
  8. 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}
  • delivered flag set only AFTER send + audit write (crash-safe)

QC gate

The QC check before the gate runs three checks on the draft:

CheckFailure action
recipient_integrityfail → gate cannot approve
attachment_integritywarn → surfaced in gate UI, approver decides
privilegefail → 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

ErrorAction
auth / fatalPark run; change_id not marked delivered
transient / rate-limitRetry 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:

  1. workflow.status(run_id) shows succeeded
  2. Audit log shows detect_change, compute_delta, draft_email, qc.verify, workflow.approve, send_email, record_outcome in sequence
  3. The email connector shows exactly one send call for status_update:{change_id}
  4. sweep_matters() does not re-trigger for the same change_id

Next steps


Last updated: 2026-06-01