# Design - Flow-Native Email Notifications

Status: design-authority draft, doc-only
Date: 2026-06-03
Review anchor: PR
[`pingcap-inc/NexusCRM#1761`](https://github.com/pingcap-inc/NexusCRM/pull/1761)
as reviewed on 2026-06-03, with source issue
[`pingcap-inc/nexuscrm-pingcap-pack#2267`](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/2267).
Scope: migrated workflow email alerts, approval notification actions, Flow
send-email authoring, email-template syntax, and WorkflowAlert retirement.
Layout note: frontend path references in this issue design are implementation
anchors from the document's review window. Verify current frontend source
ownership against `frontend-l0-l1-ownership-inventory.md` before using them as
paths on `main`.
Related:
[`#784`](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/784),
[`#871`](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/871),
[`#652`](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/652),
[`#544`](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/544),
[`#1231`](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/1231),
[`#1033`](https://github.com/pingcap-inc/nexuscrm-pingcap-pack/issues/1033),
[`MetaDrivenPlatform4AI#1460`](https://github.com/pingcap-inc/MetaDrivenPlatform4AI/issues/1460),
[`l0-l1-standard.md`](l0-l1-standard.md),
[`action-taxonomy.md`](action-taxonomy.md).

Authority: this document is the target direction for issue #2267 and the review
answer for PR #1761. It supersedes the PR #1761 direction of adding
source-backed `workflow_alert` components and runtime Salesforce merge-field
compatibility. It does not rewrite historical current-state docs such as
`action-taxonomy.md`; it records the future target. Migration helpers are
release-owned auxiliary tooling, not L0 runtime authority or a required
NexusCRM-side merge gate.

Milestone: close the email-alert notification gap by making Flow the only new
automation authoring surface for outbound email, with no runtime WorkflowAlert
fallback after migration.

## Governing Principle

One fact, one SSoT, one read path. L0 owns generic engines and protocols;
L1/L2 assembles application behavior from metadata.

For notifications, this means:

- L0 owns generic mechanisms: metadata read/write, Flow execution, action
  dispatch, native template rendering, approval lifecycle event emission,
  identity and permission primitives, pack apply/verify, and import/migration
  tooling contracts.
- L1/L2 owns application behavior: objects, approval processes, notification
  Flows, email templates, recipient policy, sender policy, business conditions,
  and generated metadata after migration.
- Runtime must not treat Salesforce `WorkflowAlert`, Salesforce merge syntax,
  or migration artifacts as durable notification behavior.

Durable runtime path after cutover:

```text
event / trigger -> Flow metadata -> sendEmail -> EmailTemplate renderer -> provider
```

No durable runtime path should read these as notification policy after cutover:

- `_System_WorkflowAlert`;
- `ProcessAction.Alert`;
- Flow `emailAlert`;
- Salesforce `{!...}` merge syntax;
- auxiliary migration reports or generated private release artifacts.

## 0. Bottom Line

Do not support legacy WorkflowAlert at runtime.

The source issues expose real notification failures: migrated email alerts lose
template references, staging has no selectable templates, and Salesforce-shaped
template bodies do not render useful record content. PR #1761 tries to fix that
by making Salesforce WorkflowAlert a stronger L0 substrate: source-backed
`workflow_alert`, object-scoped WorkflowAlert dispatch, Salesforce-style
merge-field rendering, and WorkflowAlert-specific validation.

That is the wrong durable surface.

NexusCRM's target automation surface is Flow. Existing Salesforce WorkflowAlert
and approval Email Alert metadata should be treated as migration input. The
target runtime should execute Flow-native `sendEmail` action calls, render
NexusCRM-native template syntax, and fail closed when an unmigrated
WorkflowAlert dependency remains after cutover.

Decisions fixed by this document:

- **No source-backed `workflow_alert`.** App packs should not declare
  WorkflowAlert as installable notification behavior.
- **No legacy WorkflowAlert runtime.** Existing `_System_WorkflowAlert`,
  `ProcessAction.Alert`, and Flow `emailAlert` usages must be migrated before
  cutover.
- **Flow owns notification orchestration.** Conditions, related reads,
  recipients, loops, approval fan-out, and send timing live in Flow metadata.
- **The durable native action is `sendEmail`.** Existing `emailSimple` may be a
  compatibility alias while generated metadata is normalized, but it is not the
  new authoring name.
- **Template syntax is NexusCRM-native.** The first durable contract is
  double-brace syntax such as `{{Record.Name}}`, `{{User.Email}}`, and
  `{{Variables.ApprovalUrl}}`. Salesforce `{!Object.Field}` is migration input,
  not runtime syntax.
- **External email I/O is not allowed inside atomic Flow execution.** First
  implementation rejects/routes fault when `sendEmail` is reached in an atomic
  or preview context. A post-commit outbox is a later demand-gated substrate.
- **EmailTemplate is content.** Source-backed `email_template` can be valid if
  it is generic, uses native syntax, validates render variables, and carries no
  WorkflowAlert coupling.
- **Migration tooling is auxiliary.** A release may use deterministic L1 or
  release-owned helper tooling to inventory legacy dependencies and generate
  Flow metadata, but NexusCRM core does not own an in-tree migration command,
  operator playbook, or cutover gate.

Remaining questions are product/data questions, not architecture blockers:
which target environments must be migrated first, whether historical
WorkflowAlert rows are deleted or archived after cutover, and what approval URL
shape product wants exposed in email templates.

## 1. Issue And Source Evidence

Issue #2267 reports two user-visible failures:

- Approval-process and Flow email-alert actions cannot bind templates because
  staging has no selectable email-template rows.
- Imported Salesforce template text containing tokens such as
  `{!Account.Name}`, `{!Quote.GrandTotal}`, `{!Recipient.Name}`, or
  `{!ApproverName}` does not render useful record-specific content.

The related issues show this is a migration target problem, not proof that L0
should expand Salesforce WorkflowAlert:

| Source | Evidence | Design read |
|--------|----------|-------------|
| `nexuscrm-pingcap-pack#2267` | Empty template picker plus Salesforce merge tokens emitted literally. | Real notification gap, but the proposed shape inherits Salesforce WorkflowAlert. |
| `#784` / `#871` | `migrate-workflow-flows` drops `emailTemplate` from `emailAlert` action nodes. | The fix is generated Flow send-email inputs preserving template references, not restored WorkflowAlert lookup. |
| `MetaDrivenPlatform4AI#1460` | Approval templates have missing bodies and Salesforce merge contexts. | Template content and token translation are importer/app-pack concerns. |
| `#652` / `#544` | Approval action execution and broader approval runtime gaps remain open. | Approval notifications need a lifecycle bridge to Flow. |
| `#1231` / `#1033` | Multi-approver notification/task delivery is incomplete. | Recipient fan-out belongs in approval context plus Flow iteration. |

The mistake would be to treat Salesforce workflow formatting and WorkflowAlert
metadata as the runtime oracle. They are import source shapes. The NexusCRM
runtime target is Flow metadata plus a native send-email action.

## 2. Current State Inventory

| Surface | Current behavior | Evidence | Target disposition |
|---------|------------------|----------|--------------------|
| `_System_EmailTemplate` | L0 has a table and `ActionService.LoadAndRenderTemplate` loads active templates by Id. | `backend/internal/features/automation/action_service.go`; `backend/internal/bootstrap/system_tables.json`. | Keep as reusable content, with native syntax validation. |
| `_System_WorkflowAlert` | L0 has a table with object, name, template, recipients, sender, and CC fields. | `backend/internal/bootstrap/system_tables.json`. | Migration input only; no active runtime after cutover. |
| Approval `ProcessAction.ActionType = Alert` | `ApprovalService` can route alerts through WorkflowAlert or inline `EmailTemplateId`. | `backend/internal/features/automation/approval_service.go`; `docs/design/action-taxonomy.md`. | Migrate to approval lifecycle Flow launch; no new authoring. |
| Flow `actionType = emailAlert` | `FlowExecutor` special-cases `emailAlert` and delegates to `ApprovalService` / WorkflowAlert. | `backend/internal/features/automation/executor_actions.go`. | Migrate to `sendEmail`; remove after release-owned zero-dependency cleanup. |
| Flow `actionType = emailSimple` | `FlowExecutor` maps action inputs to `ActionTypeSendEmail`. | `backend/internal/features/automation/executor_actions.go`; `shared/pkg/constants/actions.go`. | Temporary alias or existing-data compatibility; durable name is `sendEmail`. |
| Atomic Flow validation | Atomic flows reject `emailAlert` because SMTP send would hold a transaction across external I/O. | `backend/internal/features/automation/flow_executor.go`. | Extend the same class of guard to native `sendEmail` until an outbox exists. |
| Approval Email Alert UI | The UI exposes Email Alert authoring, template picker, sender, CC, and fixed recipient controls. | `frontend/src/features/approvals/components/*`; `frontend/src/infrastructure/api/approvals.ts`. | Hide/remove new WorkflowAlert authoring; expose Flow notification authoring instead. |
| L0/L1 standard | Generic L0 must not copy Salesforce WorkflowAlert vocabulary as a platform fact. | `docs/design/l0-l1-standard.md`. | This doc makes that warning binding for issue #2267. |

The current code has enough pieces to send email, but the pieces are joined
through a Salesforce WorkflowAlert-shaped compatibility path. The target is to
keep the generic pieces and remove the WorkflowAlert dependency.

### 2.1 Canonical Name Matrix

`sendEmail` is the canonical Flow action name for new authoring and generated
metadata. Current code does not yet satisfy that contract:

| Layer | Current state | Target |
|-------|---------------|--------|
| Flow metadata action type | `emailAlert` and `emailSimple` exist; `sendEmail` is not recognized by the backend as the native send path. | `sendEmail`. |
| Temporary alias | `emailSimple` maps to backend `_System_Action.Type = SendEmail`. | Accepted only for existing/generated legacy metadata until an alias-removal gate. |
| Deprecated migration input | `emailAlert` delegates to WorkflowAlert dispatch. | Migration input only; no new authoring and no runtime dispatch after cutover. |
| Frontend import | `frontend/src/core/constants/FlowConstants.ts` maps `emailAlert` and `emailSimple` to UI `sendEmail`. | Preserve import compatibility while making canonical state explicit. |
| Frontend export | `frontend/src/features/flows/utils/mappers/helpers.ts` maps UI `sendEmail` back to Salesforce `emailAlert`. | Stop re-legacying `sendEmail`; new export emits canonical `sendEmail`. |
| Backend executor enum | `_System_Action.Type = SendEmail` can remain the internal executor selector. | Backend recognizes Flow `sendEmail` and maps it to the generic `SendEmail` executor. |

Implementation consequence: a migration or UI round-trip must never convert a
canonical `sendEmail` action back into `emailAlert`.

### 2.2 Runtime Classification

| Surface | Target classification | Runtime after cutover |
|---------|----------------------|-----------------------|
| `_System_WorkflowAlert` | Migration input or historical inventory only. | No active notification dispatch. |
| `ProcessAction.Alert` | Migration input only. | No active notification dispatch. |
| Flow `emailAlert` | Migration input / deprecated alias only. | No active notification dispatch. |
| Flow `emailSimple` | Temporary compatibility alias. | Allowed only until alias-removal gate. |
| Salesforce `{!...}` syntax | Migration translation input. | Not runtime grammar. |
| Migration reports / generated patches | Release-owned evidence and procedure. | Not policy and not runtime authority. |
| `_System_EmailTemplate` | Durable content substrate. | Rendered only through native context supplied by Flow. |
| Flow `sendEmail` | Durable orchestration action. | Only notification send path. |

## 3. Target Architecture

### 3.1 L0/L1 Boundary And Durable Path

L0's durable notification responsibility is to interpret generic metadata:
Flow execution, native template rendering, action dispatch, permission-aware
context handling, and provider handoff. L0 must not own application recipient
policy, sender policy, approval notification branching, or template semantics
that depend on CRM/PingCAP workflow facts.

L1/L2 owns the assembled behavior: which Flow runs, which template it sends,
which recipients it computes, which sender it uses, and which business
condition decides whether notification is needed.

The single durable read path is:

```text
event / trigger -> Flow metadata -> sendEmail -> EmailTemplate renderer -> provider
```

Every other notification-shaped surface in the current tree is either
migration input, a temporary alias, or historical inventory.

### 3.2 Authority Table

| Concept | Owner | Runtime reader | Decision |
|---------|-------|----------------|----------|
| Email template content | L1/L2 pack or admin metadata | `sendEmail` renderer | Keep, validate with native syntax. |
| Notification orchestration | Flow metadata | Flow executor | Flow owns branching, lookups, recipient fan-out, and calls to `sendEmail`. |
| Approval lifecycle trigger | Approval engine | Approval-to-Flow bridge | Approval launches Flow with an approval context. |
| Salesforce WorkflowAlert metadata | Migration/import tooling | Migration only | No runtime dispatch after cutover. |
| Salesforce merge syntax | Migration/import tooling | Migration only | Translate to native syntax when exact, otherwise report. |
| Auxiliary migration helpers | Release/tooling owner | Not a runtime reader | May compile legacy alerts to Flow-shaped metadata and produce evidence outside L0 core. |
| Native template syntax | L0 renderer contract | `sendEmail` renderer and preview | Double-brace syntax is the first SSoT. |

### 3.3 Flow `sendEmail` Action Contract

The durable action name is `sendEmail`.

Existing `emailSimple` action calls may be accepted temporarily as an alias for
generated or pre-existing Flow metadata. New authoring and generated packs
should emit `sendEmail`. A later cleanup can remove the alias after verify gates
prove no active `emailSimple` dependency remains, but that cleanup is separate
from WorkflowAlert retirement.

Minimum action inputs:

| Input | Required | Contract |
|-------|----------|----------|
| `templateId` | one of `templateId` / `templateDeveloperName` | Preferred runtime reference. Must point at an active `_System_EmailTemplate`. |
| `templateDeveloperName` | one of `templateId` / `templateDeveloperName` | Apply/activation resolves to template Id when authored by pack business key. Runtime does not parse folder paths. |
| `to` | yes | Email address, User Id, Flow variable, or Flow collection of those values. |
| `cc` / `bcc` | no | Same shape as `to`; empty lists are allowed. |
| `from` | no | Sender policy or explicit sender address, validated against platform sender rules. |
| `record` | no | Record context for `{{Record.*}}`; default is `$Record` when present. |
| `variables` | no | Named values or maps for `{{Variables.*}}` and approval context. |
| `failureMode` | no | `fault` by default; future values may opt into explicit best-effort behavior. |

Minimum action outputs:

| Output | Contract |
|--------|----------|
| `sentCount` | Number of accepted recipients after validation/deduplication. |
| `recipientResults` | Per-recipient result records when the action runs synchronously. |
| `messageId` | Optional provider message id when a single provider id exists. |

Failure behavior:

- Missing template, inactive template, unresolved template variables, invalid
  recipient expressions, and sender-policy errors fail before send.
- In Flow, failure follows the action's fault connector when present; otherwise
  it faults the Flow element.
- The renderer must not deliver literal unresolved tokens.
- Best-effort behavior must be explicitly authored later; it is not inherited
  from Salesforce WorkflowAlert.

Observability:

- `sendEmail` execution must be visible in Flow element audit/logging with
  action name, template reference, recipient count, failure code, and provider
  result when available.
- Migration provenance should be retained on generated Flow/template artifacts
  and in the migration report, not used as runtime policy.

Security and visibility:

- Flow must construct the values it passes to `sendEmail` through the normal
  Flow read/expression paths. The renderer should not raw-read arbitrary
  related records to satisfy template variables.
- Rendering `{{Record.*}}` and `{{Variables.*}}` uses the values present in the
  Flow context. If a variable is absent, rendering fails closed.
- If a future renderer reads records directly, it must go through the
  authoritative object, record, and field visibility paths and add negative
  tests before advertising the surface.

### 3.4 Native Template Syntax

The first durable template syntax is double-brace references. This is a target
contract; current `main` does not yet implement every root below.

| Root | Meaning |
|------|---------|
| `{{Record.Field}}` | Field from the explicit `record` input or `$Record`. |
| `{{User.Field}}` | Running user / Flow user context supplied to the action. |
| `{{Variables.Name}}` | Named scalar or map explicitly supplied by Flow. |
| `{{Approval.Field}}` | Approval context explicitly supplied by the approval-to-Flow bridge. |

`Owner` is not a magic runtime read path. If authoring wants
`{{Owner.Name}}`, Flow or the renderer contract must supply an owner projection
explicitly and validate it like any other declared context. The renderer must
not raw-read Owner or related records to satisfy missing variables.

Rules:

- No runtime support for Salesforce `{!Object.Field}` syntax.
- No runtime parsing of Salesforce folder references such as
  `unfiled$public/Foo`.
- No ad hoc WorkflowAlert roots such as `Recipient` or `ApproverName`.
- No object-name inference when Flow can pass an explicit record.
- Every token in subject, HTML body, and plain body must validate against the
  declared render context before send.
- `EmailTemplate` stores content plus declared native variable requirements. It
  does not store an application-specific runtime `MergeContext` that lets L0
  infer app roots or related-record reads.
- Flow `sendEmail` supplies `record`, `variables`, and approval context
  explicitly.

Validation timing:

| Time | Validation |
|------|------------|
| Template save / pack apply | Syntax is valid; every token root is allowed; declared variable requirements are well-formed. |
| Flow activation / pack verify | The `sendEmail` action supplies every required template variable or context root. |
| Render preview | Substitution validates without sending external email. |
| Runtime send | Final context is checked; unresolved or unauthorized variables fail before provider send. |

Null and missing behavior:

- Missing required root or key is an error.
- Missing record field is an error unless the template declared the variable as
  nullable and the context supplied the key with a null value.
- Present nullable null renders as an empty string by default, with future
  formatting options handled as a separate template-rendering design slice.
- Present but unauthorized values must not be rendered; the action should fail
  before send.

Future formula-expression rendering is allowed only as a new design slice that
binds to the formula language SSoT. It must not be smuggled in as Salesforce
merge-field compatibility.

### 3.5 Approval-To-Flow Bridge

Approval notifications launch Flow.

Target implementation: approval lifecycle metadata selects a Flow and passes an
approval context. `ProcessAction.Alert` is migration input only.

The bridge is deliberately small. It contains no email templates, recipients,
sender policy, notification branching, or business conditions. Those are L1/L2
Flow facts. The bridge owns only lifecycle event emission, target record
identity, approval context, and Flow selection.

The approval event context passed into Flow should include:

| Field | Meaning |
|-------|---------|
| `Approval.ActionEvent` | Submit, step approval/rejection, final approval/rejection, recall, or reassignment. |
| `Approval.ProcessDefinitionId` | Approval process definition. |
| `Approval.ProcessInstanceId` | Runtime approval instance when present. |
| `Approval.WorkItemIds` | Current work items affected by the lifecycle event. |
| `Approval.TargetObjectApiName` | Target record object. |
| `Approval.TargetRecordId` | Target record Id. |
| `Approval.SubmitterUserId` | User who submitted the record when known. |
| `Approval.ActorUserId` | User performing the current approval action when known. |
| `Approval.ApproverUserIds` | Intended approver recipients for fan-out when known. |
| `Approval.Comments` | Comments captured on the lifecycle action when present. |
| `Approval.RequestUrl` | Product-owned URL field; see remaining product question. |

The Flow receives the target record separately as `$Record` / `record`, then
uses `sendEmail` for notifications. A multi-approver step is represented as a
collection and a Flow loop or collection-aware `to` input, not as a
WorkflowAlert recipient vocabulary.

Current correctness risk: approval `ProcessAction.Alert -> WorkflowAlert`
compatibility appears to use name-only WorkflowAlert lookup, while Flow
`emailAlert` is object-scoped. Even before removal, compatibility code must not
rely on ambiguous name-only lookup for active sends.

### 3.6 No Legacy WorkflowAlert Runtime

After cutover, runtime notification execution must not dispatch from
`_System_WorkflowAlert`.

Release-owned cutover work:

1. Inventory live dependencies:
   - `_System_WorkflowAlert` rows;
   - `ProcessAction.Alert` rows;
   - Flow `actionType = emailAlert` nodes;
   - template references;
   - recipient definitions;
   - sender policies;
   - approval phase bindings.
2. Convert each dependency to Flow-native metadata.
3. Translate templates to native syntax when exact.
4. Report unsupported tokens, recipient shapes, missing templates, and
   ambiguous senders as migration blockers.
5. Capture release-owned evidence that no active WorkflowAlert dependency
   remains in the official packs and target environments chosen for that
   rollout.
6. Disable/remove WorkflowAlert dispatch paths.

Post-cutover behavior:

- New pack manifests cannot declare `workflow_alert`.
- New frontend authoring cannot create `_System_WorkflowAlert` rows.
- Runtime fails closed if an unmigrated WorkflowAlert dependency is reached.
- Emergency rollback is deployment rollback or an explicit temporary
  compatibility build, not the default architecture.

Zero active dependency means no active runtime dependency on:

- `_System_WorkflowAlert`;
- `ProcessAction.Alert`;
- Flow `emailAlert`.

It does not require zero historical rows. Historical rows may remain as audit,
inventory, or rollback artifacts only when no active runtime path can read them
as notification behavior.

### 3.7 Migration Compiler

Salesforce workflow and approval email artifacts are compiled into Flow-native
metadata.

Workflow rule / WorkflowAlert import:

1. Convert workflow criteria into Flow start/decision metadata.
2. Convert WorkflowAlert send behavior into a `sendEmail` action call.
3. Resolve template references to EmailTemplate Ids or pack-local template
   references.
4. Convert recipients into Flow variables, lookups, loops, collection inputs,
   or resolver action calls.
5. Translate template tokens only where exact.
6. Emit a migration report for every unsupported shape.

Approval Email Alert import:

1. Convert each approval alert phase into approval lifecycle Flow launch
   metadata.
2. Pass target record and approval event context into the Flow.
3. Invoke `sendEmail` from Flow.
4. Leave the old `Alert` row unused or delete/archive it after cutover,
   depending on the data-retention decision.

Template translation:

| Salesforce input | Native output when safe | Otherwise |
|------------------|-------------------------|-----------|
| `{!<TriggerObject>.Name}` | `{{Record.Name}}` | Report if trigger object is unknown or field does not exist. |
| `{!User.Email}` | `{{User.Email}}` | Report if the desired actor is not the running user. |
| `{!ApprovalRequest.Internal_URL}` | `{{Approval.RequestUrl}}` | Report until product approves the approval URL contract. |
| `{!Recipient.Name}` | No automatic mapping. | Report and require explicit Flow recipient variables. |
| `{!ApproverName}` | No automatic mapping. | Report and require explicit Flow variables. |
| `unfiled$public/Foo` | Pack-local EmailTemplate reference resolved at apply/import time. | Report if not uniquely resolvable. |

### 3.8 Auxiliary Migration Helper Scope

Legacy WorkflowAlert conversion should be repeatable, but the helper is not a
durable L0 fact. PR-B1 moves the Salesforce-specific helper out of NexusCRM
core; this design no longer requires an in-tree command, Codex skill, operator
playbook, or checked-in zero-dependency command as a merge prerequisite.

Auxiliary L1 or release-owned tooling may provide:

- inventory of `_System_WorkflowAlert`, `ProcessAction.Alert`, Flow
  `emailAlert`, template references, sender, recipient, and approval phase
  bindings;
- dry-run reports for proposed Flow/email-template/approval changes;
- generated Flow definitions or patches that call `sendEmail`;
- approval lifecycle Flow launch bindings or migration patches;
- native EmailTemplate payloads with double-brace syntax where translation is
  exact;
- release-owned evidence that unsupported items were migrated, explicitly
  disabled, or product-waived.

Recommended helper rules:

- Idempotent: running the same input twice produces the same output and report.
- Deterministic: generated names and keys are stable and derived from source
  object/action/template identity.
- Conservative: unsupported tokens or recipient shapes are reported, not
  guessed.
- Layer-safe: PingCAP/customer template bodies remain in the target pack or
  migration output, not in public-core L0.
- Runtime-clean: generated metadata must not require `_System_WorkflowAlert`.
- Reviewable: generated Flows should include source provenance pointing back to
  the legacy alert/action/template they replaced.

Out of scope for auxiliary helpers:

- inventing new recipient semantics not expressible in Flow;
- translating ambiguous Salesforce tokens by name similarity;
- editing runtime code as part of environment remediation;
- writing private tenant payload into this public-core repository;
- becoming a second durable read path for notification behavior.

### 3.9 PR #1761 Salvage Split

PR #1761 contains generic email-template substrate work mixed with
WorkflowAlert expansion. Future implementation should split them.

Keep if WorkflowAlert-free:

| Candidate | Why it can be kept |
|-----------|--------------------|
| Source-backed `email_template` component kind | EmailTemplate is durable content, not notification behavior. |
| Communication install phase before automation references | Templates must exist before Flows/actions reference them. |
| `OwnerPack` ownership | Pack-owned template lifecycle needs deterministic ownership. |
| Deterministic IDs | Generated references need stable IDs across apply/verify. |
| DeveloperName / business-key reference resolution | Packs need stable authoring keys that resolve before runtime. |
| Apply / live projection / drift verify / orphan sweep | Template content needs the same lifecycle safety as other managed metadata. |

Reject as durable L0:

| Candidate | Why it is rejected |
|-----------|--------------------|
| Source-backed `workflow_alert` | Makes WorkflowAlert installable runtime behavior and creates a second notification SSoT. |
| WorkflowAlert runtime dispatch | Keeps a second read path beside Flow `sendEmail`. |
| Runtime Salesforce merge-field compatibility | Makes import syntax into runtime grammar. |
| WorkflowAlert recipient vocabulary | Turns Salesforce recipient values into generic L0 notification policy. |
| Broad runtime `MergeContext` | Risks making EmailTemplate hidden application logic and a second related-record read path. |

### 3.10 Transaction And Preview Semantics

First implementation:

- The external email I/O capability is allowed only outside atomic Flow
  execution, regardless of spelling: `emailAlert`, `emailSimple`, or
  `sendEmail`.
- Actual send is rejected in preview/no-DML mode.
- EmailTemplate render preview/diagnostics is allowed as separate non-send
  work.
- If a fault connector exists, the rejection routes through the fault path.
- If no fault connector exists, the Flow element fails.

Why: SMTP/provider calls are external I/O. Holding record-write locks across
external I/O is unsafe. The current `emailAlert` atomic-flow guard identifies
the right failure class and should be generalized to native send-email.

Deferred substrate: post-commit notification outbox.

Open the outbox work only when product needs email from atomic record-write
flows. The outbox design must cover persistence, retry, idempotency,
observability, provider errors, and cancellation before it becomes the default
send path.

## 4. Key Decisions

| Decision | Consequence |
|----------|-------------|
| D-1: Flow is the only new notification orchestrator. | New notification PRs add Flow action contract, approval-to-Flow launch, or migration tooling, not WorkflowAlert behavior. |
| D-2: `sendEmail` is the durable native action name. | `emailSimple` is a compatibility alias only; new authoring and generated packs emit `sendEmail`. |
| D-3: EmailTemplate is content. | Source-backed `email_template` can proceed only with native syntax validation and no WorkflowAlert coupling. |
| D-4: WorkflowAlert is migration input only. | Source-backed `workflow_alert` is rejected; runtime dispatch is removed after zero-dependency migration. |
| D-5: Runtime does not normalize Salesforce merge syntax. | `{!Object.Field}` support belongs in importer translation, not `LoadAndRenderTemplate`. |
| D-6: Approval notifications launch Flow. | `ProcessAction.Alert` migrates to Flow launch; multi-approver fan-out is Flow-owned. |
| D-7: Native templates fail closed. | Missing variables or unsupported roots fail before send; no literal-token delivery. |
| D-8: First implementation is non-atomic only. | Atomic Flow send-email requires a future post-commit outbox design; the guard applies to the email I/O capability regardless of alias. |
| D-9: Migration helpers are auxiliary. | Cutover evidence is release-owned; NexusCRM core does not require an in-tree migration command, skill, playbook, or zero-dependency command. |
| D-10: Salvage `email_template`, reject `workflow_alert`. | Generic template lifecycle work from #1761 can land only when separated from WorkflowAlert runtime behavior. |

## 5. Non-Goals

- Do not implement Salesforce Workflow Rules as a supported runtime surface.
- Do not implement Salesforce WorkflowAlert as an L1 pack component.
- Do not add Salesforce `{!...}` merge syntax as runtime template syntax.
- Do not parse Salesforce email-template folder paths at runtime.
- Do not keep a best-effort WorkflowAlert fallback after cutover.
- Do not move PingCAP-specific template bodies or recipient policy into the
  public-core repo.
- Do not implement the post-commit notification outbox in the same PR as the
  first Flow-native `sendEmail` contract.
- Do not make template rendering raw-read related records to satisfy missing
  variables.
- Do not make auxiliary migration tooling a durable runtime authority or second
  notification read path.
- Do not let frontend export convert canonical `sendEmail` back to
  `emailAlert` after the canonical contract lands.

## 6. Runway And Gates

### Phase 0 - Stop The Wrong Expansion

Work:

- Do not merge source-backed `workflow_alert`.
- Split any generic renderer safety work from WorkflowAlert expansion.
- Retarget #2267 and #1761 toward this document.
- Treat current WorkflowAlert rows as migration inventory only.

Gate:

- No new default registry entry, manifest kind, frontend create path, or runtime
  normalization path for WorkflowAlert lands without explicitly overriding this
  document.

### Phase 1 - Flow `sendEmail`

Work:

- Add/document `sendEmail` action metadata.
- Keep `emailSimple` as an alias if needed for existing Flow metadata.
- Make frontend import/export and backend execution preserve canonical
  `sendEmail`; do not round-trip it back to `emailAlert`.
- Validate template references, recipients, variables, sender policy, and
  native template tokens.
- Reject `sendEmail` in preview and atomic Flow contexts.
- Add synthetic-object tests.

Gate:

- A Flow can send a templated email with `{{Record.*}}`, `{{User.*}}`, and
  `{{Variables.*}}` without reading `_System_WorkflowAlert`.
- UI/backend round-trip preserves canonical `sendEmail`.
- Unresolved template variables fail before send.
- Atomic and preview contexts fail closed or route fault.

### Phase 2 - Approval-To-Flow Launch

Work:

- Implement or activate approval lifecycle Flow launch.
- Pass the approval event context into Flow.
- Model multi-approver fan-out as Flow collection iteration or collection-aware
  `to`.
- Add verification that distinguishes migrated Flow notifications from
  unmigrated `ProcessAction.Alert`.

Gate:

- Approval submit/step/final lifecycle events can notify intended approvers
  through Flow `sendEmail` with no WorkflowAlert row.

### Phase 3 - Migration And Cutover

Work:

- Optionally use L1 or release-owned helper tooling for inventory, dry-run
  reports, and generated pack patches.
- Convert Salesforce workflow email alerts into Flow definitions.
- Convert approval Email Alert actions into approval lifecycle Flow launches.
- Translate templates to native syntax where exact.
- Emit migration reports for unsupported tokens, recipients, senders, or
  missing templates.
- Capture release-owned evidence for the chosen official packs and target
  environments before disabling WorkflowAlert runtime dispatch.

Gate:

- No active `_System_WorkflowAlert`, `ProcessAction.Alert`, or Flow
  `emailAlert` dependency remains in the official packs or target environments
  selected for that rollout.
- Release-owned evidence is attached to the cutover review, including
  unsupported-item disposition when helper tooling is used.
- Issues #784/#871 are fixed by generated Flow action inputs retaining template
  references, not by restoring WorkflowAlert lookup.
- Post-cutover runtime rejects any remaining WorkflowAlert dependency.

### Phase 4 - Freeze Or Remove WorkflowAlert Data

Work:

- Remove or hide WorkflowAlert authoring UI.
- Remove Flow `emailAlert` special-case execution and approval WorkflowAlert
  dispatch code.
- Decide data retention for historical `_System_WorkflowAlert` rows.

Gate:

- No production runtime code path reads `_System_WorkflowAlert` for active
  notification behavior.

## 7. Verification

Doc validation:

```bash
git diff --check -- docs/design/issue-2267-flow-native-email-notifications.md
```

Implementation validation for future PRs:

- Flow `sendEmail` tests for native template variables, unresolved-variable
  failure, recipient validation, sender policy, fault routing, and provider
  failure behavior.
- Canonical-name tests proving frontend import/export and backend execution
  preserve `sendEmail`, treat `emailSimple` as a temporary alias, and keep
  `emailAlert` migration-only.
- Template validation tests over subject, HTML body, and plain body.
- Native render-context tests for declared roots, required/nullable variables,
  missing-key failure, nullable null rendering, and no raw related-record read.
- Negative tests proving runtime does not render Salesforce `{!Object.Field}`
  syntax unless migration translated it first.
- Approval-to-Flow tests for each supported lifecycle phase.
- Multi-approver fan-out regression tests tied to #1231/#1033.
- Helper-owned migration tests, when such tooling changes, proving Salesforce
  workflow email alerts become Flow `sendEmail` calls with template references
  preserved.
- Release evidence proving no active `ProcessAction.Alert`, Flow `emailAlert`,
  or `_System_WorkflowAlert` dependency remains for the selected rollout scope.
- Negative post-cutover runtime tests proving unmigrated WorkflowAlert
  dependencies fail closed.
- Guardrail tests preventing source-backed `workflow_alert` from entering the
  default pack registry.
- Source-backed `email_template` lifecycle tests only if the component lands
  WorkflowAlert-free: deterministic IDs, owner pack, reference resolution,
  drift verify, and orphan sweep.
- Atomic and preview tests proving `sendEmail` rejects/routes fault rather than
  sending inside unsafe contexts.

## 8. Remaining Questions

These questions do not change the architecture above. They are product,
rollout, or retention decisions needed before full cutover.

| Question | Owner | Blocks |
|----------|-------|--------|
| Which environments and official packs must prove zero WorkflowAlert dependency before WorkflowAlert dispatch is disabled? | Product / release owner | Phase 3 cutover evidence. |
| After migration, should historical `_System_WorkflowAlert` rows be deleted, kept read-only for audit, or moved into migration artifacts outside runtime tables? | Product / data-retention owner | Phase 4 cleanup. |
| What exact approval URL should `{{Approval.RequestUrl}}` expose, and who owns URL stability? | Product / frontend / platform routing owner | Approval template parity for links. |
| Is post-commit notification outbox required for the first production milestone, or can atomic Flow email remain unsupported until demand appears? | Product / platform owner | Any requirement to send email from atomic record-write flows. |
| Which repository or artifact store owns generated migration reports and generated pack patches for private environments? | Release / tooling owner | Phase 3 review and audit trail when helper tooling is used. |

No remaining question reopens WorkflowAlert as a runtime compatibility surface.

## 9. PR #1761 Disposition

The current #1761 review loop is finding validation and lifecycle problems
inside the new WorkflowAlert/email-template source-backed substrate. Those
findings are useful symptoms, but this design changes the blocker:

- source-backed `workflow_alert` is the wrong target;
- runtime Salesforce merge-field normalization is the wrong target;
- object-scoped WorkflowAlert dispatch is migration inventory support at most,
  not the future notification architecture.

Recommended action:

- Do not land source-backed `workflow_alert`.
- Do not land runtime Salesforce merge-field normalization.
- Keep only generic email-template safety work that serves native
  Flow `sendEmail`.
- Retarget #2267 toward Flow-native `sendEmail`, approval-to-Flow launch,
  native template validation, migration-time template translation, and
  zero-dependency cutover.
