Notification

Atom grounded on Final Critique 4 — 2026-06-18 9 invariants Alloy model buggy twin ×1 regulated · via 2 composers composed by 2
Derived card — generated from the corpus (status line, invariant headers, sibling formal-model files, Composes edges). The specification below is canonical. composition graph →
Table of contents
  1. Notification
    1. Summary
    2. Intent
    3. Structure
      1. Identity model
      2. State
      3. Capability requirement
      4. Operations
      5. Invariants
    4. Examples
      1. Shared Todo — assignment notification delivery
      2. Support queue — escalation alert
      3. Compliance system — policy change
      4. Rejection path — invalid create
      5. Regulated adversarial scenarios
    5. Generation acceptance
      1. Conformance checks
      2. External checks
    6. Non-goals
    7. Edge cases
      1. String policy
      2. Atomicity of a terminal transition
      3. Bulk expiry
      4. Deliver persistence failure
      5. Payload retention
    8. Composition notes
    9. Terms
      1. Vocabulary
        1. Create
        2. Deliver
        3. Fail
        4. Expire
        5. Status Of
        6. Pending For
        7. Notification Id
        8. Recipient Ref
        9. Payload
        10. Created At
        11. Status
        12. Delivered At
        13. Failed At
        14. Expired At
        15. Pending
        16. Delivered
        17. Failed
        18. Expired
        19. Now
        20. Invalid Request
        21. Not Known
        22. Not Pending
    10. Standards references
    11. Status
    12. Ledger
    13. Decisions

Summary

Notification records whether a single piece of information was accepted by the transport for a single recipient. Where Subscription records who wants to know about a class of events, Notification records the outcome of one delivery. It creates a permanent, unchangeable record of a delivery attempt and tracks it to one of three end states: Delivered (the transport layer confirmed receipt), Failed (the transport returned a definite error), or Expired (the delivery window ran out with no recorded outcome). These are kept as three distinct states because each answers a different question and is handled by a different part of the system — the delivery layer, the failure handler, the expiry scheduler. The pattern does not decide who should be notified or how to reach them; by the time it is asked to create a record, the recipient and the content are already settled. Nothing is ever deleted, so the full delivery history of any notification is always recoverable. This is what underlies delivery audit trails in regulated settings, and it is the substrate for retry logic, which records each retry as a new notification rather than altering the failed one.


Intent

WHY: A notification is a promise to tell somebody something, and the interesting part is not the telling — it is the record of whether it happened. Systems that skip this concept end up asking their transport layer what happened weeks ago, which is a question no transport layer can answer. The atom holds the delivery record: who it was for, what it said, when it was made, and exactly one outcome — reached, failed, or ran out of time. It sends nothing. It does not know what a webhook is, does not retry, does not decide whether a bounced email is a failure or an expiry. What it guarantees is that every attempt is one record with one terminal answer and a timestamp, so did we tell them is answered from the store rather than from a log nobody kept.

Structure

Identity model

Identity 1: The atom MUST identify a notification by the notification id.
Identity 2: The host MUST allocate a notification id at the atom's seam.
Identity 3: The transition MUST NOT allocate a notification id.
Identity 4: The business caller MUST NOT supply a notification id.
Identity 5: The atom MUST NOT reuse a notification id.
Identity 6: The atom MUST NOT identify a notification by the recipient ref with the payload.
Identity 7: Two notifications carrying one recipient ref and one payload MUST carry two notification ids.

Term notification: one delivery record — one recipient, one payload, one outcome.

Term notification id: the opaque value naming one notification — a Notification Id.

Term recipient ref: the opaque reference naming who the notification is for — a Recipient Ref; compared by equality and never interpreted.

Term payload: the opaque content the notification carries — a Payload; stored and returned unchanged.

Term seam: the atom’s I/O boundary as the section titled Logic Confinement Principle in execution-contract.md declares it; the host injects the clock reading and the notification id here.

Term transition: the atom’s evaluation of one call against the notification store, as the section titled Logic Confinement Principle in execution-contract.md declares it.

Term business caller: the party whose action the call carries, as the section titled Logic Confinement Principle in execution-contract.md declares it; never the source of an injected value.

Term now: the wall-time reading the host takes at the seam and hands to the transition — a Now, as the section titled Logic Confinement Principle in execution-contract.md declares it; never read inside the transition, never supplied by the business caller.

