# Issue 979 Flow-Owned Create Preview

Status: implementation-ready, revised after local smoke and review scope split
Scope class: metadata-driven standard action and Flow runtime contract
Date: 2026-05-17

## Purpose

Issue #979 defines Flow-owned copy and dynamic inherit on create. The first
implementation slice is the missing standard-action preview contract:
metadata-defined create-from-source actions need a no-DML target create image
that is resolved by the same server-side action and Flow metadata used by final
execution.

The fix must keep the platform as generic L0 substrate. It must not add object
branches to persistence, action handlers, or frontend runtime code.

## Current State

The codebase already has most of the lower-level pieces:

- `_System_StandardAction` owns metadata-defined standard action binding,
  required permissions, atomic intent, target objects, action schema, field
  defaults, guards, and internal access scopes.
- `StandardActionDispatcher.Dispatch` runs
  `POST /services/data/v63.0/actions/standard/:name` by looking up the action,
  validating action inputs, applying `$Input` defaults, evaluating guards, then
  invoking the bound Flow.
- `FlowExecutor.PreviewRecordCreateFields` already runs a Flow through the same
  recordCreate path as execution while replacing persistence with a no-DML
  preview wrapper. It captures each user-facing `recordCreate` field map after
  `_System_ObjectFieldMapping`, Flow input assignments, and
  `_System_ActionFieldDefault` rows have resolved.
- The generic preview primitive is intentionally strict: `recordUpdate`,
  `recordDelete`, Wait, locks, and non-subflow action calls fail closed. That is
  still the right default for arbitrary Flow preview, but standard-action create
  preview needs a narrower product contract: continue past metadata-owned
  update/finalizer steps as no-ops after the create images have been captured.
  The live `convertLead` Flow uses this shape for OpportunityContactRole
  primary-role maintenance and source Lead stamping.
- Frontend code can execute standard actions through `dataAPI`, but there is no
  action preview API. Legacy create prefills still rely on URL params and
  frontend field-copy logic.

The missing contract is therefore not a new mapping engine. It is a standard
action preview endpoint that shares the dispatcher's authoritative preparation
path and calls the existing Flow no-DML preview primitive.

## Contract

- Execution and preview share one metadata preparation path:
  `_System_StandardAction`, `_System_ActionSchema`,
  `_System_ActionFieldDefault`, `_System_ActionInputGuard`, and
  `_System_StandardActionInternalAccessScope`.
- Preview deliberately uses a singular v1 request shape:

  ```json
  {"input":{"sourceRecordId":"..."}}
  ```

- Preview returns one result object for that input. It is not an Actions API
  batch endpoint; batch preview can be added later only when the preview
  runtime has request-isolated synthetic-record state for every input.
- Parent preview selection uses the captured create reference graph. A graph
  root is a captured create that is referenced by descendants and does not
  itself reference another captured create. That keeps multi-level graphs such
  as parent -> child -> schedule anchored on the parent, not the most-referenced
  intermediate node. If the graph has no root, the first captured create is the
  fallback parent. `TargetObjects` remains action placement metadata only and
  is not used for preview shaping.
- Preview never commits records, updates records, deletes records, sends email,
  persists FlowInterview rows, or trusts client-stamped copied fields.
- Flow preview remains fail-closed for `recordUpdate`, `recordDelete`, waits,
  locks, and action calls other than subflow calls. Standard-action discovery
  only publishes a preview endpoint for create-only flow shapes; protected
  update/finalizer steps such as `previewRecordUpdate` and
  `stampConvertedAnchors` stay hidden until their no-DML path has live-update
  validation parity.
- Preview errors use the same `FlowExecutionError` shape as action execution so
  the UI can block Save before a user submits an impossible create.
- The endpoint is:

  ```text
  POST /services/data/v63.0/actions/standard/:name/preview
  ```

## Response Shape

The preview result has this shape:

```json
{
  "actionName": "quoteFromOpportunity",
  "isSuccess": true,
  "parentPreview": {
    "object": "TargetObjectApiName",
    "fields": {
      "Name": "Resolved value"
    }
  },
  "childPreview": [
    {
      "object": "TargetChildObjectApiName",
      "count": 3,
      "rows": [
        {"Field": "Value"}
      ]
    }
  ]
}
```

