---
id: STATE-PFM-NOT-005
title: Notification delivery
object: DOM-PFM-NOT-003
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: queued
  meaning: Work is ready for dispatch or already on its channel queue.
- name: deferred
  meaning: Reserved for later quiet-hours and digest processing; no current writer assigns it.
- name: sending
  meaning: A consumer has claimed the delivery for an adapter attempt.
- name: sent
  meaning: The channel accepted the message and supplied its reference.
- name: delivered
  meaning: Delivery is complete; console deliveries begin here.
- name: failed
  meaning: Sending ended with permanent, exhausted or ambiguous failure.
- name: skipped
  meaning: A recorded availability or reachability reason prevented sending.
- name: bounced
  meaning: The vocabulary allows provider bounce feedback, although current adapters do not report it.
- name: cancelled
  meaning: Withdrawal ended the delivery before sending.
- name: expired
  meaning: The request deadline ended the delivery before sending.
transitions:
- from: queued
  to: sending
  trigger: The channel consumer claims queued work.
  guard: Work is queued, the notice applies, the request is unexpired, an adapter exists and the conditional
    claim succeeds.
  side_effects:
  - Refresh the notice progress summary.
  who: Channel delivery worker
  provenance: test
  source: services-platform:modules/notifications/tests/integration/services/persistence/deliveries.integration.spec.js#lets
    only one of two consumers racing for the send win it
- from: sending
  to: sent
  trigger: The adapter accepts the delivery.
  guard: The delivery must remain in sending.
  side_effects:
  - Save the provider reference and sent event.
  who: Channel delivery worker
  provenance: observed
  source: services-platform:modules/notifications/services/dispatch/deliver-delivery.ts:deliverQueuedDelivery
- from: sent
  to: delivered
  trigger: The adapter or provider confirms delivery.
  guard: The delivery must be sent.
  side_effects:
  - Append the delivery confirmation event.
  who: Channel delivery worker
  provenance: observed
  source: services-platform:modules/notifications/services/dispatch/deliver-delivery.ts:deliverQueuedDelivery
- from: sending
  to: queued
  trigger: A retryable adapter result or exception leaves another receive available.
  guard: The receive count is below eight.
  side_effects:
  - Return work with exponential delay starting at thirty seconds, capped at thirty minutes with twenty-percent
    jitter and respecting a longer provider delay.
  who: Channel delivery worker
  provenance: test
  source: services-platform:modules/notifications/tests/unit/slack-system-adapter.spec.js#answers a 429
    as a retryable failure carrying Retry-After
- from: sending
  to: failed
  trigger: The adapter refuses permanently, the last receive fails, or reconciliation detects fifteen
    minutes of ambiguity.
  guard: The delivery must remain in sending.
  side_effects:
  - Retain the provider, exhaustion or ambiguity reason.
  - On the final receive, return the item immediately for dead-letter handling.
  - Publish the terminal-failure metric for an HQ alert.
  who: Channel delivery worker; nightly reconciliation
  provenance: test
  source: services-platform:modules/notifications/tests/integration/services/dispatch/deliver-delivery.integration.spec.js#fails
    the delivery attempts_exhausted on the last receive and gives the item back at once to reach the dead-letter
    queue
- from: sending
  to: skipped
  trigger: The adapter reports that delivery is unavailable.
  guard: The delivery must remain in sending.
  side_effects:
  - Store the skip reason and emit a process-limited warning.
  who: Channel delivery worker
  provenance: observed
  source: services-platform:modules/notifications/services/channels/slack-system/slack-system-adapter.ts
- from: queued
  to: skipped
  trigger: Dispatch cannot find the named adapter.
  guard: The delivery must still be queued.
  side_effects:
  - Record adapter unavailability.
  who: Channel delivery worker
  provenance: observed
  source: services-platform:modules/notifications/services/dispatch/deliver-delivery.ts:deliverQueuedDelivery
- from: queued
  to: cancelled
  trigger: Withdrawal or replacement reaches queued work.
  guard: The delivery must still be queued.
  side_effects:
  - Delete the associated dispatch work.
  who: Notification processing worker; chat delivery consumer
  provenance: test
  source: services-platform:modules/notifications/tests/integration/services/dispatch/deliver-delivery.integration.spec.js#cancels,
    not sends, a delivery whose notification was superseded after it was queued
- from: queued
  to: expired
  trigger: Processing or reconciliation detects an expired request.
  guard: The delivery must still be queued.
  side_effects: []
  who: Channel delivery worker; nightly reconciliation
  provenance: observed
  source: services-platform:modules/notifications/services/orchestration/expire-request.ts
- from: failed
  to: queued
  trigger: An HQ operator requests an exhausted-delivery retry.
  guard: Only failed deliveries with attempts exhaustion qualify.
  side_effects:
  - Create fresh dispatch work for a later dispatcher run.
  who: Rhythm notification operator
  provenance: test
  source: services-platform:modules/notifications/tests/integration/services/dispatch/deliver-delivery.integration.spec.js#queues
    a delivery that ran out of receives again, with its work row, and nothing else
invalid:
- from: delivered
  to: queued
  why: Successful terminal delivery cannot return to the queue.
- from: sending
  to: sending
  why: A competing consumer cannot acquire an existing sending claim.
- from: failed
  to: sending
  why: A retry must first requeue work; ambiguous failures do not qualify.
- from: skipped
  to: queued
  why: Skipped deliveries retain their reason and remain terminal.
concurrency: Expected-state updates decide which consumer owns a send. Queue receive count supplies the
  retry budget; there is no database attempt counter. Operator retry uses a separate conditional update.
  Provider-event identities deduplicate the retained history, which is capped at fifty events.
invariants:
- INV-DOM-PFM-NOT-003-07
evidence:
- 'services-platform at ad3f6a65: modules/notifications/services/persistence/deliveries.ts (TRANSITIONS,
  transitionDelivery, settleAmbiguousDeliveries, requeueExhaustedDelivery); services/dispatch/deliver-delivery.ts;
  notifications.settings.ts.'
---
# Notification delivery

## The Lifecycle

```mermaid
stateDiagram-v2
    state "queued" as s0
    state "deferred" as s1
    state "sending" as s2
    state "sent" as s3
    state "delivered" as s4
    state "failed" as s5
    state "skipped" as s6
    state "bounced" as s7
    state "cancelled" as s8
    state "expired" as s9
    s0 --> s2
    s2 --> s3
    s3 --> s4
    s2 --> s0
    s2 --> s5
    s2 --> s6
    s0 --> s6
    s0 --> s8
    s0 --> s9
    s5 --> 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

Successful terminal delivery cannot return to the queue. A competing consumer cannot acquire an existing sending claim. A retry must first requeue work; ambiguous failures do not qualify. Skipped deliveries retain their reason and remain terminal.

## Concurrency and Timing

Expected-state updates decide which consumer owns a send. Queue receive count supplies the retry budget; there is no database attempt counter. Operator retry uses a separate conditional update. Provider-event identities deduplicate the retained history, which is capped at fifty events.