WHY: One attempt, one record. A retry is a new notification with a new id rather than a second outcome on the old one, which is what keeps how many times did we try answerable and stops a terminal record being rewritten (Identity 7, Non-goal 3).

State

State 1: EVERY notification MUST stand in EXACTLY ONE OF pending, delivered, failed, expired.
State 2: EVERY notification MUST carry notification id, recipient ref, payload, created at and status.
State 3: A delivered notification MUST carry delivered at.
State 4: A failed notification MUST carry failed at.
State 5: An expired notification MUST carry expired at.
State 6: A pending notification MUST NOT carry a terminal stamp.
State 7: [Create] MUST stamp created at from the injected now.
State 8: A terminal transition MUST stamp the transition's terminal stamp from the injected now.
State 9: The atom MUST NOT offer a transition out of a terminal status.
State 10: The atom MUST NOT delete a notification.
State 11: The atom MUST NOT hold a transport.
State 12: The atom MUST NOT hold a retry.
Term status: pending delivered failed expired — the Status field’s four values: awaiting an outcome, reached, attempted without success, or out of time.
Term terminal stamp: delivered at failed at expired at — the one stamp a terminal status carries.

Term created at: the instant the notification was recorded — a Created At.

WHY: Four states and exactly one terminal stamp each, because the audit question is what happened to this one, and a record carrying two terminal stamps answers it twice (State 6, Invariant 3.1). Nothing about transport lives here: a webhook, a push and an email produce the same three outcomes, and an atom that knew the difference would have to be re-specified every time a deployment changed channel (State 11).

Capability requirement

Capability requirement 1: The deployment MUST supply now at the seam.
Deleted: Capability requirement 2. Execution Contract Logic confinement 7 owns it.
Deleted: Capability requirement 3. Execution Contract Logic confinement 7 owns it.
Deleted: Clock semantics 1. Execution Contract Logic confinement 7 owns it.
Deleted: Clock semantics 2. Execution Contract Logic confinement 7 owns it.
Deleted: Clock semantics 3. Non-goal 16 owns it.

WHY: What the deployment supplies, which is what the family means. The rule stood under Operation — one action’s rules — while naming no action, because this spec was migrated before the standard family had a home in an atom; the five atoms migrated a day later put the same obligation here. The words are the words the rule carried (council read 76).

Operations

create(recipient_ref, payload)
  answers notification_id
  refuses invalid-request | storage-failure

deliver(notification_id)
  answers ok
  refuses not-known | not-pending | storage-failure

fail(notification_id)
  answers ok
  refuses not-known | not-pending | storage-failure

expire(notification_id)
  answers ok
  refuses not-known | not-pending | storage-failure

status_of(notification_id)
  answers notification | not-known

pending_for(recipient_ref)
  answers notification_ids
Operation 1: [Create] MUST record EXACTLY ONE notification per successful call.
Operation 2: [Create] MUST stand the notification in pending.
Operation 3: [Create] MUST answer notification id.
Operation 4: IF recipient ref is empty THEN [Create] MUST answer invalid-request.
Operation 5: [Create] MUST accept an empty payload.
Operation 6: The atom MUST NOT read a payload.
Operation 7: IF the store refuses the write THEN [Create] MUST answer storage-failure.
Operation 8: IF no notification EXISTS for the notification id THEN a terminal transition MUST answer not-known.
Operation 9: IF the notification's status DOES NOT EQUAL pending THEN a terminal transition MUST answer not-pending.
Operation 10: [Deliver] MUST stand the notification in delivered.
Operation 11: [Fail] MUST stand the notification in failed.
Operation 12: [Expire] MUST stand the notification in expired.
Operation 13: [Expire] MUST NOT require a delivery attempt.
Operation 14: The atom MUST NOT choose between fail and expire.
Operation 14a: The atom MUST NOT hold a delivery window.
Operation 14b: A composing pattern MUST own the delivery window an expiry answers to.
Operation 15: IF the store refuses the write THEN a terminal transition MUST answer storage-failure.
Operation 16: A refused call MUST leave the notification as the call found the notification.
Operation 17: [Status Of] MUST answer the notification's stored fields.
Operation 18: IF no notification EXISTS for the notification id THEN [Status Of] MUST answer not-known.
Operation 19: [Pending For] MUST answer the notification id of EVERY pending notification carrying the recipient ref.
Operation 20: [Pending For] MUST NOT answer a terminal notification's notification id.
Operation 21: [Pending For] MUST NOT order the answer.
Operation 22: [Status Of] MUST NOT write.
Operation 23: [Pending For] MUST NOT write.
Deleted: Operation 24. Capability requirement 1 owns it.
Deleted: Operation 25. Execution Contract Logic confinement 3 owns it.
Deleted: Operation 26. Execution Contract Logic confinement 3 owns it.

