# Design: Record-Triggered Flow Scheduled Paths (`#1464`)

## Status

- `Design Mode`
- current state: **review-ready design contract draft**
- issue: [#1464](https://github.com/pingcap-inc/MetaDrivenPlatform4AI/issues/1464)
- related docs:
  - [flow-system-overview.md](./flow-system-overview.md)
  - [flow-issue-remediation-plan-2026-04-26.md](./flow-issue-remediation-plan-2026-04-26.md)

## Purpose

This document turns `#1464` from a backlog item into an execution-ready design
contract.

It answers the four questions that still block implementation:

1. What is the **durable owner** of scheduled-path pending work?
2. How do later record updates **cancel / replace / reschedule** pending work?
3. What are the exact **fire-time semantics**?
4. What are the required **retry / idempotency / kill-switch** semantics?

This doc is intentionally narrower than the broader remediation plan. It is the
`#1464`-specific design slice that later implementation PRs should follow.

## Current State

Current repo/runtime evidence:

- `FlowStart.ScheduledPaths` already exists in the metadata model
  (`shared/pkg/models/salesforce_flow.go:214`).
- `FlowInterview.PausedUntil` already provides one durable wake-up primitive
  (`shared/pkg/models/salesforce_flow.go:550-556`).
- `SchedulerService` already polls due paused interviews and resumes them
  (`backend/internal/features/automation/scheduler_service.go:105-109`).
- `FlowInterviewService` already supports paused-interview claiming/resume
  semantics (`backend/internal/features/automation/flow_interview_service.go:194-224`).

References:

- `shared/pkg/models/salesforce_flow.go:214,550-556`
- `backend/internal/features/automation/scheduler_service.go:105-109`
- `backend/internal/features/automation/flow_interview_service.go:194-224`

Current gap:

- record-triggered scheduled paths are modeled in metadata but not consumed by
  the runtime
- the runtime therefore has no durable contract yet for deferred branches rooted
  at `Start.scheduledPaths[]`

## Target State

`#1464` is considered design-complete when NexusCRM has a clear contract for:

- one durable owner of pending scheduled-path work
- explicit cancellation / replacement / rescheduling semantics after later
  record updates
- explicit fire-time reload + eligibility re-check semantics
- explicit retry / idempotency / kill-switch semantics
- an implementation split and validation matrix that preserve the Salesforce
  baseline unless a delta is declared explicitly

The target system behavior is:

- admins author scheduled paths in Flow metadata
- the runtime materializes at most one active pending instance per logical
  scheduled-path key
- later qualifying saves update/reschedule that instance
- later disqualifying saves cancel it
- due execution reloads current record state and runs the scheduled branch in
  its own transaction

## Salesforce Baseline

This design assumes the following product-boundary baseline for record-triggered
Flow scheduled paths:

- scheduled paths are platform-managed deferred execution, not just “run later
  if still relevant”
- later record updates can cancel and/or reschedule pending scheduled work when
  the scheduling conditions or source time change
- fire-time execution uses current record state and re-checks eligibility

This doc does **not** claim that Salesforce internally stores those jobs in the
same shape NexusCRM will use.

It does claim that NexusCRM should preserve the externally visible behavior
unless a delta is declared explicitly.

## Non-Goals

- Flow Builder scheduled-path authoring UI
- business-hours-aware scheduling
- sub-minute precision
- tenant-specific scheduling rules in code
- shipping a second durable async authority without named necessity

## Recommended Contract

### 1. Durable owner

Recommended default: **extend `FlowInterview` as the durable owner** for pending
scheduled-path work.

Why:

- the runtime already has durable wake-up semantics there
- `FlowInterview` is already the audit/debug object for paused Flow runtime
- reusing it keeps deferred Flow state under one owner unless a concrete
  mechanism proves reuse insufficient

This is the default hypothesis, not an untouchable dogma.

If later implementation evidence proves a dedicated scheduled-job table is
required, the design must name the missing mechanism first.

### 2. Identity model

The runtime must track pending scheduled work by a stable logical key:

- `(record id, flow definition/version, scheduled path name)`

That key is the minimum needed to answer:

- does pending work for this same path already exist?
- should the new triggering save cancel / replace / reschedule it?

Recommended rule:

- at most **one active pending scheduled-path instance per logical key**
- later qualifying record updates may replace the pending instance's fire time
  and scheduling basis
- later disqualifying record updates may cancel the pending instance

This is the behavior we want to preserve from the Salesforce baseline.

### 3. Enqueue semantics

When a record-triggered Flow matches on save:

1. run the immediate start connector synchronously if one exists
2. for each `scheduledPaths[]` entry:
   - compute the candidate `fireAt`
   - resolve the logical key
   - upsert or cancel the pending scheduled-path instance according to the
     latest record state

This means scheduled-path handling is **stateful replacement logic**, not
append-only “one job per trigger event”.

### 4. Cancellation / replacement / rescheduling

This is the load-bearing semantic that must be explicit before coding.

For later saves on the same record:

- if the record still matches and the scheduled path still applies:
  - update the existing pending instance to the newest `fireAt`
  - update any fields needed to re-run the branch with the latest scheduling
    basis
- if the record no longer matches the start criteria:
  - cancel the existing pending instance
- if the source DateTime field becomes null or otherwise invalid for scheduling:
  - cancel the existing pending instance

The worker should not rely on “old jobs self-skip later” as the primary model.

That weaker behavior would be a product delta and would need explicit
declaration, justification, next-tenant impact, and verification.

### 5. Fire-time semantics

When a pending scheduled-path instance becomes due:

1. claim it atomically
2. load the current record state
3. if the record is deleted, mark the instance skipped/canceled
4. re-evaluate the original start criteria against the current record
5. if the record is no longer eligible, mark the instance skipped/canceled
6. execute the scheduled-path connector branch in its own transaction

Important distinction:

- update-time cancellation/replacement is the **primary** convergence mechanism
- fire-time eligibility re-check is the **secondary** safety net

Both are required.

### 6. Time-source semantics

Support both:

- `RecordTriggerEvent`
- `RecordField`

Recommended rules:

- `RecordTriggerEvent`: `fireAt = commit time + offset`
- `RecordField`: `fireAt = record field value + offset`

If `RecordField` is null at enqueue/update time:

- do not create or retain pending work for that path

If a later record update changes the referenced time field:

- recompute and replace the existing pending instance

### 7. Retry / idempotency / kill-switch

Recommended contract:

- delivery semantics are **at-least-once with visible audit**, not at-most-once
- the worker may retry infrastructure/runtime failures
- business-condition skips are terminal and not retried
- the design must explicitly separate:
  - retryable execution failure
  - canceled/replaced pending work
  - skipped because record is no longer eligible

Kill-switch:

- a global setting may disable scheduled-path processing
- while disabled:
  - new pending work should not be newly enqueued as active scheduled execution
    work
  - existing pending work should remain durable, not be destroyed
- re-enable should resume normal processing

Idempotency:

- because retries may happen after claim and after partial side effects,
  scheduled-path execution must be treated as replayable work
- action execution inside scheduled paths must therefore be idempotent, or the
  implementation PR must name the dedupe/guard mechanism that makes replay safe
- support/debug surfaces must expose whether an instance:
  - completed once
  - failed and was retried
  - was canceled/replaced before fire time

## Why `FlowInterview` reuse is still the recommended starting point

`FlowInterview` already gives us:

- durable storage
- wake-up time
- claim/resume path
- Flow-native audit identity

The main new requirement is not “wake something up later”.

It is:

- maintain exactly one active pending instance for a logical scheduled-path key
- mutate/cancel that pending instance when later record saves change the
  scheduling basis

So the real design question is:

- can `FlowInterview` be extended to model that key + mutable pending state
  cleanly?

If yes, reuse wins.
If no, the alternative design must name the exact missing mechanism.

## Decision Criteria: `FlowInterview` vs dedicated scheduled-job table

Choose `FlowInterview` reuse unless the alternative proves one of these:

1. the logical key and replacement semantics cannot be represented cleanly
2. claim/cancel/replace transitions become unsafe or unreadable
3. observability/support requirements fundamentally need a different primary
   object
4. reschedule/cancel behavior would require a second hidden state channel
   anyway

If none of those are proven, do not add a second durable owner.

## PR Split

### PR-A: durability contract and metadata closure

- finalize the durable owner
- finalize the logical key
- finalize cancel/replace/reschedule semantics
- complete any metadata-model additions needed for scheduled-path fidelity
- add contract tests for roundtrip + scheduling-basis computation

### PR-B: enqueue / replace / cancel path

- hook record-trigger path into scheduled-path handling
- compute `fireAt`
- create, replace, or cancel pending instances
- support mixed immediate + scheduled execution

### PR-C: worker / due execution path

- scan due pending instances
- claim atomically
- perform current-record reload
- re-check eligibility
- execute scheduled connector branch
- record completed/skipped/canceled/failed terminal states

### PR-D: admin/debug visibility and operational hooks

- support/debug visibility for pending scheduled-path instances
- metrics / logs / failure visibility
- kill-switch and operational wiring if not already landed earlier

## Validation Plan

The design is not closed until the following are covered:

1. same record, same scheduled path, later update changes the schedule time
   - pending work is replaced/rescheduled
2. same record, same scheduled path, later update breaks eligibility
   - pending work is canceled
3. `RecordField` source changes from value -> null
   - pending work is canceled
4. due execution reloads current record and re-checks eligibility
5. mixed immediate + scheduled path works in the same Flow
6. worker claim path avoids double execution
7. kill-switch preserves durability while preventing active processing

## Closure Criteria

`#1464` design is ready to leave `Design Mode` only when:

1. durable owner is frozen
2. cancellation / replacement / rescheduling semantics are frozen
3. fire-time semantics are frozen
4. retry / idempotency / kill-switch contract is frozen
5. the chosen model is justified against Salesforce baseline and current repo
   evidence
6. the implementation PR split and validation matrix are clear enough that a
   later implementer can proceed without redefining the contract
