# Design: Flow Engine Gap Fix — Order/Contract/Invoice_Collection__c/Account

Generated by /office-hours on 2026-04-16
Branch: main
Repo: pingcap-inc/MetaDrivenPlatform4AI
Status: APPROVED
Mode: Startup
Supersedes: amber-main-design-20260416-154433.md

## Problem Statement

NexusCRM's Flow engine has 37 active flows for Order, Contract, Invoice_Collection__c, and Account objects. 17 record-triggered AutoLaunchedFlows execute correctly. **20 Workflow/InvocableProcess type flows are completely inert** — they never fire because the trigger listener cannot match them.

These 20 flows are load-bearing business automation: Order expiration alerts (7/15/30/60/90 day email chains), field auto-population (Account Name, Partner Name, Contracting Entity, Start/End dates), Invoice amount calculations, Account status management. They're broken now, affecting real business operations.

This is not a feature request. It's 54% of the automation layer being dead.

## Demand Evidence

- All 20 Workflow/InvocableProcess flows are imported from production Salesforce but sit in the database inert
- Business processes that depend on them (Order expiration notifications, field auto-fill on record save, Invoice calculations) are not running
- User confirmed: "Broken now" — not preemptive, actual business impact
- All object categories (Order, Contract, Invoice_Collection__c, Account) equally critical

## Status Quo

The current workaround is... nothing. These flows don't execute. Any business logic they were supposed to automate is either being done manually or not happening at all. The data in FlowDefinition and Flow tables is correct (imported from Salesforce), but the engine can't interpret the legacy format.

## Gap Analysis (7 Gaps)

### GAP 1: Workflow/InvocableProcess flows never trigger (CRITICAL)

20 of 37 flows have `ProcessType = Workflow` or `InvocableProcess` with `TriggerObjectOrEvent = NULL`. They encode trigger config in `processMetadataValues`:
- `ObjectType` = target object (e.g., `Order_Product__c`, `Order`, `Invoice_Collection__c`)
- `TriggerType` = `onAllChanges` / `onCreateOnly`
- Record refs use `myVariable_current` / `myVariable_old` instead of `$Record` / `$Record__Old`
- `start` element is `null` (no FlowStart)

`FlowTriggerListener.executeRecordTriggerFlows()` at `flow_trigger_listener.go:100` only checks `def.TriggerObjectOrEvent` — never reads `processMetadataValues`. These flows never match.

**Affected flows (by object):**

| Object | Flow | Business Purpose |
|--------|------|------------------|
| Order | Order_Account_Name | Auto-fill Account Name |
| Order | Order_Partner_Name | Auto-fill Partner Name |
| Order | Order_Start_Date_End_Date | Sync start/end dates |
| Order | Order_PingCAP_RevRec_Entity | Set RevRec entity |
| Order | Order_Expiration_Notice_90 | 90-day expiration email chain (with Waits) |
| Order | Order_Expiration_Alert_7/15/30/60/90 | Region-specific expiration emails (InvocableProcess) |
| Contract | Contract_PingCAP_Contracting_Entity | Set contracting entity |
| Account | Account_PingCAP_Contracting_Entity | Set contracting entity |
| Account | Account_PoC_Status | Update PoC status |
| Invoice | Collections (multiple) | Amount calc, region mapping, Account ID, cash collected |
| Invoice | Invoice_Notice | Send invoice notices |
| Account | RevRec_Account | RevRec field update |
| Account | Partner_Account_CM_Approval | CM approval submission |
| Quote | Quote_Account_Opportunity_field | Field sync |

### GAP 2: Wait elements not modeled (HIGH)

`Order_Expiration_Notice_90` uses 3 Wait elements for time-delayed actions (send email at 90 days, then 60, then 30 before expiration). `FlowMetadata` struct at `salesforce_flow.go:84` has **no Waits field**. The JSON deserializes but Wait data is silently dropped. The executor at `flow_executor.go:338` has no `executeWait` case.

Without Wait support, the entire Order expiration notification pipeline is dead even after Gap 1 is fixed.

### GAP 3: Formula resources not evaluated (MEDIUM)