Term terminal transition: a Deliver, a Fail or an Expire call — the three that end a notification, sharing one precondition pair.

Term pending at an instant: created at at or before the instant, and the terminal stamp either absent or after the instant — the reconstruction an auditor runs over stored fields, never over status, which carries the present rather than the past.

The case space, and the rule that owns each case:

Call Case Answer Effect on the notification store
Create recipient present, store accepts notification id one notification lands in Pending (Operation 1, Operation 2)
Create empty or whitespace-only recipient, or over the cap Invalid Request none (Operation 4, String 5, String 6)
Create empty payload notification id one notification lands — the payload is the caller’s business (Operation 5, Operation 6)
Deliver notification is pending ok PendingDelivered, delivered at stamped (Operation 10, State 8)
Fail notification is pending ok PendingFailed, failed at stamped (Operation 11)
Expire notification is pending, attempted or not ok PendingExpired, expired at stamped (Operation 12, Operation 13)
any terminal transition notification already terminal Not Pending none — including a second Deliver (Operation 9)
any terminal transition id names nothing Not Known none (Operation 8)
any write store refuses storage-failure none (Operation 7, Operation 15, Operation 16)
Status Of id names a notification its stored fields none — the call reads (Operation 17, Operation 22)
Status Of id names nothing not-known none (Operation 18)
Pending For recipient has pending notifications their ids, unordered none (Operation 19, Operation 21)
Pending For recipient has none, or only terminal ones empty list none (Operation 20)
Pending For over-length or whitespace-only recipient empty list none — a read with a bad argument has a correct answer (String 8, String 9)

WHY: The three terminal transitions share one precondition pair — known, and pending — which is why they are one rule each and two rules between them rather than nine (Operation 8, Operation 9). Which operational event counts as a failure and which as an expiry is the deployment’s policy and deliberately not the atom’s: a bounced address is a failure in one shop and an expiry in another, and an atom that decided would make two deployments’ records mean different things while looking identical (Operation 14, External check 1).

Invariants

  • Invariant 1 — Notification immutability.
    Invariant 1.1: A recorded notification's notification id, recipient ref, payload and created at MUST NOT change.
    Invariant 1.2: A landed terminal stamp MUST NOT change.
    
  • Invariant 2 — Status monotonicity.
    Invariant 2.1: A status MUST move from pending to EXACTLY ONE OF delivered, failed, expired.
    Invariant 2.2: A status MUST NOT move to pending from a terminal status.
    Invariant 2.3: A status MUST NOT move between two terminal statuses.
    Invariant 2.4: A notification MUST read as pending at an instant ONLY IF the notification is pending at an instant.
    
  • Invariant 3 — Terminal states are exclusive.
    Invariant 3.1: A notification whose status DOES NOT EQUAL pending MUST carry EXACTLY ONE terminal stamp.
    Invariant 3.2: A pending notification MUST NOT carry a terminal stamp.
    
  • Invariant 4 — Terminal timestamps match status.
    Invariant 4.1: A delivered notification MUST carry delivered at.
    Invariant 4.2: A notification carrying delivered at MUST stand in delivered.
    Invariant 4.3: A failed notification MUST carry failed at.
    Invariant 4.4: A notification carrying failed at MUST stand in failed.
    Invariant 4.5: An expired notification MUST carry expired at.
    Invariant 4.6: A notification carrying expired at MUST stand in expired.
    
  • Invariant 5 — Id stability.
    Invariant 5.1: [Create] MUST set the notification id.
    Invariant 5.2: A notification id MUST NOT change.
    
  • Invariant 6 — No id reuse.
    Invariant 6.1: Two notifications MUST NOT share a notification id.
    
  • Invariant 7 — Pending query excludes terminals.
    Invariant 7.1: [Pending For] MUST answer a notification id ONLY IF the notification's status EQUALS pending.
    
  • Invariant 8 — Timestamp ordering.
    Invariant 8.1: IF delivered at DOES NOT EQUAL blank THEN created at MUST NOT EXCEED delivered at.
    Invariant 8.2: IF failed at DOES NOT EQUAL blank THEN created at MUST NOT EXCEED failed at.
    Invariant 8.3: IF expired at DOES NOT EQUAL blank THEN created at MUST NOT EXCEED expired at.
    

    WHY: best-effort under a clock that moves backward; the deployment owns clock discipline (Execution Contract Logic confinement 7).

  • Invariant 9 — Notification durability.
    Invariant 9.1: The atom MUST NOT delete a notification record.
    Invariant 9.2: The notification set MUST NOT shrink.
    Invariant 9.3: [Status Of] MUST answer a created notification's fields for the store's life.
    

