---
id: STATE-PFM-NOT-011
title: Communication message dispatch
object: DOM-PFM-NOT-005
product_area: platform
module: notifications
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
states:
- name: draft
  meaning: Rendering is complete and the message awaits a send claim.
- name: submitting
  meaning: A provider attempt is open and may still be running.
- name: submitted
  meaning: The provider accepted the message and its reference is recorded.
- name: withheld
  meaning: Protection rules prevented sending and retained their reason.
- name: cancelled
  meaning: Staff cancelled the message before a send claim.
- name: expired
  meaning: The deadline passed before sending.
- name: failed
  meaning: Sending ended with permanent failure or exhausted queue receives.
transitions:
- from: draft
  to: submitting
  trigger: The consumer claims a rendered message.
  guard: Expiry and protection checks pass, a lane token is available and the expected-state update succeeds.
  side_effects:
  - Open an in-progress attempt.
  who: Notification processing worker
  provenance: observed
  source: services-platform:modules/communications/services/persistence/messages.ts:claimMessageForSubmission
- from: draft
  to: withheld
  trigger: The final send check rejects the address or environment policy.
  guard: No additional condition is imposed beyond its source state.
  side_effects:
  - Retain the withholding reason with the state.
  who: Notification processing worker
  provenance: test
  source: services-platform:modules/communications/tests/integration/services/dispatch/reserved-domain-suppression.integration.spec.js#withholds
    its message with the reserved-domain reason, so the history says why it was not sent
- from: draft
  to: expired
  trigger: The final send check detects expiry.
  guard: No additional condition is imposed beyond its source state.
  side_effects:
  - Finish without opening an attempt.
  who: Notification processing worker
  provenance: observed
  source: services-platform:modules/communications/services/persistence/messages.ts:claimMessageForSubmission
- from: draft
  to: cancelled
  trigger: Staff request cancellation.
  guard: Message-update authority and the draft state are required.
  side_effects: []
  who: Association communication administrator
  provenance: observed
  source: services-platform:modules/communications/services/persistence/messages.ts:cancelMessage
- from: submitting
  to: submitted
  trigger: Provider acceptance or qualifying feedback resolves an unfinished submission.
  guard: The message must remain submitting.
  side_effects:
  - Close the attempt as accepted with the provider reference.
  who: Notification processing worker; feedback processor
  provenance: observed
  source: services-platform:modules/communications/services/persistence/messages.ts:recordMessageSubmitted;
    services-platform:modules/communications/services/feedback/process-ses-event.ts:processSesEvent
- from: submitting
  to: draft
  trigger: A throttle or temporary failure permits another receive.
  guard: The message must remain submitting.
  side_effects:
  - Close the attempt with its temporary result and defer the rest of the request.
  who: Notification processing worker
  provenance: observed
  source: services-platform:modules/communications/services/persistence/messages.ts:recordMessageSubmissionFailed
- from: submitting
  to: failed
  trigger: A permanent refusal or final-receive failure ends the attempt.
  guard: The message must remain submitting.
  side_effects:
  - Store permanent failure or receive exhaustion; final-receive work proceeds toward dead-letter handling.
  who: Notification processing worker
  provenance: observed
  source: services-platform:modules/communications/services/persistence/messages.ts:recordMessageSubmissionFailed
- from: submitting
  to: submitting
  trigger: A later claim encounters an old unresolved submission.
  guard: The previous attempt is older than fifteen minutes.
  side_effects:
  - Close the prior attempt as unknown and open another, accepting the possibility of duplicate provider
    sending.
  who: Notification processing worker
  provenance: test
  source: services-platform:modules/communications/tests/integration/services/persistence/messages.integration.spec.js#claims
    it again once the ambiguity window has passed, the unknown attempt closed
- from: failed
  to: draft
  trigger: An operator redrives exhausted request work.
  guard: The stored request is on its first receive, failure means attempts exhausted, and exhausted-message
    retry is enabled by the default-on switch.
  side_effects:
  - Continue attempt numbering from the retained history.
  who: Rhythm notification operator (queue redrive)
  provenance: observed
  source: services-platform:modules/communications/services/persistence/messages.ts:retryExhaustedMessages
invalid:
- from: submitted
  to: draft
  why: Accepted provider submissions cannot be returned to draft.
- from: withheld
  to: cancelled
  why: Cancellation applies only before a draft is claimed.
- from: failed
  to: draft
  why: Permanent failures do not qualify for exhausted-work redrive.
concurrency: Expected-state updates coordinate claims. An unresolved attempt inside fifteen minutes remains
  in flight. The twenty-minute queue visibility exceeds the ambiguity window and consumer timeout. Each
  invocation handles one request, keeping its receive budget separate from other requests.
evidence:
- 'services-platform at ad3f6a65: modules/communications/db/communication-messages.table.ts; services/persistence/messages.ts;
  services/dispatch/{process-communication-request,dispatch-messages}.ts; docs/communications/architecture.md
  §7.5, §18.6, §18.7.'
---
# Communication message dispatch

## The Lifecycle

```mermaid
stateDiagram-v2
    state "draft" as s0
    state "submitting" as s1
    state "submitted" as s2
    state "withheld" as s3
    state "cancelled" as s4
    state "expired" as s5
    state "failed" as s6
    s0 --> s1
    s0 --> s3
    s0 --> s5
    s0 --> s4
    s1 --> s2
    s1 --> s0
    s1 --> s6
    s1 --> s1
    s6 --> s0
```

## Transitions Explained

The transition rows describe stored state changes, their checks and their recorded effects. A queue receive, provider outcome and staff action can reach different lifecycle boundaries.

## What Cannot Happen

Accepted provider submissions cannot be returned to draft. Cancellation applies only before a draft is claimed. Permanent failures do not qualify for exhausted-work redrive.

## Concurrency and Timing

Expected-state updates coordinate claims. An unresolved attempt inside fifteen minutes remains in flight. The twenty-minute queue visibility exceeds the ambiguity window and consumer timeout. Each invocation handles one request, keeping its receive budget separate from other requests.
