# NexusCRM Flow Subsystem: End-to-End Design Overview (Engine + Builder + Action Substrate)

> **Purpose:** Consolidate “what Flow is / how it runs / how it is stored / how standard actions now execute through it / key invariants / known gaps & roadmap” into one shareable design overview for future design decisions and issue follow-ups.
>
> **In scope:**
> - Backend: Flow metadata surface (Tooling API), triggering & execution (TriggerListener / Executor), standard-action dispatch, scheduling & Wait resume (Scheduler), runtime audit (FlowInterview)
> - Frontend: where the Flows page and Flow Builder live, and the API surface they depend on (not a UI tutorial)
> - Verification: where contract tests and e2e-api cases should land
>
> **Out of scope:** a per-business-flow intent walkthrough (that belongs in “flow inventory / runnability audit” material).

---

## 0. Context and Design Principles

### 0.1 Goal: a Salesforce-compatible, metadata-driven automation and action layer

Under NexusCRM’s three-layer model:

- **L0 Code (the engine):** a generic Flow executor. It must not embed tenant facts such as `__c` field names, picklist values, RecordType names, object table names, etc.
- **L2 Metadata (configurable):** FlowDefinition/FlowVersion/FlowMetadata (canvas JSON), trigger conditions, filters, actions, variables/formulas should live as metadata in system tables. Admins/migration tooling can change them without a deploy.
- **L3 Data (business records):** user records. Flow inputs/outputs revolve around reading/writing business data.

This means: **Flow is not “business logic written in Go”**. Business logic is expressed as metadata; Go only implements execution semantics.

As of the flow-native Lead Convert refactor, the subsystem should be understood as two layers:

- **Flow runtime substrate:** generic execution semantics for record ops, decisions, actions, formulas, Wait, subflows, etc.
- **Metadata surfaces on top of Flow:** FlowDefinition / FlowVersion for automation, plus standard-action registry rows that dispatch into Flow graphs.

So the repo is no longer modeling Flow as only a “record automation engine.” It is also the execution substrate for platform standard actions when those actions can be expressed as metadata-driven graphs.

### 0.2 Correctness anchor: “frontend is the protocol specification”

The repo’s correctness model treats frontend request shapes/endpoints as anchored to real Salesforce v63 behavior. The backend should implement the protocol; it should not “fix” the frontend to accommodate backend gaps (see `CLAUDE.md`).

---

## 1. Core Data Model (FlowDefinition / Flow / FlowInterview + Standard-Action Satellites)

The Flow subsystem is represented by three system-automation tables:

- **FlowDefinition:** the logical definition (DeveloperName, ProcessType, trigger info, current active version)
- **Flow (FlowVersion):** versioned canvas (Metadata JSON + ProcessType + Status + VersionNumber)
- **FlowInterview:** runtime instances (Running/Paused/Finished/Error, ContextData, CurrentElement, PausedUntil, etc.)

And, for flow-native standard actions, a companion registry family:

- **_System_StandardAction:** action registry row (name, target objects, required permissions, flow binding)
- **_System_ActionSchema:** action input/output contract
- **_System_ActionFieldDefault:** action-scoped default values applied at recordCreate time
- **_System_ActionInputGuard:** permission-guarded input rules

Backend types are defined in:
- `shared/pkg/models/salesforce_flow.go` (`FlowDefinition` / `FlowVersion` / `FlowMetadata` / `FlowInterview` …)

### 1.1 Metadata “typed + raw dual-channel” design

`FlowVersion` keeps two representations:

- `Metadata` (typed struct, used by the executor)
- `MetadataRaw` (`json.RawMessage`, used for **lossless API roundtrip**)

Key behaviors:
- `HydrateMetadata()`: parse DB `MetadataJSON` into `Metadata`, and also store the raw JSON into `MetadataRaw`
- `SerializeMetadata()`: before writing to DB, prefer `MetadataRaw` to avoid data loss from typed roundtrips