The v1 response includes row-level child captures because
`PreviewRecordCreateFields` already captures them generically. A future product
UX can choose to display only counts.

The response is a caller-readable projection, not the raw no-DML insert image.
Preview execution may use standard-action internal write scopes, but
serialization re-applies the caller's normal object read and field visibility
checks before returning parent or child fields.

## Implementation Shape

Backend:

- Add `StandardActionDispatcher.Preview`.
- Add shared registry preparation after each endpoint binds its own wire
  request (`inputs` for dispatch, `input` for preview). It performs:
  - required-permission checks;
  - request cardinality validation;
  - action schema load;
  - action field default load;
  - record-scoped default stamping on context;
  - action guard load;
  - lazy standard-action execution context creation with root flow and internal
    access scopes.
- Keep existing per-input validation helpers unchanged:
  `$Input` defaults, schema validation, picklist-tag validation, and guard
  evaluation.
- `Dispatch` calls `ExecuteFlow` after the shared preparation step.
- `Preview` calls the standard-action create preview wrapper after the same
  preparation step. The wrapper shares the generic no-DML capture engine but
  remains fail-closed for update/finalizer elements until they have full no-DML
  parity with the live action path.
- Add a preview hook test seam beside the existing executor hook so route tests
  can assert dispatcher behavior without standing up the full Flow runtime.
- Parent/child response shaping uses only the captured create graph and never
  overloads `TargetObjects`, whose existing contract is UI placement.
- Parent/child field maps are filtered through the request user's read-side
  object/FLS projection before JSON serialization.
- Register the preview route beside the execution route.

Frontend:

- Add `previewEndpoint` to standard action discovery metadata.
- Add typed `dataAPI.previewStandardAction`.
- Keep legacy URL-param create prefills as compatibility only. This PR provides
  the server-authoritative preview API that generic and app-pack create UIs can
  call; existing bespoke renderers do not become the source of truth.

## Edge Cases

- Missing action row: return the same standard-action not-found envelope as
  execution. Preview does not fall through to legacy Go action handlers.
- Missing required permissions: return the same insufficient-permissions
  envelope as execution.
- Missing or malformed `input`: return an invalid-request error before Flow
  preview runs.
- Schema/default/guard failures return `isSuccess=false` in the singular
  preview result.
- Generic Flow preview attempts recordUpdate, recordDelete, lock acquisition,
  wait, or non-subflow action calls: use the existing preview runtime errors.
- Standard-action create preview attempts `recordUpdate` or protected
  finalizer action calls such as `previewRecordUpdate` /
  `stampConvertedAnchors`: fail closed and omit the preview endpoint from
  discovery. This PR does not claim update-preview parity; that belongs to the
  stacked update substrate work.
- Standard-action create preview attempts to read rows created earlier in the
  same preview run through `recordLookup`: supported only when the lookup can
  be satisfied by the request-scoped synthetic overlay after `QueryService`
  applies object access, record access, and field projection to the preview
  row. Durable `recordLookup` elements that run before the first no-DML
  `recordCreate` remain supported because they read the same already-persisted
  source graph that live execution reads.
- Active before-create automation on a created object contains `recordLookup`:
  fail closed as the same deferred synthetic read-overlay problem, because the
  before-save flow would read durable storage without seeing earlier
  preview-created rows.
- Flow captures zero creates: return success with no `parentPreview`; the UI can
  treat this as not previewable.
- Multiple parent-like creates: choose the first captured declared target object,
  preserving Flow order and keeping authoring deterministic.
- Child creates of several objects: group child previews by object and preserve
  row order within each group.

## Rejected Alternatives

1. Add frontend-only preview mappings.
   That would duplicate `_System_ObjectFieldMapping`, Flow input assignments,
   formulas, guards, permissions, and action defaults in the browser.
2. Execute the action and roll back a transaction for preview.
   That risks side effects outside SQL rollback boundaries and makes preview
   depend on write-path behavior such as autonumber reservation.