12 flows use `formulas` array. Expressions used:
- `TODAY()`, `TODAY()+30`, `TODAY()+60`, `TODAY()+90`
- `ADDMONTHS(NOW(), 3)`, `ADDMONTHS(NOW(), 15)`
- `ADDMONTHS(TODAY(), 1)`

The formula engine at `backend/pkg/formula/engine.go` supports all these functions. The gap is wiring: the flow executor's `resolveValue` doesn't evaluate formula resources — it only resolves element references and literals.

### GAP 4: `submit` action type not handled (MEDIUM)

`Partner_Account_CM_Approval` uses `actionType: "submit"` for approval submission. `executor_actions.go:79` only handles `emailAlert` and `emailSimple` directly. `submit` falls through to metadata-based lookup.

### GAP 5: `flow` action type subflow chain (MEDIUM)

5 `Order_Expiration_Alert_*` flows are called via `actionCalls` with `actionType: "flow"` (not the `subflows` array). This Salesforce pattern routes through `executeActionCall` which doesn't recognize `flow` as a special action type.

### GAP 6: Cross-object __r reference traversal (HIGH)

Both Workflow and AutoLaunchedFlow types use relationship references:
- `myVariable_current.Order__r.Overall_Start_Date__c`
- `$Record.Org__r.Account__c`
- `$Record.Account.PLG_BD_Pool__c`

The executor's `resolveNestedReference` can walk dot-separated paths within a single object but cannot traverse `__r` relationships (which require a DB lookup to resolve the related record).

### GAP 7: IsBlank operator missing (LOW)

Some flows use `IsBlank` in conditions. The executor only implements `IsNull`. Salesforce `IsBlank` checks for both null AND empty string.

## Constraints

- No historical burden — this is a greenfield CRM, no patches or workarounds
- Contract is a first-class target object alongside Order, Invoice_Collection__c, Account
- No more Salesforce metadata sync — one-time migration, flows will be maintained in NexusCRM going forward
- Loop N+1 risk acknowledged: short-term doc/logging, long-term Bulkification deferred

## Scope After Business Audit (Phase 0)

A per-flow business audit was completed. The original 20 inert flows were classified:

**KEEP — 14 legacy flows to migrate:**
- Contract: `Contract_PingCAP_Contracting_Entity`
- Order: `Order_PingCAP_RevRec_Entity`, `Order_Account_Name`, `Order_Partner_Name`
- Order_Product__c: `Order_Start_Date_End_Date`
- Invoice_Collection__c: `Collections_Region_PingCAP_Contracting_Entity_Marketplace`, `Collections`, `Collections_Cash_Collected`, `Collections_Invoice_Status_marketplace`, `Order_Expiration_Notice_90` (aka "Trigger Order Expiration Alert"), `Invoice_Notice`, `Collections_Account_ID`, `Collection_Partner_Name`
- Account: `Account_PingCAP_Contracting_Entity`

**KEEP — 5 InvocableProcess callees** (invoked by Order_Expiration_Notice_90 via `actionType: "flow"`):
- `Order_Expiration_Alert_7`, `_15`, `_30`, `_60`, `_90`

**DROP — obsolete, will be deactivated (not migrated):**
- Account: `Account_PoC_Status`, `Account_Partner_Sales_Owner_Set`, `Account_status_Flow`, `Partner_Account_CM_Approval` (uses `submit` — this removes Gap 4 from scope), `RevRec_Account`, `Quote_Account_Opportunity_field`, `Sync_Account_Status_to_Lead`, `Account_And_Lead_Sum_Approval_Poc`, `Account_Auto_Create_Org`, `Account_Related_Leads_Based_on_Website_Domain_Create/Update`
- Lead/PoC/Org: `BN_*` flows (3), `Lead_Owner_Is_Change_By_Account_Send_Email`, `LeadSetOwnerByAccount`, `Org_Set_Account`, `Auto_Set_Poc_Account_Contact_Lead_opportunity`, `update_account_latest_month_end_arr`

**Revised scope totals:**
- Flows to migrate: **19** (14 + 5 callees)
- Flows to deactivate (set `Status = 'Obsolete'`): **~16**
- Gaps removed from scope: **Gap 4 (`submit` action)** — Partner_Account_CM_Approval is being dropped

