sf-flow-development

Salesforce Flow development — flow types, patterns, bulkification, error handling, testing, subflows, Flow vs Apex. Use when building or reviewing Flows. Do NOT use for pure Apex or Platform Event architecture.

Salesforce Flow Development

Procedures for building, testing, and maintaining Salesforce Flows. Flow type details, governor limits, bulkification rules, and the Flow vs Apex decision matrix live in the reference file.

@../_reference/FLOW_PATTERNS.md

When to Use

  • Building or debugging Salesforce Flows (record-triggered, screen, scheduled, autolaunched, orchestration)
  • Deciding between Flow and Apex for automation
  • Configuring fault paths, bulkification, or governor-safe flow patterns
  • Implementing subflows, versioning, or modular flow design
  • Migrating from Process Builder to Record-Triggered Flows

Before Save: Field Updates (Recommended Pattern)

Flow: ACC_SetAccountPriority (Before Save, Account, before insert OR before update)
Entry Condition: {!$Record.AnnualRevenue} >= 1000000

Steps:
  1. Assignment: {!$Record.Priority__c} = "High"
  2. Assignment: {!$Record.Tier__c} = "Enterprise"

Notes:
  - No Get Records, Update Records, or Create Records elements allowed
  - No DML of any kind
  - Very fast — runs in memory before DB write

After Save: Related Record Creation

Flow: OPP_CreateNegotiationTask (After Save, Opportunity, after update)
Entry Condition: {!$Record.StageName} = "Negotiation"
                 AND {!$Record.StageName} <> {!$Record__Prior.StageName}

Steps:
  1. Get Records: User WHERE Id = {!$Record.OwnerId}
  2. Create Records: Task (
       Subject = "Review negotiation checklist"
       WhatId = {!$Record.Id}
       OwnerId = {!$Record.OwnerId}
       ActivityDate = TODAY() + 3
       Priority = "High"
     )

Before Save vs After Save Decision

Need to update the SAME record's fields?
  -> Before Save (no DML counted, faster)
  Note: Before Save flows cannot use Get Records or DML elements,
  but CAN access parent fields via cross-object formula references
  (e.g., {!$Record.Account.Name}).

Need to create/update OTHER records?
  -> After Save (can do DML on other objects)

Need to send emails or call external services?
  -> After Save (outbound actions need committed record)

Bulkification Patterns

DML Outside the Loop (Critical)

BAD:
  Loop: For each Contact in {!Contacts}
    |
    +-> Update Records: Contact  <- DML inside loop = 1 DML per record

GOOD:
  Loop: For each Contact in {!Contacts}
    |
    +-> Assignment: Add to contactsToUpdate collection

  Update Records: {!contactsToUpdate}  <- Single DML for entire collection

SOQL Outside the Loop

BAD:
  Loop: For each Opportunity in {!Opportunities}
    |
    +-> Get Records: Account WHERE Id = {!loopVar.AccountId}  <- SOQL per record

GOOD:
  Get Records: Account WHERE Id IN {!opportunityAccountIds}  <- single query

  Loop: For each Opportunity in {!Opportunities}
    |
    +-> (Use data from pre-fetched collection, no SOQL)

  Heap warning: If pre-fetch returns 10K+ records, the collection may
  exceed the heap size limit (see @../_reference/GOVERNOR_LIMITS.md). Filter aggressively, or move to Batch Apex.

Subflows: Modular Design

Parent Flow: SCR_CustomerOnboarding (Screen Flow)
  |
  +-- Subflow: VAL_ValidateAddress (Autolaunched)
  |     Input:  {!streetAddress}, {!city}, {!state}
  |     Output: {!isValidAddress}, {!normalizedAddress}
  |
  +-- Subflow: INT_CreateCRMAccount (Autolaunched)
  |     Input:  {!accountData}
  |     Output: {!newAccountId}
  |
  +-- Subflow: NOT_SendWelcomeEmail (Autolaunched)
        Input:  {!accountId}, {!contactEmail}

Variables passed between parent and subflow must be marked as available for input/output in the subflow definition.


Error Handling: Fault Connectors

Every element that can fail should have a fault connector.

[Update Records: Update Account]
         |
         +-(SUCCESS)-> [Next Step]
         |
         +-(FAULT)-> [Assignment: errorMessage =
                       "Failed: " + {!$Flow.FaultMessage}]
                         |
                         +-> [Screen: Display Error to User]
                              OR
                              +-> [Create Records: Error_Log__c]
                              OR
                              +-> [Custom Notification: Notify Admin]

Get Records Best Practices

Element: Get Records
Object: Contact
Filter: AccountId = {!$Record.Id} AND IsActive__c = true
Store All Records: No (when you need just one) / Yes (for a collection)

Tips:
  - Add filter conditions to reduce records returned
  - Select only the fields you need
  - Use "Only the first record" when you need a single result
  - Filter on indexed fields when possible (Id, OwnerId, ExternalId__c)

Custom Labels and Custom Metadata in Flows

Custom Labels

Formula Resource:
  Name: FormattedMessage
  Data Type: Text
  Value: {!$Label.Welcome_Message} & " " & {!ContactRecord.FirstName}

Custom Metadata Types