Implementation: `shared/pkg/models/salesforce_flow.go` (`HydrateMetadata` / `SerializeMetadata`)

> **Why this matters:** Salesforce Flow metadata JSON contains many fields the engine does not “care about” but must preserve. Relying only on a typed struct risks silently dropping unknown fields and corrupting metadata on Builder/Tooling API roundtrips.

### 1.2 Action metadata is not Flow JSON

The standard-action refactor clarified a useful boundary:

- Flow JSON should express **execution graph semantics**
- Action satellites should express **invocation contract semantics**

Examples:

- whether an action is visible/invocable: `_System_StandardAction`
- request shape and required inputs: `_System_ActionSchema`
- permission-gated optional inputs: `_System_ActionInputGuard`
- action-scoped create defaults: `_System_ActionFieldDefault`

This avoids leaking action contract concerns into ad hoc Flow context conventions such as `_defaults.*`.

---

## 2. Triggering and Execution: Four Entry Points + One Resume Path

Flow execution has four primary entry points (plus Wait resume):

1. **Record-triggered:** record events (BeforeSave / AfterSave / BeforeDelete)
2. **Scheduled:** periodic execution (based on `FlowStart.schedule`)
3. **Invocable custom flow / REST:** manual invocation (`POST /actions/custom/flow/:name`)
4. **Standard action / REST:** registry-based dispatch (`POST /actions/standard/:name`)
5. **Wait resume:** the Scheduler scans due paused interviews and calls resume

### 2.1 Record-triggered: `FlowTriggerListener`

Entry: `backend/internal/features/automation/flow_trigger_listener.go`

Responsibilities:
- Subscribe to record events (before/after create/update/delete)
- Load FlowDefinitions and match by `TriggerObjectOrEvent` + `TriggerType` + `ActiveVersionId`
- Build inputs (key variables: `"$Record"`, `"$Record__Old"`, `"trigger"`)
- Call `FlowExecutor.ExecuteFlow()`
- For **BeforeSave**: merge in-memory `$Record` mutations back into the event payload (so the DML is affected)

Key design points:
- **Security context elevation:** record-triggered flows run as system automation; they should not be blocked by the triggering user’s permissions (`WithSecurityContext(SystemContext, ...)`).
- **Performance: transient FlowInterview mode**
  - The record-trigger path wraps context with `WithTransientFlowInterviews`, allowing the executor to skip two DB roundtrips (create + update interview).
  - Goal: avoid “one audit row per flow per save” in the hot path.
- **BeforeSave semantics marker:** `WithBeforeSaveFlow` tells recordUpdate with implicit `$Record` to do **in-memory mutation**, not DB UPDATE.
- **Re-entry guard (record-trigger stack):** re-entry for the same `(object, recordID)` on the same goroutine is short-circuited to avoid cycles.
- **`attributes.type` stamping:** ensure `$Record.attributes.type` exists, so record ops can infer the target object type (Salesforce trigger records always have it).

### 2.2 Execution semantics core: `FlowExecutor`

Entry: `backend/internal/features/automation/flow_executor.go` (logic is split across multiple files)

Shape:
- `ExecuteFlow(apiName, inputs)`: locate active FlowVersion and enter `executeFlowVersion`
- Decide interview persistence mode (default: persisted; record-trigger opts out)
- Initialize interview ContextData (inputs + flow variables + formula resources, etc.)
- Enter the execution loop from Start / StartElementReference, dispatch by element type:
  - `executor_elements.go`: Decision / Assignment / Loop / Wait / CustomError / Screen (stub) …
  - `executor_record_ops.go`: recordCreate / recordUpdate / recordLookup / recordDelete
  - `executor_actions.go`: ActionCall (emailAlert/emailSimple/flow(subflow))
  - `executor_helpers.go`: resolveValue / evaluateCondition / nested reference / formula helpers, etc.