## Premises

1. **Convert, don't adapt:** The 19 kept legacy flows will be converted to native AutoLaunchedFlow format via a one-time migration script. No dual-format support in the engine. (Validated: no future SF sync)
2. **Wait is first-class:** Implement Wait as a flow element with persistent interview state (Paused status, scheduler wake-up, resume from CurrentElement). Not simulated via external jobs.
3. **Wire, don't rebuild:** The existing formula engine covers all flow formula functions. Connect it to the executor's value resolution.
4. **Lazy-load with warnings:** Cross-object __r references resolved via on-demand DB lookup with interview-level caching. Warn log when __r detected inside Loop context. Bulkification deferred.
5. **Staged activation:** Flows are activated in phases, not all at once. Simple Workflow field-updates first, then scheduled flows, then Wait-based `Order_Expiration_Notice_90` last — only after pause/resume is production-proven.
6. **Atomic Wait persistence:** `executeWait` wraps transient-to-persistent promotion + `PausedUntil` set in a single DB transaction. Partial state is impossible; an orphaned paused interview cannot exist.

## Cross-Model Perspective

Codex (independent cold read) provided:

- **Steelman:** "They're building a Salesforce-compatible automation runtime, not just making flows fire. The strong version: admins keep authoring in Salesforce flow semantics, NexusCRM executes faithfully on Go + TiDB, and the automation layer is an operationally credible compatibility surface."
- **Key insight:** Wait is the architecturally most important gap — it changes the engine from "run-to-completion" to "durable workflow runtime."
- **Challenged Premise 1:** Migration would be pushed over if Salesforce metadata is re-imported. Invalidated by user confirming no future sync.
- **48-hour prototype:** Recommended runtime compatibility shim over migration (effectively our rejected Approach C). Not adopted because user explicitly chose "no legacy burden."

## Approaches Considered

### Approach A: Migration Script + Engine Enhancement (CHOSEN)

Summary: One-time migration script rewrites 20 flows to native format + engine additions for Wait, Formula, __r, action types.

Effort: M
Risk: Medium (migration needs per-flow regression testing)
Completeness: 9/10

Pros:
- Clean engine with single format — no dual-path complexity
- Migration is one-time, results in standard flows going forward
- Engine enhancements benefit all flows, not just legacy ones

Cons:
- Migration script needs careful per-flow validation
- Must handle edge cases in processMetadataValues parsing
- If a flow's metadata doesn't cleanly map, manual intervention needed

### Approach B: Dual-Mode Engine

Summary: Keep original data, teach engine to parse processMetadataValues and support both reference styles.

Effort: L
Risk: Medium
Completeness: 7/10

Pros:
- No data transformation risk
- Original Salesforce metadata preserved

Cons:
- Engine complexity permanently doubled
- Every future feature must consider both formats
- Contradicts "no legacy burden" principle

### Approach C: Hybrid (Shim then Migrate)

Summary: Runtime compatibility shim first (get flows running), then migrate in batches.

Effort: M-L
Risk: Low
Completeness: 8/10

Pros:
- Fastest time to first flow running
- Lower risk (no data transformation upfront)

Cons:
- Two phases of work
- Temporary compatibility code that must be removed
- Total effort higher than Approach A

## Recommended Approach

**Approach A: Migration Script + Engine Enhancement**

Rationale: User explicitly stated "no legacy burden." One-time migration is clean and permanent. Engine enhancements (Wait, Formula, __r) are required regardless of migration approach. The additional work of writing a migration script is offset by the permanent simplicity of a single-format engine.

## Implementation Phases

### Phase 0: Business Audit & Deactivation (before engine changes)

1. Complete per-flow business audit — done (see "Scope After Business Audit" section)
2. For the ~16 dropped flows, update `FlowDefinition.ActiveVersionId = ''` and set `Flow.Status = 'Obsolete'` so the trigger listener stops considering them. This is reversible (data preserved).
3. Verify: run the 17 existing record-triggered flows through a test invocation round — confirm no regressions from removing the dropped flows' unreachable paths.

### Phase 1: Migration Script (unblocks everything)

