---
id: WF-PFM-OPS-004
title: Tell a job’s requester how it ended
product_area: platform
module: operations
revision: 1
status: draft
design_intent: confirmed
implementation_evidence: observed
inference: []
origin: 'services-platform:docs/hq/rhythm-hq-plan.md#D11 and #E5 (approved K8)'
trigger: A notifying kind invokes requester delivery from its success path or failure consequence.
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
implements:
  - CAP-PFM-OPS-003
moves:
  - STATE-PFM-OPS-003
crosses:
  - SVC-PFM-OPS-jobs
idempotency: >-
  A job-and-outcome key identifies the notice. Delivered or suppressed results prevent another claim, while
  pending and failed rows remain claimable if delivery is invoked again.
concurrency: >-
  Pending claims do not reserve a single sender. Result writes carry neither an ownership token nor a state
  condition, so duplicate sends and overwritten results are possible.
hops:
  - 'n': 1
    actor_or_service: Notice capability
    action: 'Find the kind and job, then skip undeclared delivery, a removed job or a job without a requester.'
    anchor: 'services-platform:modules/jobs/services/notices/notify-job-requester.ts:notifyJobRequester'
    seam: sync
    on_failure: Return the ordinary skip result; log caught lookup failures.
    left_inconsistent: No delivery record may exist when lookup fails before claiming.
    observer: Function log is the observation surface.
    provenance: observed
    source: 'services-platform:modules/jobs/services/notices/notify-job-requester.ts:notifyJobRequester'
  - 'n': 2
    actor_or_service: Notice capability
    action: Claim the job-and-outcome row and increment its attempt count.
    anchor: 'services-platform:modules/jobs/services/persistence/job-notices.ts:claimJobNotice'
    seam: sync
    on_failure: Stop when an already handled row matches no claim; contain actual database errors.
    left_inconsistent: A failed claim leaves the requester without this attempt.
    observer: Notice record is the observation surface.
    provenance: test
    source: >-
      services-platform:modules/jobs/tests/integration/services/persistence/job-notices.integration.spec.js#answers
      nothing at all to the second claim, so nothing is sent twice
  - 'n': 3
    actor_or_service: Notice capability
    action: >-
      Resolve the stored requester’s address, render the kind’s message and HQ link, and use the shared mail
      sender.
    anchor: 'services-platform:modules/jobs/services/notices/job-notice-email.ts'
    seam: sync
    on_failure: Record missing addresses or delivery errors as failed when possible.
    left_inconsistent: An interrupted sender can leave a pending claim with unknown delivery.
    observer: Notice outcome and delivery-failure metric is the observation surface.
    provenance: test
    source: >-
      services-platform:modules/jobs/tests/unit/services/notices/job-notice-email.spec.js#refuses to send at
      all when the sending address is unset, naming both variables
  - 'n': 4
    actor_or_service: Notice capability
    action: >-
      Persist delivered, suppressed or failed, with handling time, provider reference or shortened error as
      applicable.
    anchor: 'services-platform:modules/jobs/services/persistence/job-notices.ts:recordJobNoticeOutcome'
    seam: sync
    on_failure: Log outcome-write failures without propagating them to the job.
    left_inconsistent: An accepted message may lack a reliable stored outcome and can be sent again.
    observer: Notice record is the observation surface.
    provenance: observed
    source: 'services-platform:modules/jobs/services/persistence/job-notices.ts:recordJobNoticeOutcome'
branches:
  - condition: Outside production
    path: The mail path suppresses the message and records suppression as a handled outcome.
waits: []
evidence:
  - >-
    services-platform at ad3f6a6: modules/jobs/services/notices (notify-job-requester.ts,
    job-notice-email.ts), services/persistence/job-notices.ts, db/job-notices.table.ts;
    modules/tenants/services/provisioning/provisioning-notice.ts; docs/jobs.md "Telling the requester".
  - >-
    codex examination 2026-10-23: checked the pinned source anchors, planning references and enforcement
    boundaries; unresolved limits are recorded in module findings.
controls: [TC-AVL-001, TC-AVL-004, TC-AVL-005, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-AZ-006, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-IAM-001, TC-IAM-002, TC-IAM-004, TC-IAM-005, TC-IAM-006, TC-LOG-004, TC-LOG-005, TC-MON-001, TC-MON-002, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-PRV-004, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-VEN-001, TC-VEN-002, TC-VEN-003]
---

# Tell a job’s requester how it ended

## What It Does

The shared notice function resolves who receives a job outcome and records the delivery attempt. Each kind supplies the message content. Workspace provisioning is the only notifying kind in the pinned Platform catalogue.

## The Flow

```mermaid
flowchart TD
  H0["Find the kind and job, then skip undeclared delivery, a removed job or a job without a requester."]
  H1["Claim the job-and-outcome row and increment its attempt count."]
  H0 --> H1
  H2["Resolve the stored requester’s address, render the kind’s message and HQ link, and use the shared mail sender."]
  H1 --> H2
  H3["Persist delivered, suppressed or failed, with handling time, provider reference or shortened error as applicable."]
  H2 --> H3
```

## When Something Fails

Address, settings, rendering and sender errors are contained. A later invocation may reclaim a failed or pending row, but no independent sweep guarantees that invocation. A stopped sender or competing claimant can cause another message; competing result writers can also replace a handled state. Today’s HQ destination is accessible to staff rather than future customer requesters.

## Open Design Questions

The requester destination and notification channels remain open. Guaranteed retry and claim-safe result persistence require stronger delivery machinery.
