# Design: Flow-Native Lead Convert

## Status: DRAFT
## Date: 2026-04-23
## Author: Daemonxiao

---

## Overview

`convertLead` should be implemented as a metadata-driven standard action, not as a bespoke backend handler.

The target execution path is:

1. `POST /actions/standard/:name` reaches a generic standard-action route.
2. The backend resolves `:name` from `_System_StandardAction`; `convertLead` is only the action name.
3. Input validation and default materialization come from metadata-owned schema rows, guard rows, and default rows.
4. The action dispatches a seeded Flow, `Std_ConvertLead`.
5. `Std_ConvertLead` executes the conversion and calls supporting subflows.

This keeps Lead Convert aligned with the repository’s metadata-driven architecture:

- backend code provides only generic substrate
- platform-owned behavior is expressed as public seed metadata
- tenant-specific behavior stays in tenant-private metadata, not public code
- the Lead Convert-specific implementation track is limited to seeded Flow metadata, while remaining runtime work is limited to generic standard-action substrate gaps and the Flow-engine gaps documented in `docs/design/flow/flow-engine-gap-analysis.md`

## Goals

- Remove the bespoke `convertLead` handler as the system of record.
- Make Flow metadata the orchestration authority for Lead Convert.
- Preserve Salesforce-compatible `LeadConvert` semantics by inferring mode from field presence instead of synthetic enums.
- Keep tenant-specific rules, defaults, permissions, and child behavior out of the public repo.
- Close only the generic standard-action substrate and the Flow-engine gaps required for the seeded Flow path.

## Non-Goals

- public-repo storage of tenant-specific mappings or validation rules
- public-repo storage of tenant-specific child-relink registries
- Go-owned fallbacks for Opportunity defaults, converted anchors, or converted-lead write protection
- imperative re-implementation of Lead Convert logic in a bespoke handler
- admin un-convert behavior; if needed, it should be designed as a separate metadata-driven standard action

## Design Principles

### Flow is the system of record

Lead Convert orchestration belongs to Flow metadata. Backend code is limited to generic action dispatch, metadata lookup, and Flow execution.

### Metadata defines behavior

- action schema is metadata-owned and stored as rows matching Salesforce v63 `LeadConvert`
- defaults are metadata-owned
- converted anchors are metadata-discovered
- write protection is metadata-enforced

No Lead-specific Go branch should substitute for missing metadata. In particular, invented mode fields such as `contactMode` and `opportunityMode` are forbidden; Contact, Opportunity, and Account paths are inferred from field presence inside `Std_ConvertLead`.

### Public and private metadata stay separate

The public repo may ship generic substrate and platform-owned standard seed rows. Tenant-specific metadata must remain private.

## End-to-End Contract

The convert path is defined as the following contract:

1. Dispatch:
   `convertLead` is resolved through the generic standard-action mechanism, not through a dedicated handler. The route shape is `POST /actions/standard/:name`.

2. Input handling:
   the action contract is metadata-owned, represented by metadata rows matching Salesforce v63 `LeadConvert`, and does not use a Go DTO or struct as the authority. Create-vs-reuse behavior is inferred from field presence.

3. Execution:
   the resolved action invokes `Std_ConvertLead`.

4. State transition:
   `Std_ConvertLead` performs the conversion, updates the Lead’s converted state, and invokes its subflows.

5. Downstream behavior:
   downstream org-owned automation may observe the converted state transition after the standard action commits, but that post-commit org-specific behavior is not encoded in the public standard action and is outside the guaranteed atomic execution boundary of `Std_ConvertLead`.

## Behavioral Contract

### Core semantics

- Convert is synchronous and atomic across `Std_ConvertLead` and the supporting subflows it invokes. If the Flow runtime cannot guarantee a single DB transaction across that execution boundary, the missing transaction/savepoint primitive is a blocking platform gap and `convertLead` must not ship with partial-commit semantics.
- The guaranteed atomic boundary is the `Std_ConvertLead` execution graph plus the supporting subflows it invokes through the standard action path. Org-owned downstream automation that only observes the converted state is a post-commit compatibility concern, not part of that transaction boundary.
- `Std_ConvertLead` is a run-to-completion Flow. It does not rely on wait execution, deferred resumption, or scheduler-driven continuation.
- The Lead is retained as the historical anchor.
- Converted Leads become read-only.

### User-visible behavior