1. Parse `processMetadataValues` from each of the 20 flows to extract `ObjectType`, `TriggerType`, `ObjectVariable`, `OldObjectVariable`
2. Generate native `FlowStart` element with proper `triggerType`, `object`, `recordTriggerType`, `filters`. **Exception:** Flows with `ProcessType = InvocableProcess` that have no `ObjectType` in `processMetadataValues` are migrated to `ProcessType = AutoLaunchedFlow` with no `FlowStart` element — they remain callable-only (invoked by other flows via `actionType: "flow"`), not record-triggered.
3. Rewrite all `myVariable_current.X` references to `$Record.X` and `myVariable_old.X` to `$Record__Old.X`. References with `__r` segments (e.g., `myVariable_current.Order__r.Start_Date__c`) are rewritten to `$Record.Order__r.Start_Date__c` and will be resolved at runtime via the GAP 6 lazy-load mechanism in Phase 2.
4. Update `FlowDefinition.TriggerObjectOrEvent` and `TriggerType`
5. Update `Flow.Metadata` JSON with rewritten metadata
6. Validate each flow roundtrips correctly (serialize → deserialize → same logical structure)

### Phase 2: Engine Enhancements (enables full functionality)

1. **__r lazy-load (HIGH)** — In `resolveNestedReference`, detect `__r` segments, call `query.QueryWithFilter` to fetch related record, cache in interview context keyed by `{objectType}:{recordId}`. Emit warn log when `__r` resolution occurs inside Loop execution context. Known limitation: interview-level cache is populated once per reference per interview. If a flow action updates the related record, subsequent `__r` lookups in the same interview return stale data. Acceptable for current flows (none exhibit this pattern); documented as a known limitation.

2. **Wait element (HIGH)** — Full specification:
   - Add `FlowWait` struct to `salesforce_flow.go` with fields: `Name`, `Label`, `WaitEvents []FlowWaitEvent` (each with `conditionLogic`, `conditions`, `connector`, `eventType`, `inputParameters`), `DefaultConnector`, `FaultConnector`
   - Add `Waits []FlowWait` to `FlowMetadata`
   - **Schema migration:** Note that `FlowInterview` already has `PausedDate DATETIME NULL` (records *when* paused) and `CurrentElement VARCHAR` (already present, stores where to resume). Add new nullable column `PausedUntil TIMESTAMP NULL` (records *when to wake up* — distinct from PausedDate). Add composite index `(InterviewStatus, PausedUntil)` to make the scheduler query efficient. Migration is backward-compatible (additive only, no data change). Must deploy before code.
   - **Transient interview promotion (critical):** Record-triggered flows use transient (in-memory-only) interviews for performance. `PauseInterview()` currently rejects transient interviews with a loud error. Before `executeWait` can pause, it must promote the transient interview to persistent. The promotion + pause must be atomic: a single DB transaction that (a) inserts a FlowInterview row with full ContextData, (b) sets InterviewStatus=Paused, CurrentElement, PausedUntil in the same row. Failure to commit = no row created, executor returns a fault. Orphaned paused interviews cannot exist.
   - **Executor:** `executeWait` evaluates each wait event's conditions against the current context. If exactly one condition is met, follow that event's connector. If all conditions are time-based and unmet, promote-if-transient + persist Paused state atomically, then return `""` (halt execution). If none of the conditions will ever become true (malformed), use the default connector or fault connector.
   - **Resume contract:** The SchedulerService queries `SELECT * FROM FlowInterview WHERE InterviewStatus = 'Paused' AND PausedUntil <= NOW()` on its existing 60-second check interval (uses the new composite index). For each match, it calls `FlowInterviewService.ResumeInterview(ctx, interviewID)`. `ResumeInterview` internally: (a) loads the interview by ID, (b) loads the FlowVersion via `interview.FlowVersionId` and hydrates metadata, (c) re-runs formula evaluation against current context (fresh TODAY/NOW), (d) sets `InterviewStatus=Running`, clears `PausedUntil`, (e) re-enters `FlowExecutor.executeFlowMetadata` at `interview.CurrentElement`.
   - **Catch-up:** If `PausedUntil` is in the past when the scheduler picks it up (e.g., server downtime), the interview resumes immediately — the Wait condition re-evaluates and completes.