Two important context modes:
- **BeforeSave record-trigger:** allows `$Record` in-memory mutation which the listener merges back into the DML payload
- **AfterSave / Scheduled / REST:** recordUpdate issues DB UPDATEs (side effects against committed data)

### 2.3 Scheduled: `SchedulerService`

Entry: `backend/internal/features/automation/scheduler_service.go`

Responsibilities:
- Every minute tick:
  1) execute scheduled flows (decide by FlowDefinition + `FlowStart.schedule`)
  2) resume due paused interviews (Wait)

Design notes:
- Scheduled flows run as a system user.
- Scheduled flows keep persisted FlowInterviews (operator visibility).
- Wait resume is batched and concurrency-capped (one backlog should not stall the whole tick).

> **Known systemic risk:** the scheduler currently uses `time.Now()` (local timezone) while schedule times in metadata are UTC strings. If comparison is done inconsistently, all scheduled flows can be “shifted” rather than broken. This needs a single explicit policy (compare in UTC everywhere, or convert consistently).

### 2.4 Invocable custom flow / REST: Flow Actions API

Routes live in `backend/internal/interfaces/rest/`:

- `flow_invocable.go`:
  - `GET /services/data/v63.0/actions/custom/flow` (list invocable flows)
  - `GET /services/data/v63.0/actions/custom/flow/:name` (schema)
  - `POST /services/data/v63.0/actions/custom/flow/:name` (execute)
- `flow_interviews.go`:
  - `POST /services/data/v63.0/actions/custom/resumeFlow` (resume interview)

This surface is used for:
- “manual flow execution” from the frontend
- `actionType="flow"` invocations inside other flows (subflow / invocable chains)

### 2.5 Standard action / REST: registry -> flow dispatch

Routes live in `backend/internal/interfaces/rest/standard_action_dispatcher.go`:

- `GET /services/data/v63.0/actions/standard`
- `POST /services/data/v63.0/actions/standard/:name`

Execution shape:

1. Look up active `_System_StandardAction`
2. Enforce declared permissions
3. Validate payload via `_System_ActionSchema`
4. Evaluate `_System_ActionInputGuard`
5. Stamp action context + preload `_System_ActionFieldDefault`
6. Call `FlowExecutor.ExecuteFlow()`

This is an important design shift: some “platform actions” now execute as Flow graphs rather than hardcoded Go handlers. The action registry is the invocable contract boundary; the Flow graph is the execution plan.

#### Salesforce baseline and candidate paths

The protocol baseline here is the Salesforce standard-actions REST surface:

- `GET /services/data/v63.0/actions/standard`
- `POST /services/data/v63.0/actions/standard/:name`

For NexusCRM, there are two plausible implementation families behind that
surface:

1. keep standard actions as code-native handlers
2. keep the REST contract stable, but let migrated actions dispatch through
   metadata + Flow graphs

This document records the current repo choice for migrated actions; it does
**not** claim that Salesforce internally stores standard actions as Flow graphs.

#### Current repo shape and declared delta

Current repo evidence shows the following design:

- `_System_StandardAction` is the registry row that binds a standard action name
  to a Flow developer name and required permissions
- `_System_ActionSchema`, `_System_ActionFieldDefault`, and
  `_System_ActionInputGuard` carry the invocation contract satellites
- `StandardActionDispatcher` owns the stable REST contract and dispatches the
  migrated action to `FlowExecutor`
- actions not yet migrated may still fall through to legacy code-native
  handlers

References:

- `shared/pkg/models/z_generated.go:643-714,1717-1741` (`SystemActionFieldDefault`,
  `SystemActionInputGuard`, `SystemActionSchema`, `SystemStandardAction`)
- `backend/internal/interfaces/rest/standard_action_dispatcher.go:300-316,323-350,375-470`
- `backend/internal/interfaces/rest/standard_actions_handler.go:33-36`

Declared NexusCRM delta:

- **REST parity target:** keep the Salesforce-shaped `actions/standard` surface
- **implementation delta:** NexusCRM allows migrated standard actions to be
  implemented as metadata-driven Flow graphs behind that surface, instead of
  requiring a permanent code-native handler per action

