---
id: CAP-PFM-NOT-016
title: Author and publish association e-mail content
feature: FEAT-PFM-NOT-07
product_area: platform
module: notifications
revision: 1
status: draft
product_intent: proposed
implementation_evidence: observed
supersedes: []
source_sha:
  services-platform: ad3f6a6582843008ed0f5a17b131878d15829af1
  react-console: baf6dae382d6f41dce976c7f848667ccc37e076f
intent_sources:
  - >-
    services-platform:docs/communications/architecture.md#15. Decisions for
    Andrew
  - >-
    services-platform:docs/communications/architecture.md#2026-10-09 — Final
    acceptance
  - >-
    services-platform:docs/communications/architecture.md#16.5 CC and BCC
    (Andrew, 2026-10-09)
  - >-
    services-platform:docs/communications/architecture.md#18. Rework 2026-10-10:
    queue pointers, S3 for request data and bodies, SQS owns retries
purpose: >-
  Associations prepare checked e-mail drafts and publish stable wording for
  workflow delivery.
outcomes:
  - Associations publish reusable e-mail content for supported purposes.
  - >-
    Preview, readiness and test-send operations expose content preparation
    checks.
concepts:
  - CON-PFM-NOT-004
actors:
  - ACT-PFM-028
  - ACT-PFM-030
surfaces:
  - console
  - api
preconditions: []
postconditions: []
permissions:
  - key: platform.communication-templates.read
    where_checked: api
    scope: tenant
  - key: platform.communication-templates.create
    where_checked: api
    scope: tenant
  - key: platform.communication-templates.update
    where_checked: api
    scope: tenant
  - key: platform.communication-templates.delete
    where_checked: api
    scope: tenant
  - key: platform.communication-templates.publish
    where_checked: api
    scope: tenant
  - key: platform.communication-templates.test-send
    where_checked: api
    scope: tenant
rules:
  - id: BR-CAP-PFM-NOT-016-01
    statement: >-
      Platform-rendered purposes require an association's published template;
      Suggested starter content is authoring help rather than a delivery
      fallback.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/lifecycle/contact-role-compliance-communication.integration.spec.js#offers
      starter text that the purpose itself never sends
    origin: none
  - id: BR-CAP-PFM-NOT-016-02
    statement: >-
      Purpose discovery lists only Platform-rendered purposes and their
      metadata-provided records; Template operations reject caller-rendered and
      Marketing-version-owned purposes by name.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/templates/caller-rendered-purposes.integration.spec.js#refuse
      a template for the caller-rendered purpose
    origin: none
  - id: BR-CAP-PFM-NOT-016-03
    statement: >-
      Template drafts require a trimmed, bounded name, known purpose, valid
      reply-to and up to ten canonical, distinct copy and blind-copy addresses
      each; invalid content can remain saved with publication findings.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/persistence/templates.integration.spec.js#refuses
      more copies than one message may carry, naming the field
    origin: none
  - id: BR-CAP-PFM-NOT-016-04
    statement: >-
      Active templates per purpose are limited; archived templates are excluded
      from the count; Exceeding the limit rejects creation without writing.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/persistence/templates.integration.spec.js#refuses
      a template once the cap is filled with ACTIVE ones
    origin: none
  - id: BR-CAP-PFM-NOT-016-05
    statement: >-
      Draft replacement checks the version read; Stale writes fail, and archived
      templates cannot be edited or published.
    kind: validation
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/e2e/templates.e2e.spec.js#refuses
      a stale version as 409
    origin: none
  - id: BR-CAP-PFM-NOT-016-06
    statement: >-
      Content checking rejects unsafe or unparseable Liquid, unavailable
      variables, included templates and prohibited HTML; Pasted Handlebars
      receives a syntax-specific finding with line and column.
    kind: validation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/lifecycle/contact-role-compliance-communication.integration.spec.js#refuses
      content that reads an undeclared variable and includes a file
    origin: none
  - id: BR-CAP-PFM-NOT-016-07
    statement: >-
      Publication creates the next immutable numbered version, storing its
      content hash and variable usage and updating the current-version
      reference; invalid drafts cannot publish; later edits leave earlier
      versions unchanged.
    kind: lifecycle
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/e2e/templates.e2e.spec.js#froze
      the draft as version 1, with the content hash and the variables it reads
    origin: >-
      services-platform:docs/communications/architecture.md#2026-10-09 — Final
      acceptance
  - id: BR-CAP-PFM-NOT-016-08
    statement: >-
      Archiving hides a template from normal lists but preserves its detail and
      versions for historical references.
    kind: lifecycle
    enforced_at:
      - api
      - service
    must_prove: false
    provenance: test
    source: >-
      services-platform:modules/communications/tests/e2e/templates.e2e.spec.js#keeps
      it readable by id, because it is kept for history
    origin: none
  - id: BR-CAP-PFM-NOT-016-09
    statement: >-
      A source record owns one active template per purpose; initial save creates
      and publishes version one; later partial saves preserve omitted fields,
      clear optional null fields and publish only changed content; invalid saves
      and dry runs write nothing; Clearing archives idempotently, and concurrent
      first saves converge on one template.
    kind: lifecycle
    enforced_at:
      - service
      - database
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/templates/owned-templates.integration.spec.js#keeps
      every field the partial content left out
    origin: >-
      services-platform:docs/communications/architecture.md#2026-10-09 — Final
      acceptance
  - id: BR-CAP-PFM-NOT-016-10
    statement: >-
      General template operations may edit owned content but cannot change
      ownership or purpose; Archiving an owned template is equivalent to
      clearing its owner setting.
    kind: invariant
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/templates/owned-templates.integration.spec.js#drops
      the purpose and the owner from the body before the operation sees them
    origin: none
  - id: BR-CAP-PFM-NOT-016-11
    statement: >-
      A Marketing message version owns one published template version; Identical
      saves return it without writes; changing published content is refused;
      Staff template lists omit these records.
    kind: invariant
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/templates/message-version-templates.integration.spec.js#refuses
      different content for a message version that is published, as an illegal
      operation
    origin: none
  - id: BR-CAP-PFM-NOT-016-12
    statement: >-
      Preview uses the delivery renderer on sample or supplied data and returns
      rendered output, empty merge-field names and validation findings; it
      neither persists nor sends a message.
    kind: policy
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/templates/render-preview.integration.spec.js#renders
      the same subject, character for character
    origin: none
  - id: BR-CAP-PFM-NOT-016-13
    statement: >-
      Test sends target a test mailbox or the requesting user's address, prefix
      the subject with [TEST], use purpose samples and follow ordinary queued
      delivery.
    kind: permission
    enforced_at:
      - api
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/e2e/templates.e2e.spec.js#refuses
      an address that is neither a test mailbox nor the caller’s own as 403
    origin: none
  - id: BR-CAP-PFM-NOT-016-14
    statement: >-
      Readiness evaluates up to one hundred records or templates, reporting
      whether sending is possible and distinguishing absent templates from
      unpublished ones.
    kind: policy
    enforced_at:
      - service
    must_prove: false
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/requests/handle-invocation.integration.spec.js#answers
      not ready, with no_published_version, for a template that has only a draft
    origin: none
  - id: BR-CAP-PFM-NOT-016-15
    statement: >-
      Rendering reads purpose-declared record fields, including association
      custom fields; missing record fields produce empty text instead of
      failing.
    kind: calculation
    enforced_at:
      - service
    must_prove: true
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/services/requests/record-merge-fields.integration.spec.js#renders
      the contact's value for the custom field into the stored HTML body
    origin: >-
      services-platform:docs/communications/architecture.md#2026-10-09 — Final
      acceptance