Get Records:
  Object: Service_Config__mdt
  Filter: DeveloperName = "Production"
  Fields: Endpoint_URL__c, Timeout_Ms__c

Testing Flows

Apex Test Coverage for Record-Triggered Flows

@IsTest
public class OppNegotiationFlowTest {

    @TestSetup
    static void setup() {
        Account acc = TestDataFactory.createAccount('Test Account');
        insert acc;
    }

    @IsTest
    static void testNegotiationTask_stageChanged_createsTask() {
        Account acc = [SELECT Id FROM Account LIMIT 1];

        Opportunity opp = new Opportunity(
            Name      = 'Test Opp',
            AccountId = acc.Id,
            StageName = 'Prospecting',
            CloseDate = Date.today().addDays(30),
            Amount    = 50000
        );
        insert opp;

        Test.startTest();
        opp.StageName = 'Negotiation';
        update opp;
        Test.stopTest();

        List<Task> tasks = [SELECT Subject, Priority
            FROM Task WHERE WhatId = :opp.Id];
        System.assertEquals(1, tasks.size());
        System.assertEquals('Review negotiation checklist', tasks[0].Subject);
    }

    @IsTest
    static void testNegotiationTask_bulk_createsTasksForAll() {
        Account acc = [SELECT Id FROM Account LIMIT 1];

        List<Opportunity> opps = new List<Opportunity>();
        for (Integer i = 0; i < 200; i++) {
            opps.add(new Opportunity(
                Name      = 'Bulk Opp ' + i,
                AccountId = acc.Id,
                StageName = 'Prospecting',
                CloseDate = Date.today().addDays(30),
                Amount    = 1000 * i
            ));
        }
        insert opps;

        Test.startTest();
        for (Opportunity opp : opps) { opp.StageName = 'Negotiation'; }
        update opps;
        Test.stopTest();

        Integer taskCount = [SELECT COUNT() FROM Task WHERE WhatId IN :opps];
        System.assertEquals(200, taskCount);
    }
}

New Screen Flow Components (Spring '26)

Five native Screen Flow components that eliminate the need for custom LWC in common patterns:

ComponentBest For
Kanban BoardStage/status reassignment wizards, visual prioritization
MessageConfirmation prompts, warnings before destructive steps
File PreviewDocument review steps in approval flows
Integrated ApprovalWizard-based approval flows where reps submit and track
Editable Data TableMass-edit child records within a guided wizard

Record-Triggered Flows on Files (Spring '26)

Record-Triggered Flows now support ContentVersion as a triggering object.

Object: ContentVersion
Trigger: A record is created or updated
Entry Conditions:
  - {!$Record.FileExtension} = 'pdf'
  - {!$Record.Title} Does NOT Contain 'DRAFT'

Use cases:
  - Auto-tag documents based on filename patterns
  - Notify a team when a contract document is uploaded
  - Create audit log when a sensitive file type is uploaded

Orchestration Flow Design

Orchestration Flows manage long-running, multi-step processes spanning hours or days.

Orchestration Flow: Employee_Onboarding
+-- Stage 1: "System Setup"
|   +-- Background Step: Create user account (Autolaunched Flow)
|   +-- Background Step: Provision email (Autolaunched Flow)
+-- Stage 2: "HR Review"
|   +-- Interactive Step: Complete onboarding form (Screen Flow -> HR Manager)
+-- Stage 3: "Equipment & Access"
|   +-- Interactive Step: Order equipment (Screen Flow -> IT Team queue)
|   +-- Background Step: Grant system permissions (Autolaunched Flow)
+-- Stage 4: "Orientation"
    +-- Interactive Step: Schedule orientation (Screen Flow -> Employee)

Key Design Principles

  • Stages execute sequentially -- Stage 2 waits for all Stage 1 steps
  • Steps within a stage can run in parallel
  • Interactive steps pause the orchestration until the assigned user completes the Screen Flow
  • Background steps run automatically using Autolaunched Flows
  • Define fault paths on background steps to prevent stalled orchestrations

Flow Versioning and Activation

Each Flow has:
  - Multiple versions (v1, v2, v3...)
  - Only ONE active version at a time
  - Inactive versions can be tested without activating

Deployment:
  - Deploying creates a new version
  - Set status: Active in Flow metadata to auto-activate:
<status>Active</status>
sf project deploy start \
    --metadata "Flow:OPP_CreateNegotiationTask" \
    --target-org myOrg

Process Builder Migration

1. IDENTIFY — List all active Process Builders
   sf data query -q "SELECT Id, MasterLabel, ProcessType FROM Flow
     WHERE ProcessType = 'Workflow' AND Status = 'Active'" --json

2. ANALYZE — Document trigger object, criteria, and actions

3. CREATE — Build equivalent Record-Triggered Flow:
   - Before Save Flow for field updates
   - After Save Flow for create records, email alerts

4. TEST — Deploy to sandbox, test with bulk data (200+ records)

5. DEACTIVATE — Turn off Process Builder, monitor 1-2 weeks

6. CLEANUP — Delete via destructiveChanges.xml

Related

  • Agent: sf-flow-agent -- for interactive, in-depth guidance
  • Constraints: sf-apex-constraints (for Apex-invoked flows)
  • Reference: @../_reference/FLOW_PATTERNS.md