Why this delta is intentional:

- it keeps the public contract stable while moving business/action semantics out
  of Go branches and into metadata
- it lets admin-visible action contract facts live in registry rows rather than
  scattered handler code
- it makes “which actions are active / invocable / guarded / defaulted” a
  metadata concern, consistent with the repo's metadata-driven rules

#### Next-tenant impact and verification anchors

Next-tenant impact:

- a greenfield tenant should consume the same stable REST surface
- tenant-specific action visibility or defaults should come from metadata rows,
  not code branches
- the repo should not require tenant-specific standard-action handlers just to
  vary action contract semantics that fit the registry/Flow model

Verification anchors:

- contract/handler tests under
  `backend/internal/interfaces/rest/standard_action_dispatcher_test.go`
- generated model definitions for the registry satellites in
  `shared/pkg/models/z_generated.go`
- end-to-end migrated-action behavior where the action path observably changes
  backend automation semantics

---

## 3. Two Runtime Modes: Durable Workflow vs Atomic Action

The current subsystem has two materially different execution modes.

### 3.1 Durable workflow mode

Used by classic automation workloads:

- record-triggered flows
- scheduled flows
- invocable custom flows
- any flow that uses `Wait`

Characteristics:

- may persist and resume interviews
- may span wall-clock time
- optimized for automation semantics, not single-transaction rollback

### 3.2 Atomic action mode

Used by flow-native standard actions that need cross-object rollback semantics.

The marker is `FlowMetadata.RunAtomically=true`.

Characteristics:

- one transaction boundary across the effective call chain
- subflows inherit the parent atomic boundary even if their own JSON omits `runAtomically`
- suitable for multi-record platform actions such as flow-native Lead Convert

Restrictions:

- atomic execution may not contain `Wait`
- atomic execution may not perform inline email I/O such as `emailAlert`

This is not a cosmetic restriction. It is the architectural line between “durable workflow runtime” and “transactional action runtime.”

## 4. Wait and Persistence: From “Run-to-Completion” to Durable Workflows

Wait extends the engine beyond synchronous execution:

- On Wait:
  - interview becomes `Paused`, persists `ContextData` + `CurrentElement`
  - set `PausedUntil` (or equivalent resume time)
- The Scheduler scans due paused interviews:
  - `ListDuePausedInterviews(now, limit)`
  - `ResumeInterview(interviewID)`: re-hydrate flow version + restore context + continue execution

Boundary semantics to explicitly define (and test):
- Formulas involving `TODAY()/NOW()` should be recomputed on resume (avoid stale timestamps).
- Record context semantics on resume must be consistent (use the paused snapshot vs re-fetch latest record state), otherwise “record changed during wait” becomes a source of subtle bugs.

Wait should be treated as a first-class durable-workflow capability, not a convenience feature. It is what turns the engine from “run to completion now” into “persist, wake up later, continue.”

---

## 5. Variables, References, and Conditions: Turning Metadata into Executable Semantics

### 5.1 Naming and core global variables

Key executor variables:
- `"$Record"`: the triggering record (or a record provided by the invoker)
- `"$Record__Old"`: the prior record (AfterSave update / BeforeSave update)
- `"$Flow.*"`: runtime/time globals provided by executor/helpers
- `"$User"`: user context (needed in some scenarios)

The Builder and the frontend resource picker must be able to express these references; otherwise you get “engine supports it, UI cannot author it” drift (see issue `#1607`).

### 5.2 Reference resolution (`resolveValue` / nested reference)

Common reference forms in Flow metadata:
- `elementReference`: references a variable/resource/element output
- dotted path: `$Record.Account.Owner.Email`, `$Record__Old.StageName`, etc.

Executor helpers typically handle:
- unwrapping typed wrappers (stringValue/booleanValue/elementReference, etc.)
- dotted-path traversal
- cross-object relationship traversal (often via `__r`) which may require lazy DB fetch + caching