Immutability and durability give auditability — the delivery history is the store, with no gaps. Exclusivity and the status-stamp match give the unambiguous record property — one outcome per notification, with a stable time. Monotonicity and ordering make Pending For a deterministic snapshot of what is still unresolved.

Examples

The same atom, three domains, identical mechanic.

Shared Todo — assignment notification delivery

A Notification Fanout pattern creates a notification when a task is assigned: create(dev_d, {type: "task:assigned", task_id: t1, assigned_by: manager_m}) → notif_77. The WebSocket layer pushes the payload to dev_d’s active session and calls deliver(notif_77)Delivered. status_of(notif_77) returns status = delivered, Delivered At set. pending_for(dev_d) returns [].

If dev_d is offline, the push attempt produces a connection error: fail(notif_77)Failed. The composing system may create a retry notification: create(dev_d, {same payload}) → notif_78 — a new Pending record, distinct id, independent outcome.

If neither delivery nor failure is recorded within the expiry window: expire(notif_77)Expired. status_of(notif_77) returns Expired At.

Support queue — escalation alert

When queue 9 escalates, the fanout pattern creates one notification per subscribed supervisor: create(supervisor_s, {type: "escalation", queue: 9, ticket: t22}) → notif_33. Email delivery succeeds: deliver(notif_33)Delivered. status_of(notif_33) returns status = delivered, Delivered At. Elapsed delivery time is delivered_at − created_at.

Supervisor_s later asks “was I notified about the queue-9 escalation?”pending_for(supervisor_s) returns an empty list (nothing pending), and status_of(notif_33) returns status = delivered with Delivered At. The delivery record answers the question from stored fields alone.

Compliance system — policy change

An administrator broadcasts a policy update. Three compliance officers each receive a notification: create(officer_a, {type: "policy:updated", policy_id: p7}) → notif_101, similarly for officers b and c. Officer_a’s email bounces: fail(notif_101). Officers b and c are delivered successfully. status_of(notif_101) shows Failed At; status_of(notif_102) and status_of(notif_103) show Delivered At. An operator queries pending_for for each officer — empty for all three. The notification store shows: two Delivered, one Failed; the composing system creates a retry for officer_a or escalates to a secondary channel.

Rejection path — invalid create

A composing system attempts to create a notification with an empty recipient reference: create(recipient_ref: "", payload: {type: "task:assigned", task_id: "t1"}) → invalid-request. No Notification Id is issued; no record enters the store. The composing system must supply a non-empty recipient reference before the notification can be created.

Regulated adversarial scenarios

