---
status: complete
related:
  - backend/internal/interfaces/rest/standard_action_dispatcher.go
  - backend/internal/bootstrap/standard_actions.json
  - backend/internal/bootstrap/standard_actions.go
  - backend/internal/bootstrap/standard_flows.go
  - backend/internal/bootstrap/standard_flows/Std_ConvertLead.flow.json
  - backend/internal/bootstrap/standard_flows/Std_SyncOpportunityContactRole.flow.json
  - backend/internal/bootstrap/standard_flows/Std_RelinkLeadChildren.flow.json
  - backend/internal/features/automation/flow_executor.go
  - backend/internal/features/automation/executor_record_ops.go
  - backend/internal/features/automation/executor_actions.go
  - backend/internal/features/automation/field_default_resolver.go
  - backend/internal/features/automation/field_mapping_applier.go
  - backend/internal/features/metadata/metadata_service.go
---

# Lead Convert Refactor — Outcome

This branch replaces the hardcoded `StandardActionsHandler.ConvertLead` path
with a flow-native standard-action implementation whose behavior is driven by
registry rows, flow JSON, and schema metadata rather than Lead-specific Go
branches.

## What shipped

### 1. Standard-action registry + upgrade-safe seeding

- `_System_StandardAction` remains the single registry row for an invocable
  platform action.
- `_System_ActionSchema`, `_System_ActionFieldDefault`, and
  `_System_ActionInputGuard` remain the satellite tables that describe
  input/output shape, action-scoped defaults, and permission-guarded input
  rules.
- `backend/internal/bootstrap/standard_actions.go` now reconciles satellite
  rows against the shipped pack: standard rows removed from the pack are soft-
  deleted on upgrade, reintroduced rows reactivate, and customized rows are
  preserved.
- `pack_reconcile.go` + `pack_reconcile_tables.go` extract that reconcile logic
  and pin its key columns against `system_tables.json` so the bootstrap logic
  cannot silently drift from the DDL.

### 2. Dispatcher-driven action execution

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

1. Looks up the active `_System_StandardAction` row.
2. Parses `RequiredPermissions` fail-closed. Malformed JSON is surfaced as a
   500, not silently treated as "no perms".
3. Enforces every declared permission.
4. Validates request shape from `_System_ActionSchema`.
5. Evaluates `_System_ActionInputGuard` rows.
6. Stamps `StandardActionRef` + preloaded `_System_ActionFieldDefault` rows on
   `ctx`.
7. Calls `FlowExecutor.ExecuteFlow`.

`convertLead` no longer falls through `dispatchLegacy`; a missing or inactive
registry row now yields 404, which is the correct admin-driven "action
disabled" signal. `dispatchLegacy` still exists only for `changeOwner` and
`createRecord`.

`GET /services/data/v63.0/actions/standard` also shipped as the metadata-
driven discovery surface for frontend button visibility. The UI can now render
standard-action affordances from active permitted registry rows instead of
hardcoded button logic.

### 3. Action-scoped recordCreate default pipeline

The old `_defaults.*` flow convention is gone.

For `recordCreate` elements running under a standard-action flow, the executor
now applies values in this order:

1. caller-supplied values already present in the payload
2. `_System_ObjectFieldMapping` bulk copies
3. flow `inputAssignments`
4. `_System_ActionFieldDefault` rows for the active action + target object
5. schema `DefaultValue` inside persistence
6. required-field validation inside persistence

Important details:

- unresolved `elementReference` misses are skipped so they do not block later
  default layers
- explicit JSON `null` is still preserved as author intent
- subflows inherit the parent action context, so action defaults apply by
  `TargetObject` inside `Std_SyncOpportunityContactRole` and other child flows

This is what closes the fresh-org Opportunity path: `StageName` comes from the
object schema default, and `CloseDate` comes from the seeded
`_System_ActionFieldDefault` row rather than a flow-authored `_defaults`
reference.

### 4. Atomic flow substrate

`RunAtomically` remains the single-transaction execution mode for flows that
need cross-object rollback semantics.

The engine now enforces the rule at the effective execution boundary, not just
on the child flow's own metadata:

- a flow with `RunAtomically=true` cannot contain `Wait` or `emailAlert`
- a non-atomic child invoked under an atomic parent cannot contain them either

That matters because subflows inherit the parent's open transaction even when
their own JSON omits `runAtomically`.

### 5. Flow-native Lead Convert graph

The shipped flow graph is:

```text
GetLead
  -> CheckAlreadyConverted
  -> ResolveAccount
  -> ResolveContact
  -> ResolveOpportunity
  -> CreateAccount / CreateContact / CreateOpportunity as needed
  -> SyncOpportunityContactRole (subflow)
  -> RelinkLeadChildren (subflow)
  -> MarkLeadConverted (stampConvertedAnchors)
```

Behavior that used to live in the legacy handler is now expressed as:

- flow decisions and custom errors
- `_System_ObjectFieldMapping` rows
- `_System_ActionFieldDefault` rows
- `_System_ActionInputGuard` rows
- metadata-driven converted-lookup discovery in `stampConvertedAnchors`

The old `"Prospecting"` / `"Pipeline"` literals are deleted from the platform
binary. If a tenant wants a picklist literal default, it belongs in metadata,
not Go.

## Metadata-driven scorecard

The refactor is aligned with the platform architecture for the parts it ships:

- no Lead-specific object branch was added to the generic automation engine
- no picklist/status literal is required in platform code for the convert path
- guard policy (`SkipOpportunityOnConvert`) is metadata-driven through custom
  permission + input-guard rows
- field copy rules are registry-driven via `_System_ObjectFieldMapping`
- default application moved out of flow convention and into the engine boundary
- upgrade reconciliation now keeps shipped metadata and runtime metadata in sync

## Admin changes that now require no code change

- change whether `doNotCreateOpportunity=true` is allowed:
  `_System_ActionInputGuard` + custom permission grant
- change Lead->Account / Contact / Opportunity copy rules:
  `_System_ObjectFieldMapping`
- change convert-time `CloseDate` default:
  `_System_ActionFieldDefault`
- change whether the frontend should show `convertLead`:
  `_System_StandardAction.IsActive` plus permission grants via
  `GET /actions/standard`
- add more converted-anchor lookup fields:
  create metadata that matches the `Converted<X>Id` convention

## Still deferred

- `changeOwner` / `createRecord` still live behind the remaining
  `dispatchLegacy` cases
- RecordType stage 4 is still deferred pending a profile-to-record-type
  assignment table; the current branch does not claim to solve that gap
- `sendNotificationEmail` is still out of scope because atomic flows cannot
  perform external email I/O inline
- describe-native quick-action emission is still a follow-up; the current
  metadata-driven discovery surface is `GET /actions/standard`

## Regression assets on this branch

- automation tests covering the action-default pipeline, inherited atomicity,
  converted-anchor stamping, and full `Std_ConvertLead` wiring
- bootstrap tests covering shipped-pack contracts, reconcile behavior, and
  drift between `PackTable` descriptors and `system_tables.json`
- dispatcher tests covering registry lookup, malformed permission JSON,
  permission enforcement, and `GET /actions/standard`
- e2e lead-convert cases for fresh-org Opportunity creation and admin override
  of the seeded `CloseDate` default