Important current limitation:

- `FlowRecordLookup` / `FlowRecordUpdate` / `FlowRecordCreate` still treat `object` as a static string in flow JSON.
- That is why `changeOwner` has not yet migrated to Flow: it needs a runtime-supplied object name (`objectApiName`), which the current primitive set cannot yet express cleanly.

### 5.3 Condition evaluation (Decision / Start filters / record filters)

Conditions consist of:
- `LeftValueReference`
- `Operator` (EqualTo / IsNull / IsBlank / …)
- `RightValue` (typed wrapper; sometimes an “all-null wrapper” requiring truthiness semantics)

Typical pitfalls (worth locking behind regression tests):
- `IsBlank` / `IsNull` inversion when booleanValue=false
- “all-null rightValue wrapper” truthiness checks (Salesforce semantics are not `== nil`)
- PRIORVALUE rewriting for dotted paths

---

## 6. Tooling API, Action Discovery, and the Builder: CRUD Surface for Metadata

### 6.1 Tooling API (FlowDefinition / FlowVersion)

Backend entry:
- `backend/internal/interfaces/rest/flow_tooling.go` (FlowDefinition CRUD, FlowVersion CRUD, activate/deactivate)

Goal: match Salesforce Tooling API object/field shapes so the frontend Builder can manage flow metadata via SOQL/CRUD.

### 6.2 Standard-action discovery is now part of the authoring contract

`GET /services/data/v63.0/actions/standard` is now the metadata-driven discovery surface for standard-action affordances.

That matters architecturally because frontend visibility is part of the contract:

- if an action is active and permitted, the frontend should discover it from metadata
- if an admin disables an action, the button should disappear without a code change

This follows the same philosophy as Flow Builder fidelity: the UI should consume metadata, not re-encode platform facts in literals.

### 6.3 Frontend entry points (Flows page and Builder routing)

Frontend entry points (for orientation only):
- `frontend/src/pages/FlowsPage.tsx` (Flows management page)
- `frontend/src/features/flows/**` (flow components, execution modal, interview panel, test coverage panel)
- `frontend/src/infrastructure/api/flows` (client wrapper for flow tooling/actions API)

> The key constraint: the Builder and discovery surfaces must express the same capabilities the engine supports, or drift becomes permanent.

---

## 7. Observability and Failure Model (FlowInterview + Error Surfacing)

FlowInterview’s value:
- provides an audit/debug baseline for **scheduled / invocable / wait-resume** runs
- for **record-trigger hot paths**, audit is optional (transient mode favors performance)

For standard actions:

- action discovery / permission / schema failures should fail before executor entry
- flow runtime failures should surface as Flow execution errors or CustomError-derived user-facing failures
- atomic flows especially need clear error attribution because one bad subflow path rolls back the whole action

Suggested failure stratification:
- engine/system errors: interview status becomes Error with searchable codes/messages
- business validation errors (ValidationRule/CustomError): should be distinguishable and UI-friendly (CustomError supports user-facing errors)

---

## 8. Testing and Verification Strategy (Minimal Closed Loop)

Treat Flow verification as four layers:

1) **Unit (semantic regressions):** condition evaluation, reference resolution, PRIORVALUE, IsBlank, etc.
2) **Contract tests (protocol stability):** Tooling API objects (FlowDefinition/Flow) and actions/custom/flow endpoints must remain stable (see `.agent/skills/sf-contract-test/SKILL.md`).
3) **Action-contract tests:** registry lookup, guard evaluation, action defaults, inherited atomicity, standard-action discovery.
4) **e2e-api cases (end-to-end):** encode past production-grade failure modes (e.g., AfterSave self-deadlock, scheduled start-filter semantics, fresh-org standard-action defaults) to prevent regressions.

Important: when validating against remote TiDB Cloud branches, separate **correctness failures** from **environment/RTT timeouts**. Many “flow failures” are actually request context cancellations due to query latency.