Three scenarios the notification store must survive in regulated contexts:

  • Regulator audit — demonstrate all notifications for a compliance event. A compliance auditor asks “show all notifications created for the policy:updated event on 2025-03-14, and whether each was delivered.” The auditor queries the notification store for notifications where created at falls on 2025-03-14 and the payload references the relevant policy. Status Of for each returned id shows the delivery outcome — Delivered At, Failed At, or Expired At. The notification store answers from stored fields alone; Invariants 1 and 3-4 guarantee the delivery record is complete and unambiguous.
  • Disputed delivery — actor claims they were not notified. Officer_a claims they received no notification of policy update p7. The investigator queries the notification store for notifications where recipient_ref = officer_a and the payload references policy_id: p7. If a record exists with Delivered At set, Invariant 1 (notification immutability) is the structural answer: the notification was created with that recipient and delivery was confirmed at that time. If the record shows Failed At or Expired At, the store confirms delivery was not completed and documents why. The notification store is the single source of truth; no external corroboration is required.
  • Breach investigation — identify Pending notifications that may have exposed payload data. A security incident requires identifying all notifications that were Pending at the time of breach (2025-06-01T03:00Z) and may have carried sensitive payload data. The investigator queries for notifications where created_at ≤ 2025-06-01T03:00Z and either status = pending (still unresolved now) or the applicable terminal timestamp falls after 2025-06-01T03:00Z (meaning the notification was Pending during the breach window but has since resolved). The reconstruction logic mirrors the Subscription pattern: created_at ≤ T and (status = pending or delivered_at > T or failed_at > T or expired_at > T). Status Of for each candidate returns the current record; created at confirms the exposure window. The notification store answers the exposure scope question from stored fields alone without recourse to logs or developer narration.

Generation acceptance

This atom’s acceptance is what an external auditor can clear from the notification store’s stored fields, with no recourse to source code, runbooks or developer narration. The audit surface is the store, not the action surface.

Conformance checks

Check 1.1: An auditor MUST read EVERY notification's notification id, recipient ref, payload, created at and status from the store (State 2).
Check 2.1: An auditor MUST reconstruct a notification's status at a past instant from created at and the terminal stamp (Invariant 2.4).
Check 2.2: An auditor MUST read the reconstruction as deterministic on stored fields (Invariant 1.2).
Check 2.3: An auditor MUST read the reconstruction's wall-clock truth as best-effort (Invariant 8.1, Execution Contract Logic confinement 7).
Check 3.1: An auditor MUST find no notification carrying two terminal stamps (Invariant 3.1).
Check 3.2: An auditor MUST find no pending notification carrying a terminal stamp (Invariant 3.2).
Check 4.1: An auditor MUST find EVERY terminal stamp matching the notification's status (Invariant 4.2, Invariant 4.4, Invariant 4.6).
Check 5.1: An auditor MUST find the notification set never shrinking across two readings (Invariant 9.1, Invariant 9.2).
Check 6.1: An auditor MUST identify which composing patterns a deployment wired in (Composition note 1).

External checks

External check 1: An auditor MUST read the deployment's fail-versus-expire policy from the deployment's own declaration (Operation 14).
External check 2: An auditor MUST read a payload's retention from the composing Retention Window records (Non-goal 13).
External check 3: An auditor MUST read who created a notification from the composing Actor Identity attestations (Non-goal 11).
External check 4: An auditor MUST read the transport's own outcome from the deployment's delivery layer (Non-goal 5).

NOTE: EVERY check names the rule the check tests. External check 1 is the one that makes cross-deployment audit possible: without the declared policy, one shop’s failed at and another’s expired at record the same operational event and no reader can tell.

Non-goals

Non-goal 1: The atom MUST NOT evaluate a subscription.
Non-goal 2: The atom MUST NOT choose a notification's recipient.
Non-goal 3: The atom MUST NOT retry a failed notification.
Non-goal 4: A composing pattern retrying MUST create a second notification.
Non-goal 5: The atom MUST NOT deliver a notification.
Non-goal 6: The atom MUST NOT hold a transport's mechanism.
Non-goal 7: The atom MUST NOT order two pending notifications.
Non-goal 8: The atom MUST NOT record a recipient's reading.
Non-goal 9: A deployment needing a read acknowledgement MUST create a second notification for the acknowledgement.
Non-goal 10: The atom MUST NOT validate a payload.
Non-goal 11: The atom MUST NOT record who called [Create].
Non-goal 12: The atom MUST NOT deduplicate two notifications.
Non-goal 13: The atom MUST NOT purge a payload.
Non-goal 14: The atom MUST NOT gate [Create].
Non-goal 15: The atom MUST NOT expire notifications in bulk.
Non-goal 16: A deployment needing a defensible timeline MUST compose a trusted timestamping pattern.