3. **Formula wiring (MEDIUM)** — When an interview is initialized (per trigger invocation), evaluate all formula resources in `flowMeta.Formulas` using `formula.Engine.Evaluate()`. Build a `formula.Context` from `interview.ContextData` ($Record → Record, $Record__Old → Prior, flow variables → Variables). Store each formula result in `interview.ContextData` keyed by the formula's `Name` field, so downstream elements can reference them via `elementReference`. **Wait-resume correctness:** Formula resources whose expressions reference `TODAY()` or `NOW()` must be re-evaluated each time the interview resumes from a Wait state. On resume, re-run formula evaluation against the current context before re-entering the Wait element's condition check. This prevents stale dates — e.g., `TODAY()+30` computed at interview creation would hold the wrong date when the flow resumes 30 days later.

4. **`flow` action type (MEDIUM)** — In `executeActionCall`, detect `actionType == "flow"` and route to `ExecuteFlow(ctx, action.ActionName, inputParams, user)`. Execute synchronously — the parent interview blocks until the child completes, same as native subflow behavior. Map `inputParameters` to the child flow's input variables, map `outputParameters` back to the parent's context.

5. **~~`submit` action type~~** — **REMOVED FROM SCOPE.** Partner_Account_CM_Approval was the only consumer and it's being deactivated in Phase 0.

6. **`IsBlank` operator (LOW)** — Add to condition evaluator: true when value is nil OR empty string `""`. Used by `Contract_Update_Account_Contract_Valid` (verified).

### Phase 3: Testing & Validation

1. **Fixture source:** Dump matching records from staging DB into `backend/testdata/flow-fixtures/` as redacted JSON. Real production-shaped data beats hand-built fixtures.
2. **Regression tests (CRITICAL):** For each of the 17 existing working flows, capture pre-change execution snapshots. After engine changes, replay the same inputs and assert identical output. The engine changes (formula wiring, `__r` resolution) affect ALL flows — this proves no silent regressions.
3. Unit tests for each new element type (Wait, formula resolution, `__r` lookup with cache and Loop warning)
4. Integration tests: execute each of the 19 migrated flows against test fixtures
5. End-to-end: verify Order expiration email chain fires correctly with Wait timing, via scheduler tick
6. Atomicity test: kill the process mid-`executeWait` (simulate with panic injection) — verify no orphan paused interview is left behind
7. Performance baseline: measure flow execution time with `__r` lookups, verify Loop warning fires per iteration

### Phase 4: Staged Activation (production rollout)

**Activation happens in waves to de-risk interactions with the 17 existing live flows.**

- **Wave 1** (simple Workflow field-updates, no Wait): `Order_Account_Name`, `Order_Partner_Name`, `Order_PingCAP_RevRec_Entity`, `Order_Start_Date_End_Date`, `Contract_PingCAP_Contracting_Entity`, `Account_PingCAP_Contracting_Entity`, `Collection_Partner_Name`, `Collections_Account_ID`, `Collections_Region_PingCAP_Contracting_Entity_Marketplace`. Activate one at a time. Monitor for duplicate field updates or recursion with existing flows.
- **Wave 2** (moderate complexity): `Collections`, `Collections_Cash_Collected`, `Collections_Invoice_Status_marketplace`, `Invoice_Notice`.
- **Wave 3** (the hard one): `Order_Expiration_Notice_90` as Scheduled + its 5 `Order_Expiration_Alert_*` callees. Activate only after Wave 1+2 have been stable for a few days and atomic Wait/resume has passed the atomicity test in Phase 3.

Between waves, verify (a) no duplicate field updates against any record, (b) existing 17 flows' behavior unchanged, (c) no accumulating paused interviews that never resume.

## Open Questions