---

## 9. Known Gaps and Roadmap (Issue Buckets)

This section lists design-level buckets (not an exhaustive list):

### 9.1 P0: capability gaps still needing closure
- Entry condition “Formula evaluates to true”: #1444
- Record-triggered scheduled paths (deferred execution): #1464
- TextTemplate rendering + sender semantics (emailSimple + OrgWideEmailAddress): #1465

Flow `<formulas>` resource evaluation (`#1456`) is no longer a blank runtime
surface on current `main`:

- the executor already evaluates declared Flow formulas into interview context
- resume paths already refresh those formulas so time-sensitive expressions do
  not stay frozen across Wait

What remains open for `#1456` is the closure work around activation-time
validation, dependency/cycle guards, and end-to-end evidence on the production
lead-assignment flows — not first-time evaluator wiring.

### 9.2 Correctness: small but hard semantic bugs
- scheduled start-filter IsBlank booleanValue=false inversion: #1801
- Decision/condition semantics: #1760 (IsChanged parity, etc.)

### 9.3 Builder drift: engine capabilities not expressible in UI
- multi-level `__r` refs / metadata raw channel, etc.: #1607

### 9.4 Runtime/perf: not a logic bug, but affects usability
- remote TiDB RTT causing record-trigger flow timeouts (needs local / prod-shape validation strategy): #1817 (see comments)

### 9.5 Standard-action substrate gaps

- `changeOwner` still needs dynamic object-name support in Flow record ops
- `createRecord` still needs either workflow-DSL compilation to Flow or another
  architect-reviewed generic bridge; do not add action-specific branches
- RecordType stage 4 still needs `_System_ProfileRecordTypeVisibility`
- remaining `dispatchLegacy` switch should collapse to 404 after those migrations

### 9.6 Wait / Scheduler operational debt
- cleanup of orphan paused interviews (PausedUntil NULL + flow deactivated)
- scheduler timezone policy (create a concrete issue + acceptance criteria)

## 10. Current Design Summary

The current best mental model is:

- **Flow runtime** is the generic execution substrate.
- **FlowDefinition / FlowVersion / FlowInterview** are the automation metadata + runtime audit core.
- **Standard-action registry tables** are the action-contract layer that can dispatch into Flow.
- **Durable workflow mode** and **atomic action mode** are both first-class, but they have different allowed capabilities.
- **Builder / Tooling / action discovery** are part of the design contract, not optional UI convenience layers.

If a future feature proposal adds business facts to Go branches, hides action visibility in frontend literals, or bypasses the registry/flow boundary for a metadata-expressible behavior, it is moving the subsystem in the wrong direction.

---

## 11. Key Design Invariants (Must Hold Across Future Changes)

1) **Metadata-driven boundary:** engine code must not hardcode tenant business facts; changes should be possible via metadata or data-spec.
2) **Lossless roundtrip:** Tooling API flow metadata must roundtrip without losing fields due to typed-struct coverage gaps.
3) **BeforeSave vs AfterSave separation:** BeforeSave can mutate the in-flight record; AfterSave must not take the in-memory branch.
4) **Controllable persistence strategy:** record-trigger defaults to transient for performance; scheduled/invocable/wait must remain auditable.
5) **Clear resume semantics:** Wait resume semantics for time/record snapshot/formula recomputation must be defined and testable.

---

## 12. Further Reading (in this repo)

- `docs/design/flow/trigger-automation-design.md`: deeper mapping of trigger automation (before/after events) to FlowExecutor
- `docs/design/flow/flow-engine-gap-analysis.md`: historical gap analysis and phased remediation (Wait/Formula/__r, etc.)
- `docs/design/flow/flow-engine-rewrite-feasibility.md`: rewrite feasibility + DSL/IR strategy (long-arc)
- `docs/audit/1120-implicit-record-audit.md`: audit on implicit `$Record` write risks
- `docs/tasks.md`: historical Flow Builder task index