WHY: Who should hear about an event is Subscription’s and the fanout that reads it; this atom starts once the recipient is known (Non-goal 1, Non-goal 2). A retry is a new record rather than a second chance at an old one, which is what keeps the count of attempts honest (Non-goal 3, Non-goal 4). Read acknowledgement is two records because the transport accepted it and the person saw it are two facts, and one record forced to carry both loses whichever is recorded second (Non-goal 8, Non-goal 9). Payload retention is the sharp one: this store keeps every payload for the life of the system, so a deployment whose payloads carry personal or medical content composes Retention Window rather than trusting an atom that never forgets (Non-goal 13, Payload retention 1 through 3).

Where the atom breaks down: when delivered is not a single observable event — a multi-hop transport with partial acknowledgement; when the same notification must be retried in place, which this atom refuses on purpose; when the payload cannot be stored at all, which needs a reference rather than content.

Edge cases

String policy

String 1: The atom MUST compare a string input byte-exactly.
String 2: The atom MUST NOT trim a string input.
String 3: The atom MUST NOT normalize a string input.
String 4: The atom MUST NOT case-fold a string input.
String 5: The atom MUST read a whitespace-only recipient ref as empty.
String 6: IF a string input EXCEEDS the string cap THEN [Create] MUST answer invalid-request.
String 7: The business caller MUST own a recipient ref's canonical form.
String 8: [Pending For] MUST read an over-length recipient ref as matching nothing.
String 9: [Pending For] MUST read a whitespace-only recipient ref as matching nothing.

Term string cap: the deployment’s bound on a string input’s length.

Atomicity of a terminal transition

Terminal atomicity 1: The implementation MUST change status and the terminal stamp together.
Terminal atomicity 2: A crash inside a terminal transition MUST NOT leave a terminal status without the terminal stamp.
Terminal atomicity 3: A crash inside a terminal transition MUST NOT leave a terminal stamp on a pending notification.
Terminal atomicity 4: The implementation MUST serialize two terminal transitions on one notification id.

WHY: Half a transition breaks Invariant 4 while every field reads plausibly on its own — and Invariant 4’s two directions are exactly what an auditor uses to detect it, which is why the repair is the implementation’s transactional boundary rather than a reader’s inference (Terminal atomicity 1, Invariant 4.2).

Bulk expiry

Bulk expiry 1: A composing pattern MUST enumerate a recipient's pending notifications.
Bulk expiry 2: A composing pattern MUST call [Expire] for EVERY notification the enumeration returns.
Bulk expiry 3: A composing pattern MUST NOT read one expiry as a deadline sweep.

Deliver persistence failure

Deliver persistence 1: A caller MUST read storage-failure from [Deliver] as the notification whose status EQUALS pending.
Deliver persistence 2: A caller MUST retry a deliver that answered storage-failure.
Deliver persistence 3: A caller MUST NOT deliver the payload a second time on that retry.
Deliver persistence 4: A high-assurance deployment MUST raise an alert on storage-failure from [Deliver].

WHY: The transport accepted the notification and the store did not record it, so the record understates what happened — the recipient has been told and did we tell them answers no. The retry is a retry of the write, never of the send, which is the distinction a caller that treats the two alike gets wrong twice (Deliver persistence 2, Deliver persistence 3; the polarity Permissions states for its own revoke).

Payload retention

Payload retention 1: The atom MUST keep a payload for the store's life.
Payload retention 2: The atom MUST NOT offer a payload purge.
Payload retention 3: A deployment whose payloads carry sensitive content MUST compose Retention Window.
Payload retention 4: A composing pattern purging a payload MUST leave EVERY stored field as written.
Payload retention 5: A composing pattern MUST NOT delete a notification record.

WHY: Invariant 1.1 forbids a payload changing and Invariant 9.2 forbids the set shrinking, so a composed purge cannot edit the payload or drop the record. What it can do is the shredding-class destruction Audit Trail already wires over its own cascade: the content becomes unrecoverable while every stored field stands as written, which is why the invariants and the retention obligation do not collide (Payload retention 4, Payload retention 5).

Composition notes

Composition note 1: A deployment MUST declare which composing patterns the deployment wired in.
Composition note 2: A deployment MUST declare the deployment's fail-versus-expire policy.
Composition note 3: A composing pattern MUST own who is notified.
Composition note 4: A composing pattern MUST own the transport.
Composition note 5: A composing pattern needing at-most-once notification MUST guard [Create] with Duplicate Prevention.
Composition note 6: A composing pattern MUST own a payload's retention.
Composition note 7: A composing pattern MUST own the delivery window.