1. **Wait wake-up granularity:** Should the SchedulerService check for paused interviews on the same 60-second interval as scheduled flows, or does Order expiration (day-level precision) warrant a less frequent check?
2. **Migration rollback:** Should we keep a backup of original flow metadata in a separate column/table for 30 days post-migration?
3. **Order_Expiration_Notice_90 complexity:** Migrate as-is. Splitting is a separate refactoring task and out of scope for this fix.
4. **InvocableProcess flows:** The 5 Order_Expiration_Alert_* flows remain as invocable subflows after migration. They are called by Order_Expiration_Notice_90 via `actionType: "flow"` and should keep that relationship. Migration rewrites their metadata to native AutoLaunchedFlow format but preserves their InvocableProcess role (called by other flows, not triggered by records).

## Success Criteria

- 17 existing record-triggered flows continue to work identically after engine changes (regression test passes)
- 19 migrated flows execute on their correct triggers
- Order expiration email chain sends at correct day buckets (90/60/30/15/7) via Scheduled + Wait + `flow`-action fan-out
- Field auto-population flows fire on record save (Account Name, Partner Name, dates, entities)
- Invoice/Collection amount calculations execute on record create/update
- Account field-update flows (Contracting Entity) fire on record save
- Warn log emits on every `__r` resolution inside a Loop; no test fixture triggers unbounded DB calls
- Atomicity test passes: panic mid-`executeWait` leaves no orphan paused interview
- All migrated flows pass roundtrip serialization test
- Staged activation completes without duplicate field updates or recursion against existing 17 flows

## Dependencies

- Formula engine (`backend/pkg/formula`) — exists, needs wiring to executor value resolution
- SchedulerService (`scheduler_service.go`) — exists with 60s tick, needs a `resumePausedInterviews()` call added to the tick loop
- FlowInterviewService — exists with `PauseInterview()`; needs new `ResumeInterview(ctx, id, user)` method that also re-hydrates metadata and re-evaluates formulas
- FlowInterview table schema migration — add `PausedUntil TIMESTAMP NULL` column + composite index `(InterviewStatus, PausedUntil)`. Note `PausedDate` already exists (distinct semantics: "when paused" vs "when to wake"). Schema change must deploy before code
- Email infrastructure (SES) — exists, used by emailAlert actions
- ~~ApprovalService~~ — removed from scope (Partner_Account_CM_Approval deactivated in Phase 0)

## NOT in Scope

- **Dual-format engine support** (Approach B rejected) — single-format only after migration
- **`submit` action type** — consumer flow (Partner_Account_CM_Approval) being deactivated
- **Bulkification / cross-interview `__r` cache** — interview-scoped cache only, deferred to V2
- **Flow metadata backup field** — relying on dry-run + test coverage instead
- **Splitting `Order_Expiration_Notice_90`** — migrate as-is to Scheduled; splitting is a separate refactor task
- **Re-sync from Salesforce** — confirmed no future sync; flows are maintained in NexusCRM going forward
- **Wait event types beyond time-based** (`AlarmEvent`) — current flows only use time-based Waits

## What Already Exists

| Component | Status | Plan |
|-----------|--------|------|
| Flow executor dispatch loop | Full | Add Wait case only |
| Record trigger listener | Full | No change after migration |
| Interview Create/Pause/Complete | Full | Add Resume method |
| Scheduler 60s tick | Full | Add paused-interview query |
| Formula engine | Full | Wire to executor |
| Condition evaluator (20 operators) | Full | Add IsBlank only |
| `executeSubflow` | Full | Reuse for `actionType:"flow"` routing |
| emailAlert / emailSimple | Full | No change |
| FlowInterview table: CurrentElement, PausedDate | Present | Add PausedUntil + index |

## Failure Modes

| Code path | Failure scenario | Test? | Error handling? | Visible to user? |
|-----------|-----------------|-------|-----------------|------------------|
| `executeWait` — transient promotion | DB write fails during atomic promotion | YES (atomicity test) | Transaction rollback, fault connector | Silent (logged) |
| `__r` lazy-load | Related record missing | YES | Returns nil, flow continues | Silent (may surface as null in later condition) |
| `__r` lazy-load in Loop | N+1 query explosion | YES (warn log assertion) | Warn log per iteration | Operator-visible via logs |
| Formula re-eval on resume | `TODAY()` stale on wake | YES | Re-eval before Wait condition check | Could send wrong-day email |
| Scheduler query | Full table scan w/o index | Performance test | Composite index required | Scheduler lag under load |
| Migration script | Malformed processMetadataValues | YES | Dry-run diff + error exit | Migration fails loudly, data untouched |

