---
id: STATE-PFM-MTD-008
title: Business event delivery
object: DOM-PFM-MTD-005
product_area: platform
module: metadata
revision: 1
status: draft
design_intent: proposed
implementation_evidence: observed
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
states:
- name: waiting
  meaning: A committed ledger fact has an outbox row awaiting dispatch.
- name: claimed
  meaning: The dispatcher holds a temporary claim on the delivery work.
- name: in_transit
  meaning: The delivery queue has accepted messages for the resolved destinations.
- name: delivered
  meaning: The destination accepted its message.
- name: dead_lettered
  meaning: Repeated delivery failure moved the message to the dead-letter queue.
- name: undelivered
  meaning: Dispatch found no destination and removed delivery work without sending a message.
- name: swept
  meaning: The retention sweep removed the unreferenced old ledger fact.
transitions:
- from: waiting
  to: claimed
  trigger: The dispatcher obtains waiting work.
  guard: The row is available for an expiring claim.
  side_effects:
  - Mark the delivery row claimed without changing the ledger fact.
  who: Rhythm Platform (business events dispatcher)
  provenance: observed
  source: services-platform:modules/custom-objects/services/dispatch/business-events.ts:dispatchPlatformBusinessEvents
- from: claimed
  to: in_transit
  trigger: The queue accepts all destination messages.
  guard: Every resolved destination message is accepted.
  side_effects:
  - Remove its outbox row while retaining the ledger fact.
  who: Rhythm Platform (business events dispatcher)
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/dispatch/business-events.integration.spec.js#keeps
    its queue row, claimed, for the next run
- from: claimed
  to: waiting
  trigger: A send fails or the claim expires.
  guard: The claim becomes available again after its timeout.
  side_effects:
  - Retain the outbox work for a later dispatcher run.
  who: Rhythm Platform (business events dispatcher)
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/dispatch/business-events.integration.spec.js#keeps
    its queue row, claimed, for the next run
- from: claimed
  to: undelivered
  trigger: Dispatch finds no eligible destination.
  guard: The runtime subscriber registry and router lookup provide no destination.
  side_effects:
  - Remove delivery work without enqueuing a message.
  who: Rhythm Platform (business events dispatcher)
  provenance: documented
  source: 'services-platform:docs/business-events.md#4. Delivery: the dispatcher, the consumer, the intake'
- from: in_transit
  to: delivered
  trigger: The consumer succeeds at a destination.
  guard: The destination invocation accepts the envelope.
  side_effects:
  - Leave destination deduplication to the subscriber.
  who: Rhythm Platform (business events consumer)
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/dispatch/business-events.integration.spec.js#answers
    the one message whose destination refus
- from: in_transit
  to: in_transit
  trigger: A destination attempt fails.
  guard: The queue retry limit has not been exhausted.
  side_effects:
  - Report this message as failed so the queue offers it again.
  who: Rhythm Platform (business events consumer)
  provenance: test
  source: services-platform:modules/custom-objects/tests/integration/services/dispatch/business-events.integration.spec.js#answers
    the one message whose destination refus
- from: in_transit
  to: dead_lettered
  trigger: Repeated delivery reaches its retry limit.
  guard: The fifth receive fails.
  side_effects:
  - Move the message to the dead-letter queue, whose age alarm reaches alerts.
  who: Rhythm Platform (delivery queue)
  provenance: documented
  source: 'services-platform:docs/business-events.md#4. Delivery: the dispatcher, the consumer, the intake'
- from: delivered
  to: swept
  trigger: The nightly retention sweep visits an old fact.
  guard: It is older than ninety days and has no outbox reference.
  side_effects:
  - Delete the ledger fact and log the removed count.
  who: Rhythm Platform (business events dispatcher, sweep action)
  provenance: observed
  source: services-platform:modules/custom-objects/services/persistence/business-events.ts:sweepBusinessEvents
- from: undelivered
  to: swept
  trigger: The nightly sweep visits an unsent old fact.
  guard: It is older than ninety days and no delivery row protects it.
  side_effects:
  - Delete the unsent fact, ending any replay possibility.
  who: Rhythm Platform (business events dispatcher, sweep action)
  provenance: observed
  source: services-platform:modules/custom-objects/services/persistence/business-events.ts:sweepBusinessEvents
invalid:
- from: waiting
  to: swept
  why: Retention keeps facts protected by outbox work, and the application does not requeue delivered facts.
- from: delivered
  to: waiting
  why: Retention keeps facts protected by outbox work, and the application does not requeue delivered facts.
concurrency: The dispatcher is limited to one invocation and uses expiring claims. Message handling reports individual
  failures, allowing unrelated destinations to proceed. Accepted messages may be repeated, and consumers must deduplicate
  envelope identities.
invariants:
- INV-DOM-PFM-MTD-005-10
evidence:
- 'services-platform at ad3f6a65: modules/custom-objects/services/dispatch/business-events.ts; services/persistence/business-events.ts;
  aws/logic/business-events.lambda.ts; aws/lambda/BUSINESS_EVENTS_*.handler.ts, EVENT_INTAKE.handler.ts; business-events.settings.ts;
  docs/business-events.md'
---

# Business event delivery

## The Lifecycle

```mermaid
stateDiagram-v2
  [*] --> waiting: change commits
  waiting --> claimed: dispatcher run
  claimed --> waiting: queue refused or claim expired
  claimed --> in_transit: messages accepted
  claimed --> undelivered: no destination
  in_transit --> in_transit: destination failed, retried
  in_transit --> delivered
  in_transit --> dead_lettered: fifth failure
  delivered --> swept: older than ninety days
  undelivered --> swept: older than ninety days
```

## Transitions Explained

Supported mutations append ledger and delivery work transactionally. Dispatch resolves runtime destinations and queues messages; successful queue acceptance removes outbox work. A destination-free event also leaves the outbox but receives no message.

## What Cannot Happen

Retention keeps facts protected by outbox work, and the application does not requeue delivered facts.

## Concurrency and Timing

The dispatcher is limited to one invocation and uses expiring claims. Message handling reports individual failures, allowing unrelated destinations to proceed. Accepted messages may be repeated, and consumers must deduplicate envelope identities.