WHY: Notification Fanout is the wiring this atom was extracted for: an event fires, Subscription answers who is listening, and one notification is created per subscriber — the composition owns the audience, this atom owns the record (Composition note 3). Preference-Aware Notification Fanout adds Message Preference between the two. The fail-versus-expire policy is declared by the deployment rather than the atom, and it is what makes two deployments’ records comparable (Composition note 2, External check 1).

Terms

Each [Term] marker above links to its term entry here; a term entry states what the concept is and its Kind.

Vocabulary

Term actors: the atom; the host; the transition; the implementation; the deployment; a composing pattern (also: a pattern); a business caller; a caller; a recipient; an auditor; the store; a notification; a status.

Term records: notification — one delivery record, carrying notification id, recipient ref, payload, created at, status and, once it ends, one terminal stamp.

Term record verbs: identify, allocate, supply, reuse, carry, compare, trim, normalize, case-fold, read, stand, stamp, offer, delete, hold, record, answer, accept, leave, refuse, order, write, change, move, set, share, shrink, keep, own, evaluate, choose, retry, create, deliver, validate, deduplicate, purge, gate, expire, enumerate, call, serialize, compose, guard, declare, find, reconstruct, raise, require, exceed.

Term value sets: status = pending delivered failed expired. terminal stamp = delivered at failed at expired at.

Term bounds: string cap (the deployment’s bound on a string input’s length).

Term cadences: empty — a delivery window is the composing pattern’s (Operation 14b, Composition note 7).

Term qualifiers: migrated — rewritten in GRACE lang v0.35 (2026-09-12).

Term terms: notification, notification id, recipient ref, payload, seam, transition, business caller, now, string cap, status, terminal stamp, created at, terminal transition.

Create

The behavior that records a new delivery record. It assigns a fresh Notification Id (host-allocated at the seam), records Recipient Ref, Payload, and Created At, and returns the Notification Id (or Invalid Request). The record enters Pending.

Kind: Operation

Deliver

The behavior that records successful delivery, transitioning a Pending notification to terminal Delivered and stamping Delivered At. Rejected for an unknown id (Not Known) or a non-Pending notification (Not Pending).

Kind: Operation

Fail

The behavior that records a definitive delivery failure, transitioning a Pending notification to terminal Failed and stamping Failed At. Same preconditions as Deliver.

Kind: Operation

Expire

The behavior that records lapse of the delivery window without an outcome, transitioning a Pending notification to terminal Expired and stamping Expired At. Same preconditions as Deliver.

Kind: Operation

Status Of

The read-only query returning the full record for a Notification Id in any state, or Not Known if no notification has that id. Never transitions.

Kind: Operation

Pending For

The read-only query returning the Notification Id values of all Pending notifications addressed to a Recipient Ref (unordered; excludes terminals, Invariant 7). Never transitions.

Kind: Operation

Notification Id

The opaque, immutable identity of a notification — host-allocated at the I/O seam (the injected id), produced by Create, never reused (Invariant 6). The recipient and payload are properties of the notification, not its identity.

Kind: Field Field of: the notification record Projection: notification_id

Recipient Ref

The opaque reference to the intended recipient. Set on Create, immutable. Equality is byte-exact (no normalization); Pending For filters on it.

Kind: Field Field of: the notification record Projection: recipient_ref

Payload

The opaque content of the notification. Set on Create, immutable, stored and returned unchanged — never parsed, validated, or interpreted by the atom.

Kind: Field Field of: the notification record Projection: payload

Created At

The wall-time the notification was created, stamped from the injected clock at Create. Immutable (Invariant 1). The lower bound for any terminal timestamp (Invariant 8).

Kind: Field Field of: the notification record Projection: created_at

Status

The notification’s lifecycle state — Pending, Delivered, Failed, or Expired. Set to Pending on Create; transitions once to a terminal (Invariant 2).

Kind: Field Field of: the notification record Projection: status

Delivered At

The wall-time delivery was confirmed, stamped at Deliver. Present iff status is Delivered (Invariants 3 and 4); immutable once set.

Kind: Field Field of: the notification record Projection: delivered_at

Failed At

The wall-time the failure was recorded, stamped at Fail. Present iff status is Failed (Invariants 3 and 4); immutable once set.