3. Put copy/inherit behavior in `PersistenceService.Insert`.
   That would make generic persistence aware of product workflows and violate
   the L0 boundary.
4. Expand `_System_ObjectFieldMapping` into a formula or traversal language.
   The issue explicitly keeps flat copy, dynamic inherit, and child fan-out as
   separate metadata/runtime responsibilities.
5. Preview only custom Flow endpoints.
   The human create-from-source contract is standard-action scoped: permissions,
   guards, action defaults, internal access scopes, and action schema must all
   participate.

## Verification

Focused backend:

```bash
(cd backend && go test ./internal/interfaces/rest -run 'TestDispatch_KnownRegistryAction_ReachesDispatchRegistry|TestPreview_KnownRegistryAction_ReturnsCreateImage' -count=1)
```

Focused frontend:

```bash
(cd frontend && npm run build)
```

Repository hygiene:

```bash
git diff --check
```

Full baseline before merge:

```bash
(cd backend && go test ./...)
(cd shared && go test ./...)
(cd frontend && npm run build)
```

If a backend command touches `backend/internal/controlplane/packs`, set:

```bash
export NEXUSCRM_OFFICIAL_APP_PACKS_ROOT=/abs/path/to/nexuscrm-app-packs
```

`npm run build` rewrites `frontend/public/version.json`; revert that generated
change unless the version file is intentionally part of the task.

## Design Iteration Notes

1. Initial issue review showed that the desired authoring model is already
   Flow-owned: standard actions dispatch to Flow, `recordCreate` can apply
   `_System_ObjectFieldMapping`, and child fan-out remains a Flow pattern.
2. Source review found `FlowExecutor.PreviewRecordCreateFields` already exists
   and is guarded against side effects. That ruled out building a second
   preview interpreter.
3. Source review then found `StandardActionDispatcher.Dispatch` duplicates the
   exact preparation steps preview needs. The selected implementation factors
   those steps into one helper so preview cannot drift from execution.
4. The response shape was narrowed to parent/child create images instead of raw
   Flow internals. It exposes what create UIs need while keeping Flow metadata
   as the authority.
5. Final confidence check: the only new behavior is routing and shaping the
   already-resolved no-DML Flow create captures through the same standard-action
   metadata path as execution. No object names, field names, tenant facts, or
   app-specific branches are introduced.
6. Local smoke against a real TiDB + CRM pack caught a gap: `convertLead`
   captured Account/Contact/Opportunity create images, then failed when a
   subflow reached `DemoteOtherPrimaries` (`recordUpdate`). The revised design
   keeps strict preview unchanged and adds a standard-action create-preview
   wrapper that no-ops update/finalizer elements only in that endpoint's
   execution context.
7. Codex Review on PR #980 caught two boundary leaks. The final design keeps
   `TargetObjects` scoped to action placement by deriving the parent preview
   from the captured create graph root, and limits standard-action no-op
   preview semantics to protected pack-owned standard-flow context so customized
   roots and non-pack-owned/custom subflows fail closed for `recordUpdate`.
8. A later Codex Review caught that synthetic `recordLookup` overlay is a
   larger substrate than #980 should own: lookup-mediated synthetic IDs need
   complete provenance tracking, and synthetic rows must pass through the same
   read/RLS/FLS projection path as durable reads. #980 therefore keeps direct
   synthetic-ID wiring for captured creates but fails closed for lookup-after-
   create preview graphs. Capability discovery is path-sensitive: durable
   lookups before the first create are allowed, while any reachable lookup
   after a create remains deferred. The follow-up issue is #1044 and the
   design is `issue-979-preview-synthetic-read-overlay-split.md`.
9. The same fail-closed rule applies to active before-create automation on
   created objects: side-effect-safe flows are still not preview-safe if they
   perform `recordLookup`, because those reads also need the deferred synthetic
   read overlay to observe earlier no-DML creates with production read
   projection.

References:

- Issue #979: https://github.com/pingcap-inc/NexusCRM/issues/979
