# Action Taxonomy

Date: 2026-06-02
Last updated: 2026-06-05

Status: current action taxonomy SSoT.

## Purpose

The platform uses the word "action" for three different runtime concepts. Two
of them are carried in a wire field literally named `ActionType`, on two
unrelated tables, dispatched by two unrelated code paths. A third, `ActionEvent`,
sits one column over from one of those `ActionType` fields. Because the names
collide, a future change can silently merge them into "one action enum" — which
would re-couple dispatch surfaces that are deliberately independent.

This document is the single place that records what each taxonomy is, where its
constants live, who reads it, and the binding rule that keeps them apart. It is
the P2.1 spec deliverable; the constant-extraction PR that follows it
(replacing string literals with `ProcessActionType*` symbols) is justified by
the "where constants live" column below.

This is a **different axis** from the `_System_Action` ↔ `_System_StandardAction`
*table* separation documented in
[`action-table-bridge.md`](action-table-bridge.md). That doc keeps two *tables*
from being joined by name; this doc keeps three *enumerations* from being merged
into one. See "Relationship to the table bridge" below.

## The three axes

| Axis | Wire field | Values (HEAD) | Where the constants live | Who reads it | Question it answers |
|------|-----------|---------------|--------------------------|--------------|---------------------|
| 1. Automation dispatch mechanism | `_System_Action.Type` | `CreateRecord`, `UpdateRecord`, `DeleteRecord`, `SendEmail`, `CallWebhook`, `PushToConnector`, `Composite`, `Action`, `SubmitForApproval` | `shared/pkg/constants/actions.go` → `ActionType*` (e.g. `ActionTypeSendEmail`) | automation action service / flow executor (`features/automation/action_service.go`); routes an action row to its executor | **How does this action execute server-side?** |
| 2. Approval side-effect | `ProcessAction.ActionType` | `FieldUpdate`, `Alert`, native `Flow` (handled); `RecordLock` (declared in the `system_tables.json` column, not dispatched); `Task`, `OutboundMessage`, Salesforce `FlowAction` (declared/SF, unsupported -> unknown skip) | `shared/pkg/constants/process_actions.go` -> `ProcessActionType*` (e.g. `ProcessActionTypeAlert`) - a sibling file of `actions.go` in the same `constants` package | `ApprovalService.executeProcessActionMutations` (`FieldUpdate`), `executeProcessActionNotifications` (`Alert`, native `Flow`, and unknown default), `replayFieldUpdatesFromActions` / replay path (`FieldUpdate`), pack normalizer `normalizeApprovalProcessActionType` | **What side effect does this approval action emit?** |
| 3. Approval lifecycle phase | `ProcessAction.ActionEvent` | `Submit`, `StepApproval`, `StepRejection`, `FinalApproval`, `FinalRejection`, `Recall`, `Reassign` | *not constified in this PR* (out of scope) | `ApprovalService.prepareRequiredLifecycleActions` filter (`ActionEvent == '<phase>'`); callers at `approval_service.go` Submit / StepApproval / StepRejection / FinalApproval / FinalRejection / Recall sites | **At which point in the approval lifecycle does this action fire?** |

Axes 2 and 3 are two columns on the **same** `ProcessAction` row. A row says
both *what it does* (`ActionType`) and *when it does it* (`ActionEvent`). They
are orthogonal: a `FieldUpdate` can fire on `Submit` or on `FinalApproval`; an
`Alert` can fire on any phase. Conflating them — e.g. treating `Submit` as if it
were an `ActionType` — is a category error. (`approval_service.go:819` passes
`"Submit"` as the `actionEvent` argument; it is **not** an `ActionType` and must
not be touched by ActionType constant work.)