Kind: Field Field of: the notification record Projection: failed_at

Expired At

The wall-time the expiry was recorded, stamped at Expire. Present iff status is Expired (Invariants 3 and 4); immutable once set.

Kind: Field Field of: the notification record Projection: expired_at

Pending

The non-terminal state of a created notification whose delivery has not been confirmed, failed, or expired. May transition to exactly one terminal; Pending For returns only records in this state.

Kind: Member Member of: the notification status Role: Outcome

Delivered

The terminal state of a notification that reached the recipient (transport confirmed). Carries Delivered At; absorbing (Invariant 2).

Kind: Member Member of: the notification status Role: Outcome

Failed

The terminal state of a notification whose delivery was attempted and produced a definitive negative outcome. Carries Failed At; absorbing.

Kind: Member Member of: the notification status Role: Outcome

Expired

The terminal state of a notification whose delivery window lapsed without a recorded outcome. Carries Expired At; absorbing.

Kind: Member Member of: the notification status Role: Outcome

Now

The current clock reading every writing action consumes — supplied at the atom’s seam, never read inside the transition and never a signature parameter. Its only use is the immutable stamps inside committed transitions (Created At and the terminal stamp).

Kind: Parameter Parameter of: Create, Deliver, Fail and Expire Projection: now

Invalid Request

The refusal Create returns when a request field fails its rule — an empty or whitespace-only Recipient Ref (Operation 4, String 5), or a string over the deployment’s cap (String 6). An empty Payload is not a failure: Operation 5 accepts it.

Kind: Member Member of: the Create rejection Role: Outcome Projection: invalid-request

Not Known

The refusal Deliver, Fail, or Expire returns when the Notification Id references no notification in the store. Also the Status Of outcome for an unknown id.

Kind: Member Member of: the action rejection Role: Outcome Projection: not-known

Not Pending

The refusal Deliver, Fail, or Expire returns when the target is not in Pending — i.e., already in a terminal state.

Kind: Member Member of: the action rejection Role: Outcome Projection: not-pending


Standards references

  • Observer pattern (GoF) — Notification is the structured-natural-language realization of the notification object: the message delivered from subject to observer.
  • SMTP / RFC 5321 — SMTP (Simple Mail Transfer Protocol, defined in the Internet standard RFC 5321) email delivery as the canonical push transport. The three terminal states map directly to SMTP disposition: 2xx success (Delivered), 5xx permanent failure (Failed), and timeout without delivery (Expired).
  • HTTP webhooks — POST-to-URL delivery model standard in web systems. Deliver records a 2xx response; Fail records a 4xx/5xx or connection failure.
  • W3C Activity Streams 2.0 — semantic vocabulary for describing social and messaging events. Notification payloads in web deployments often conform to Activity Streams objects.
  • Apple Push Notification Service / Firebase Cloud Messaging — platform push notification services where Deliver corresponds to accepted delivery and Fail corresponds to a rejected or unregistered token.
  • Daniel Jackson, The Essence of Software — freestanding-atom posture; Payload as an opaque reference whose semantics are defined by the composing system.
  • Eiffel’s design-by-contract — preconditions on Deliver, Fail, and Expire; named rejection reasons.

Status

grounded on Final Critique 4 — 2026-06-18 — see the Ledger.

Ledger

status: grounded on Final Critique 4 — 2026-06-18
formal: verified — notification.als + 1 twin, 2026-06-03
last gate: 2026-06-18 — Final Critique 4, fresh reader — clean

open: none

Decisions

Directional changes only — the turns a future reader must know the pattern took, and why. Everything smaller lives in the commit that made it: git log -- atoms/notification.md.

  • 2026-09-12 — Rewritten in GRACE lang v0.35; nothing but language changed. Chose: labelled rules in fenced blocks, the six actions as a signature block, the three terminal transitions sharing one declared term and one precondition pair, the nine invariant numbers unchanged, Generation acceptance as conformance checks plus external checks ahead of Non-goals, Non-goals and Edge cases as two sections, the transition table kept beside the rules as the case space. Over: the prose spec. Because: the migration plan; cites.py --into notification prints nothing, so no number is frozen from outside, and this atom completes Notification Fanout’s constituent set with Subscription.

NOTE: End of Notification.