- The `Convert` affordance is shown only when the Lead is not converted and the caller has convert permission.
- Contact reuse is manual, not automatic.
- Account, Contact, and Opportunity create-vs-reuse behavior is inferred from field presence rather than synthetic mode flags.
- Opportunity skip behavior is enforced through metadata-owned `_System_ActionInputGuard`, not a hardcoded permission constant.
- Existing Opportunity paths preserve the existing Opportunity campaign attribution.
- Converted Leads remain reportable and searchable as historical records.

### Data semantics

- New Account, Contact, and Opportunity records are populated from metadata-defined mappings when those records are created by the Flow. Platform-owned standard Lead-to-Account, Lead-to-Contact, and Lead-to-Opportunity mappings may be public seed metadata; org-specific extensions for any of those targets remain tenant-private metadata.
- Ownership inheritance follows the converted record rules: explicit owner override first, then Lead owner when it is a User, then converter for queue-owned Leads, then metadata-owned fallback if present.
- `OpportunityContactRole` synchronization uses `(OpportunityId, ContactId)` as the identity key. If a row already exists for that pair, the Flow updates that row in place; otherwise it creates one. The target row becomes primary, any existing primary OCR on the same Opportunity is demoted, and role/default values come from metadata-defined defaults.
- CampaignMembers move from Lead to Contact.
- Open Task/Event activity is re-linked using `IsClosed = false` as the filter. `WhoId` is moved from Lead to Contact. `WhatId` is moved to the resolved Opportunity when one exists, otherwise to the resolved Account. Historical closed activity remains unchanged.
- Primary Campaign Source for a new Opportunity follows this precedence:
  1. metadata-defined manual action input when present
  2. newest qualifying CampaignMember
  3. next qualifying CampaignMember in the same ordering
- A qualifying CampaignMember is one with a non-null `CampaignId` and not soft-deleted. Selection order is `CreatedDate DESC`, then `Id DESC` as a stable tiebreak.
- If no qualifying campaign exists, Convert still succeeds and emits an attribution-loss signal.

## Metadata Model

### Platform-owned public substrate

The public repo should own only generic platform substrate and standard seed metadata:

- `_System_StandardAction`
- metadata-owned action input schema rows
- `_System_ActionInputGuard`
- `_System_ActionFieldDefault`
- seeded Flow definitions for:
  - `Std_ConvertLead`
  - `Std_ResolveLeadPrimaryCampaign`
  - `Std_SyncOpportunityContactRole`
  - `Std_RelinkLeadChildren`
- platform-owned standard field-mapping rows for Account, Contact, and Opportunity creation paths
- platform-owned standard default rows

### Org-owned private metadata

The following remain private, org-owned metadata:

- custom Lead mapping rows
- converted Lead `FieldPermissions`
- converted Lead `ValidationRule`
- permission assignments for skipping Opportunity creation
- tenant-specific defaults
- tenant-specific child-relink extensions
- any downstream org-specific post-convert automation

### Runtime metadata discovery

- converted anchors are discovered at runtime from Lead `FieldDefinition` rows where `ReferenceTo IS NOT NULL AND APIName LIKE 'Converted%Id'`; the legacy `leadConvertAnchorAccount`, `leadConvertAnchorContact`, and `leadConvertAnchorOpportunity` constants are removed completely
- Opportunity defaults for `StageName`, `RecordTypeId`, `CloseDate`, and `ForecastCategory` come from `FieldDefinition.DefaultValue` plus `_System_ActionFieldDefault` formula rows, with no Go fallback
- converted-lead write protection comes from metadata, not service-layer allowlists

## Flow Topology

### `Std_ConvertLead`

Responsibilities:

- validate input through metadata-defined guards
- infer Account / Contact / Opportunity path from field presence
- resolve converted anchors from field-definition metadata
- apply mapping metadata for Account, Contact, and Opportunity creation paths
- materialize metadata-defined defaults
- update the Lead converted state
- invoke the supporting subflows
- run synchronously to completion without wait elements

### `Std_ResolveLeadPrimaryCampaign`

Responsibilities:

- inspect Lead campaign history
- honor metadata-defined manual campaign input when present
- otherwise resolve the qualifying campaign for new Opportunity creation
- return the selected campaign and display context
- emit no-attribution signal when no qualifying campaign exists

### `Std_SyncOpportunityContactRole`

Responsibilities:

- ensure Contact/Opportunity linkage semantics
- create or update the primary `OpportunityContactRole`
- keep role/default behavior metadata-owned

### `Std_RelinkLeadChildren`

Responsibilities:

- handle platform-owned standard child paths in Flow
- re-link standard activity and campaign records using Flow lookups, loops, decisions, and updates

This design does not reintroduce a public child-relink registry. Tenant-specific extensions belong in tenant-private metadata outside this repo.

## Removed Artifacts

The Flow-native design removes these artifacts from the Lead Convert path:

- `_System_LeadConvertChildRelink`
- `standard_lead_convert_child_relinks.json`
- `lead_convert_child_relinks.go`
- `lead_convert_child_relink.go`
- the `RecordFilterExpr` SOQL-string substrate
- the 500-record cap
- `PermSkipOpportunityOnConvert`

## Current Platform Baseline

The current Flow runtime already provides most of the Flow execution primitives required by the seeded path:

- decisions
- loops
- record lookup
- record create/update/delete
- subflow execution
- formula evaluation wiring
- `IsBlank`
- relationship traversal used by current flow execution

`Std_ConvertLead` itself must not use wait execution. It is defined as a synchronous, run-to-completion Flow.

The remaining problem is not “Lead Convert needs a bespoke runtime.” The remaining problem is closing the generic substrate and Flow-engine gaps that still prevent the seeded Flow path from becoming authoritative.

Any remaining implementation work should be framed as:

- closing generic standard-action substrate gaps
- closing only those Flow-engine gaps documented in `docs/design/flow/flow-engine-gap-analysis.md` that block the seeded Flow path

It should not be framed as a separate imperative reimplementation of Lead Convert in backend code.

## Minimal Capability Gaps

### Generic Standard-Action Substrate Gaps

| Gap | Why it blocks the target design |
|---|---|
| Generic standard-action dispatch by name | `convertLead` cannot become metadata-resolved until the backend dispatches standard actions generically |
| `_System_StandardAction` seed for `convertLead` | there is no platform-owned action row that binds `convertLead` to `Std_ConvertLead` |
| metadata-owned action input schema and `_System_ActionInputGuard` runtime | the action contract must move out of Go DTOs and into generic metadata-driven validation |
| `_System_ActionFieldDefault` runtime | converted status and Opportunity defaults must be resolved from metadata instead of handler fallbacks |

### Flow / Flow-Engine Gaps

| Gap | Why it blocks the target design |
|---|---|
| Seeded Lead Convert flows | `Std_ConvertLead` and its supporting subflows do not yet exist as public seed metadata |
| Flow transaction/savepoint primitive | atomic convert semantics require the entire `Std_ConvertLead` execution boundary to commit or roll back as one unit |

## Verification

The design is ready to implement when these checks are satisfied:

- `convertLead` is resolved through generic standard-action dispatch using `POST /actions/standard/:name`
- the action resolves from `_System_StandardAction`
- `Std_ConvertLead` and its supporting subflows execute successfully
- no bespoke Lead Convert handler remains authoritative
- no Go fallback owns converted anchors or Opportunity defaults
- no tenant-specific metadata is added to the public repo
- `lc-001..lc-005.json` are restored to the Salesforce oracle shape rather than patched to fit an imperative handler
- the runtime proves that the `Std_ConvertLead` execution boundary commits or rolls back atomically
- child relink executes without a Lead Convert-specific 500-record cap or silent truncation
- campaign resolution, `OpportunityContactRole` synchronization, and child relink semantics match Salesforce-derived oracle evidence rather than implementation preference

Verification should include:

1. focused backend tests for standard-action dispatch and metadata-driven guard/default runtime
2. Flow execution tests for `Std_ConvertLead` and supporting subflows
3. contract tests for the `convertLead` endpoint shape, with `lc-001..lc-005.json` kept as the public oracle fixtures for request/response semantics
4. browser verification for the Lead convert UI flow
5. focused flow or contract tests that exercise campaign resolution, `OpportunityContactRole` synchronization, and child relink semantics against Salesforce-derived oracle evidence; when public fixtures are not sufficient, the implementation should attach live Salesforce comparison evidence in review
6. deployment verification in environments that supply org-owned metadata, treating downstream org-owned automation as a post-commit compatibility check rather than part of the standard action transaction boundary
7. verification that any remaining work is a Flow substrate or Flow-engine gap closure, not bespoke Lead Convert logic added to backend code