scenarios:
  - id: SC-CAP-PFM-NOT-016-01
    given: An organization type has no compliance content.
    when: >-
      Its administrator submits content with an undeclared variable and include,
      then corrects both problems.
    then: >-
      The invalid save reports both findings without persistence; corrected
      content publishes version one.
    provenance: test
    source: >-
      services-platform:modules/communications/tests/integration/lifecycle/contact-role-compliance-communication.integration.spec.js
edge_cases: []
constraints: []
depends_on: []
related:
  - CAP-PFM-NOT-017
  - CAP-PFM-NOT-019
journeys:
  - JNY-PFM-NOT-006
open_questions: []
evidence:
  - >-
    Pinned evidence inventory 1:
    services-platform:docs/communications/architecture.md;
    services-platform:modules/communications/tests/e2e/templates.e2e.spec.js;
    services-platform:modules/communications/tests/integration/services/requests/handle-invocation.integration.spec.js
  - >-
    Pinned evidence inventory 2:
    services-platform:modules/communications/tests/integration/lifecycle/contact-role-compliance-communication.integration.spec.js;
    services-platform:modules/communications/tests/integration/services/templates/owned-templates.integration.spec.js;
    services-platform:modules/communications/tests/integration/services/requests/record-merge-fields.integration.spec.js
  - >-
    Pinned evidence inventory 3:
    services-platform:modules/communications/tests/integration/services/templates/caller-rendered-purposes.integration.spec.js;
    services-platform:modules/communications/tests/integration/services/templates/message-version-templates.integration.spec.js
  - >-
    Pinned evidence inventory 4:
    services-platform:modules/communications/tests/integration/services/persistence/templates.integration.spec.js;
    services-platform:modules/communications/tests/integration/services/templates/render-preview.integration.spec.js
controls: [TC-AVL-004, TC-AVL-005, TC-AZ-001, TC-AZ-002, TC-AZ-003, TC-AZ-004, TC-CRY-001, TC-CRY-002, TC-CRY-003, TC-CRY-004, TC-DAT-001, TC-DAT-002, TC-DAT-003, TC-DAT-004, TC-IAM-001, TC-IAM-006, TC-LOG-004, TC-LOG-005, TC-MON-001, TC-PI-001, TC-PI-002, TC-PI-003, TC-PI-004, TC-PI-005, TC-PRV-001, TC-PRV-002, TC-TEN-001, TC-TEN-002, TC-TEN-003, TC-TEN-004, TC-VEN-001, TC-VEN-002, TC-VEN-003]
---

# Author and publish association e-mail content

## Purpose

Associations prepare checked e-mail drafts and publish stable wording for workflow delivery.

## How It Works

An association authors a draft for a known communication purpose. Platform checks template syntax, available merge fields and safe content. Draft findings can remain visible while invalid wording is edited. Publication freezes a numbered version instead of changing previous sends. Record-owned saves provide the same publishing outcome from an owning screen or service. Marketing-version content follows a separate immutable ownership contract. Preview and marked test delivery help authors inspect output before use. Missing published content blocks Platform-rendered workflow sending rather than substituting starter text.

The rule rows define this outcome; the acceptance set identifies existing proof and proposed checks. Implementation evidence alone does not approve the observed details.

## Relationships

This outcome supplies communication records used by acceptance, sending and inspection.