**No critical gaps:** every failure mode has a test requirement AND error handling AND either visible error or clean silent fallback.

## Worktree Parallelization Strategy

| Step | Modules touched | Depends on |
|------|----------------|------------|
| Phase 0: Deactivate dropped flows | DB only (no code) | — |
| Phase 1: Migration script | scripts/, test fixtures | Phase 0 |
| Phase 2.1: `__r` lazy-load | executor_helpers.go | — |
| Phase 2.2: Wait element + ResumeInterview + scheduler | salesforce_flow.go, flow_executor.go, executor_elements.go, flow_interview_service.go, scheduler_service.go, system_tables.json | — |
| Phase 2.3: Formula wiring | executor_helpers.go | 2.1 (same file) |
| Phase 2.4: `flow` action type | executor_actions.go | — |
| Phase 2.5: IsBlank operator | executor_helpers.go | 2.1, 2.3 (same file) |
| Phase 3: Testing | backend/*_test.go, testdata/ | All above |

**Lanes:**
- **Lane A** (sequential, all in executor_helpers.go): __r → Formula → IsBlank
- **Lane B** (independent): Wait element + scheduler
- **Lane C** (independent): `flow` action type
- **Lane D** (sequential, post-code): Migration script → Tests → Staged activation

**Execution order:** Lanes A + B + C in parallel worktrees. Merge all. Then Lane D.

**Conflict flag:** Lane A's three steps all touch `executor_helpers.go` — run sequentially within the same worktree.

Write the migration script first. Start with the simplest flow — `Order_Account_Name` (1 decision, 1 recordUpdate, straightforward field copy). Migrate it, verify it fires on Order save, then scale to the remaining 19. The migration script is the keystone: it unblocks everything else and gives you a working test harness for the engine enhancements.

## What I noticed about how you think

- You caught the N+1 risk in Loop lazy-loading before I raised it. That's not just knowing the pattern, it's thinking about where the abstraction breaks down in practice. "懒加载在 Loop 中非常危险" — you went straight to the failure mode, not the happy path.

- You pushed back on Premise 3 (formula engine wiring) and asked for evidence. Most people accept "the engine already exists" at face value. You wanted to see the actual function coverage before agreeing. That's engineering rigor, not skepticism.

- "没有任何历史负担，不要 patch or workaround" — you set the constraint before the analysis started. That framing eliminated Approach B and C before they were even proposed. Good constraints kill bad options early.

- You included Contract as a critical object after the initial analysis was running. That's attention to scope completeness in real-time, not waiting for a review cycle to catch a gap.

## GSTACK REVIEW REPORT

| Review | Trigger | Why | Runs | Status | Findings |
|--------|---------|-----|------|--------|----------|
| CEO Review | `/plan-ceo-review` | Scope & strategy | 0 | — | — |
| Codex Review | `/codex review` | Independent 2nd opinion | 1 | CLEAR | 4 findings, 4 absorbed (business audit, staged activation, atomic wait, simpler sequencing) |
| Eng Review | `/plan-eng-review` | Architecture & tests (required) | 1 | CLEAR | 8 issues, 8 resolved (PausedDate distinction, transient promotion, resume contract, Order_Expiration_Notice_90 trigger, backup field, fixture source, composite index, Gap 4 scope-out) |
| Design Review | `/plan-design-review` | UI/UX gaps | 0 | — (N/A, backend only) | — |

**CODEX:** 4 findings all absorbed. Biggest wins: per-flow business audit (6 flows dropped, Gap 4 eliminated), staged activation in 3 waves, atomic Wait persistence transaction.

**CROSS-MODEL:** Claude and Codex agreed that Wait is the architecturally most important gap. Codex found the scope reduction opportunity Claude missed (business audit). Claude found the transient-promotion atomicity risk independently; Codex sharpened it.

**UNRESOLVED:** 0

**VERDICT:** ENG + OUTSIDE VOICE CLEARED — ready to implement. Start with Phase 0 (deactivate dropped flows), then Lanes A+B+C in parallel worktrees, then migration script.