A **fourth** string literally named `ActionType` lives in
`interfaces/rest/approval_handler.go` (the Process Approvals REST request verb:
`req.ActionType`, defaulting to `Submit`, switched over `Submit` / `Approve` /
`Reject`). That is a **request-shape verb** on the Salesforce `ProcessRequest`
body — a different package and a different axis from the three runtime
enumerations above (it selects which approval *operation* the caller is invoking,
not a `ProcessAction` row's side effect or phase). It is intentionally **not**
constified by this PR; it is named here only so a reader does not assume the
three runtime axes are the complete inventory of action-named strings.

## Binding rule: document and bridge, do not unify

> Two taxonomies, documented and bridged. **Not** unified into one enum.

The automation and approval `ActionType` enumerations stay separate, and their
constant names stay **visually distinct** so they cannot silently collapse:

- automation dispatch constants are prefixed `ActionType*`
  (`ActionTypeSendEmail`, `ActionTypeCreateRecord`, …)
- approval side-effect constants are prefixed `ProcessActionType*`
  (`ProcessActionTypeFieldUpdate`, `ProcessActionTypeAlert`,
  `ProcessActionTypeFlow`)

New approval constants MUST use the `ProcessActionType*` prefix, never
`ActionType*`. The two prefixes living in the same `shared/pkg/constants`
package is intentional: co-location makes the distinction reviewable, and the
differing prefix makes an accidental merge a visible diff.

### Why merging is wrong

Each axis answers a different question (the last column of the table above).
Merging them — one enum, one switch — would force a single dispatch table to
serve "how does automation execute this" and "what side effect does approval
emit," even though the value sets only *appear* to overlap and in fact never do
(`SendEmail` is an automation executor selector; `Alert` and native approval
notification `Flow` are approval side-effect selectors; they route through
different services). A merged enum
re-creates exactly the double-dispatch smell the program is retiring: it would
let one taxonomy's value leak into the other taxonomy's switch, with
"unsupported action type" / "unknown ActionType" fallbacks papering over the
mismatch.

Phase 1.2 already separated `_System_Action` from `_System_StandardAction` at
the **table** level for the same reason (see below). This doc applies the same
discipline at the **enumeration** level. Bridging — a doc that names the
relationship and distinct constants that encode it — is the acceptable shape;
unifying is not.

## Relationship to the table bridge (different axis)

[`action-table-bridge.md`](action-table-bridge.md) governs two *tables* —
`_System_Action` (object-scoped quick/custom/automation action metadata) and
`_System_StandardAction` (global invocable standard-action registry). Its
contract: there is no implicit row-to-row bridge between them; name equality
across the two tables is not a conflict and never joined.

That is a **table-namespace** separation. This document is an
**enumeration-value** separation. They are independent: axis 1 above
(`_System_Action.Type`) is the dispatch enum *inside* the `_System_Action`
table from the bridge doc, while axes 2 and 3 live on the unrelated
`ProcessAction` table, which the table-bridge doc does not cover at all. Do not
conflate "the two action *tables* are disjoint" with "the three action
*enums* are distinct" — both are true, for different reasons.

## Salesforce v63 grounding

`ProcessAction.ActionType` (axis 2) maps to Salesforce approval-process
*actions*. Salesforce supports four approval action types. The table also calls
out NexusCRM's native `Flow` bridge because it is now a handled
`ProcessAction.ActionType` value, but it is deliberately not classified as
Salesforce `FlowAction`.

| Approval side-effect category | NexusCRM `ProcessAction.ActionType` | Status at HEAD |
|-------------------------------|-------------------------------------|----------------|
| Field Update | `FieldUpdate` | Implemented — runs inside the approval transaction (`executeProcessActionMutations`); durably replayed on Recall |
| Email Alert | `Alert` | Implemented — best-effort, outside the transaction (`executeProcessActionNotifications` → `executeApprovalEmailAlert`) |
| NexusCRM native approval notification Flow | `Flow` | Implemented as a NexusCRM-native bridge — approval lifecycle metadata binds a notification-safe Flow; the Flow owns email/template/recipient policy |
| Task | `Task` | **Delta: unsupported.** Falls to the `default` branch → logged as `unknown ActionType` and collected as a best-effort failure |
| Outbound Message | `OutboundMessage` | **Delta: unsupported.** Same unknown-skip path |
| Flow / Apex (v63 also exposes flow-launch approval actions) | `FlowAction` | **Delta: unsupported.** Same unknown-skip path |

The `ProcessAction.ActionType` column description in `system_tables.json` lists
a third value, `RecordLock` (`"FieldUpdate, Alert, RecordLock"`), that is
**declared but not dispatched** as an `ActionType` at HEAD — record locking on a
lifecycle phase is instead modeled through the `ProcessNode` `*RecordLock`
columns (`FinalApprovalRecordLock`, `FinalRejectionRecordLock`). It is not
handled by either dispatch switch and would currently take the `unknown
ActionType` path if it appeared on a `ProcessAction` row.

(`Reassign` is *not* an `ActionType` value — it is grounded only as an
`ActionEvent` lifecycle phase, axis 3 below. It is called out here so the shared
token is not mistaken for a second axis-2 value.)

**Delta summary:** NexusCRM implements the two highest-value Salesforce approval
action types (Field Update, Email Alert), and also implements a NexusCRM-native
`Flow` approval-notification bridge. That native bridge is not Salesforce
`FlowAction`: it launches a notification-safe Flow selected by lifecycle
metadata, while Salesforce `FlowAction` / Apex-style approval actions remain
unsupported and take the same unknown best-effort path as `Task` and
`OutboundMessage`. Adding a new type must be a deliberate extension (add a
`ProcessActionType*` constant + a dispatch arm), not an accidental string
literal.

`ActionEvent` (axis 3) corresponds to the Salesforce approval lifecycle phases
(Initial Submission, Approval Step, Final Approval, Final Rejection, Recall,
Reassignment). It is documented here only to keep it distinct from
`ActionType`; its constant extraction is out of scope.

## SF-standard vocabulary — not a tenant business enum

A reviewer may flag `case "FieldUpdate"` / `case "Alert"` as a business
enum/status branch (a Coding Red Line). It is not. Approval processes —
`ProcessDefinition`, `ProcessNode`, `ProcessAction`, and the
`FieldUpdate` / `Alert` action types — plus NexusCRM's native approval
notification `Flow` bridge — are **object-agnostic platform vocabulary**, part
of the core approval engine that any L1 app pack uses regardless of object
shape. These values are not tenant-authored status
strings, not a lifecycle-stage allowlist, and not a `__c` picklist; they are the
fixed vocabulary of the approval substrate, exactly like the automation
`ActionType*` dispatch values already in `shared/pkg/constants/actions.go`.

Placing the `ProcessActionType*` constants in `shared/pkg/constants/process_actions.go`
— a sibling file of `actions.go` in the same `constants` package — is therefore
SSoT consolidation of standard platform vocabulary, the same category as the
automation dispatch constants that live in `actions.go`. The two taxonomies
share a package (so the `ProcessActionType*` vs `ActionType*` prefix divergence
is reviewable package-wide and locked by `process_actions_test.go`) but sit in
separate files, which further signals they are distinct axes. This is **not** a
tenant business-branch red-line violation. The red line bites tenant-specific
enums (lifecycle stages, status allowlists, `__c` picklist values); it does not
bite the platform's own approval-engine type vocabulary.

## Scope of the implementation PR (P2.1)

In scope for the P2.1 PR (no-behavior-change rename; every constant's value is
byte-identical to the literal it replaces):

- Add `ProcessActionTypeFieldUpdate = "FieldUpdate"` and
  `ProcessActionTypeAlert = "Alert"` to a new file
  `shared/pkg/constants/process_actions.go` (a sibling of `actions.go` in the
  same `constants` package; its package-doc header records the anti-unify
  boundary), guarded by `shared/pkg/constants/process_actions_test.go`.
- Replace the production string literals with these constants at:
  - `backend/internal/features/automation/approval_service.go` (`executeProcessActionMutations` guard, `executeProcessActionNotifications` switch arms)
  - `backend/internal/features/automation/approval_action_replay.go` (replay `FieldUpdate` sites)
  - `backend/internal/controlplane/packs/source_backed_approval_process.go` (`normalizeApprovalProcessActionType` returns; add the `shared/pkg/constants` import)

`automation/action_service.go` is intentionally **not** in scope: the automation
dispatch `ActionType*` constants already exist in `actions.go`, so there is
nothing to replace there for the approval-side P2.1 deliverable. (The program
doc's PR-2 line names both files together; that conflates the two taxonomies'
literal-replacement work — only the approval side needed new constants.)

Post-P2.1 update: `ProcessActionTypeFlow = "Flow"` has since been added to the
same SSoT file as a native approval-notification side effect. It is intentionally
documented here, rather than folded into the automation `ActionType*` taxonomy
or treated as Salesforce `FlowAction`.

## Out of scope (follow-ups)

- **Frontend TS union.** `frontend/.../approvalXmlGenerator.ts` carries a
  `ProcessAction.ActionType` string union. It is not changed in the P2.1 PR;
  align it with this taxonomy as a separate frontend follow-up.
- **`ProcessAction.ActionEvent` constants (axis 3).** Not constified here. A
  later PR may introduce `ProcessActionEvent*` constants (`Submit`,
  `StepApproval`, `StepRejection`, `FinalApproval`, `FinalRejection`, `Recall`,
  `Reassign`) under the same distinct-prefix rule.
- **Test fixtures.** Approval `*_test.go` files and
  `approval_process_sync_literal_test.go` carry literal `"FieldUpdate"` /
  `"Alert"` strings. Tests are red-line-exempt; they are not churned in the
  P2.1 PR and are only touched if leaving them would break the build.
- **New Salesforce action types (`Task`, `OutboundMessage`, `FlowAction`,
  `RecordLock`).**
  Closing any of the Salesforce deltas above is a separate feature, each adding a
  `ProcessActionType*` constant plus a real dispatch arm.
- **Table-definition backlink to this doc.** A future schema-doc polish PR may
  add references from the `_System_Action` and `ProcessAction` table
  definitions in `backend/internal/bootstrap/system_tables.json`. This taxonomy
  is already the design SSoT; bootstrap comments are optional documentation,
  not a second source of truth.
- **REST `ProcessRequest.ActionType` verb (axis 4).** The Submit/Approve/Reject
  request verb in `interfaces/rest/approval_handler.go` is a separate
  request-shape axis (see "The three axes" note above) and is not constified
  here.
- **Codebase literal scanner.** The current guard is the value-set +
  cross-taxonomy disjointness tests (`process_actions_test.go`) plus review.
  A standalone literal scanner would be a new guard PR, not a dependency on a
  retired refactor program.
