Salesforce Flow Best Practices Guide
Version: 2.0.0 Last Updated: December 2025 Applies to: All flow types (Screen, Record-Triggered, Scheduled, Platform Event, Autolaunched)
This guide consolidates best practices for building maintainable, performant, and secure Salesforce Flows.
Table of Contents
Strategy & Planning
- When NOT to Use Flow ⚠️ NEW
- Pre-Development Planning ⚠️ NEW
- When to Escalate to Apex ⚠️ NEW
Flow Element Design 4. Flow Element Organization 5. Using $Record in Record-Triggered Flows 6. Querying Relationship Data 7. Query Optimization 8. Transform vs Loop Elements 9. Collection Filter Optimization
Architecture & Integration 10. When to Use Subflows 11. Custom Metadata for Business Logic ⚠️ NEW
Error Handling & Transactions 12. Three-Tier Error Handling 13. Multi-Step DML Rollback Strategy 14. Transaction Management
User Experience & Maintenance 15. Screen Flow UX Best Practices 16. Bypass Mechanism for Data Loads 17. Flow Activation Guidelines 18. Variable Naming Conventions 19. Flow & Element Descriptions ⚠️ NEW
1. When NOT to Use Flow
Before building a Flow, evaluate whether simpler declarative tools might better serve your needs. Flows add maintenance overhead and consume runtime resources—use them when their power is needed.
Prefer Declarative Configuration Over Flow
| Requirement | Better Alternative | Why |
|---|---|---|
| Same-record field calculation | Formula Field | No runtime cost, always current, no maintenance |
| Data validation with error message | Validation Rule | Built-in UI, simpler to debug, better performance |
| Parent aggregate from children | Roll-Up Summary Field | Automatic, real-time, zero maintenance |
| Field defaulting on create | Field Default Value | Native platform feature, cleaner |
| Simple required field logic | Page Layout / Field-Level Security | Declarative, no code |
| Conditional field visibility | Dynamic Forms | UI-native, better UX |
| Simple field updates on related records | Workflow Rule (if already in use) | Simpler for basic use cases |
When Flow IS the Right Choice
| Scenario | Why Flow |
|---|---|
| Complex multi-object updates | Orchestrate related changes in transaction |
| Conditional branching (3+ paths) | Decision logic beyond validation rules |
| User interaction required | Screen Flows for guided processes |
| Scheduled automation | Time-based execution |
| Platform Event handling | Real-time event processing |
| Integration callouts | HTTP callouts with error handling |
| Complex approval routing | Dynamic approval matrix |
Decision Checklist
Before creating a Flow, ask:
- Can a Formula Field calculate this value?
- Can a Validation Rule enforce this business requirement?
- Is this a simple "if changed, update field" scenario? (Consider Process Builder migration later)
- Does this require user interaction? (If no, consider automation alternatives)
- Will this run on every record save? (High-frequency = high scrutiny needed)
Rule of Thumb: If you can solve it with clicks (formula, validation, roll-up), do that first. Flows are powerful but add complexity.
2. Pre-Development Planning
Define business requirements and map logic before opening Flow Builder. Planning prevents rework and ensures stakeholder alignment.
Step 1: Document Requirements
Before building, answer these questions:
| Question | Purpose |
|---|---|
| What triggers this automation? | Defines Flow type (Record-Triggered, Scheduled, Screen) |
| What are ALL outcomes? | Identifies branches (happy path + edge cases) |
| Who are the affected users? | Determines User vs System Mode |
| What objects/fields are involved? | Identifies dependencies |
| Are there existing automations? | Prevents conflicts/duplicates |
Step 2: Visual Mapping
Sketch your Flow logic before building. Recommended tools:
| Tool | Cost | Best For |
|---|---|---|
| draw.io / diagrams.net | Free | Quick flowcharts, team sharing |
| Lucidchart | Paid | Professional diagrams, Salesforce shapes |
| Miro / FigJam | Freemium | Collaborative whiteboarding |
| Paper/Whiteboard | Free | Initial brainstorming |
Step 3: Identify Dependencies
| Dependency Type | Check Before Building |
|---|---|
| Custom Objects/Fields | Do they exist? Create with sf-metadata first |
| Custom Metadata Types | Bypass settings, thresholds, config values |
| Permission Sets | Required for System Mode considerations |
| External Systems | Callout endpoints, credentials |
| Other Automations | Triggers, Process Builders, other Flows on same object |
Step 4: Define Test Scenarios
Before building, list your test cases:
Test Scenarios for: Auto_Lead_Assignment
├── Happy Path: New Lead with valid data → Assigns correctly
├── Edge Case: Lead missing required field → Handles gracefully
├── Bulk Test: 200+ Leads created → No governor limits
├── Permission Test: User without edit access → Appropriate error
└── Conflict Test: Existing trigger on Lead → No infinite loop
Planning Deliverable Template
FLOW PLANNING DOCUMENT
═══════════════════════════════════════════════════
Flow Name: [Auto_Lead_Assignment]
Type: Record-Triggered (After Save)
Object: Lead
Trigger: On Create, On Update (when Status changes)
BUSINESS REQUIREMENTS:
1. Assign leads to reps based on territory
2. Send notification email to assigned rep
3. Update Lead Status to "Assigned"
ENTRY CONDITIONS:
- Status changed to 'New' OR Record is new
- NOT assigned to a rep yet
DECISION LOGIC:
- If Region = 'West' → Assign to User A
- If Region = 'East' → Assign to User B
- Else → Assign to Queue
ERROR HANDLING:
- If assignment fails → Log error, don't block save
DEPENDENCIES:
- Custom field: Region__c (exists ✓)
- Queue: Unassigned_Leads (exists ✓)
═══════════════════════════════════════════════════
3. When to Escalate to Apex
Flow is powerful, but Apex is sometimes the better tool. Know when to escalate to Invocable Apex.
Escalation Decision Matrix
| Scenario | Why Apex is Better |
|---|---|
| >5 nested decision branches | Flow becomes unreadable; Apex switch/if is cleaner |
| Complex math/string manipulation | Apex is more expressive (regex, math libraries) |
| External HTTP callouts | Better error handling, retry logic, timeout control |
| Database transactions with partial commit | Apex Savepoints for precise rollback control |
| Complex data transformations | Apex collections (Maps, Sets) are more powerful |
| Performance-critical bulk operations | Apex is faster for large datasets (10K+ records) |
| Unit testing requirements | Apex test classes provide better coverage metrics |
| Governor limit gymnastics | Apex gives finer control over limits |
Red Flags: When Flow is Fighting You
If you encounter these patterns, consider Apex:
🚩 RED FLAGS (Consider Apex Instead)
═══════════════════════════════════════════════════
❌ Building workarounds for Flow limitations
→ "I need to loop twice because Flow can't..."
❌ Flow canvas is unreadably complex
→ More than 20 elements, crossing connectors
❌ Performance issues at scale
→ Flow times out with realistic data volumes
❌ Need precise error messages
→ $Flow.FaultMessage isn't granular enough
❌ Complex JSON/XML parsing
→ Flow formulas are awkward for nested structures
❌ Multi-object transactions with partial rollback
→ Flow's all-or-nothing isn't sufficient
═══════════════════════════════════════════════════
The Hybrid Approach: Flow + Invocable Apex
Best practice: Use Flow for orchestration, Apex for complexity.
┌─────────────────────────────────────────────────────────────┐
│ HYBRID PATTERN: Flow orchestrates, Apex handles complexity │
└─────────────────────────────────────────────────────────────┘
Flow (Auto_Process_Order):
├── Start: Record-Triggered (Order)
├── Decision: Is Complex Processing Needed?
│ ├── Yes → Apex Action: ProcessComplexOrder (Invocable)
│ └── No → Simple Assignment Elements
├── Get Records: Related Line Items
├── Apex Action: CalculateTaxAndDiscount (Invocable)
└── Update Records: Order with calculated values
Benefits:
✓ Flow handles simple orchestration (readable)
✓ Apex handles complex math (maintainable)
✓ Apex is unit-testable (reliable)
✓ Admins can modify flow logic (accessible)
Invocable Apex Template
When escalating to Apex, use this pattern:
/**
* Invocable Apex for Flow: Complex Order Processing
* Called from: Auto_Process_Order Flow
*/
public class OrderProcessor {
@InvocableMethod(
label='Process Complex Order'
description='Calculates tax, discounts, and validates inventory'
category='Order Management'
)
public static List<OutputWrapper> processOrders(List<InputWrapper> inputs) {
List<OutputWrapper> results = new List<OutputWrapper>();
for (InputWrapper input : inputs) {
OutputWrapper output = new OutputWrapper();
try {
// Complex logic here
output.isSuccess = true;
output.message = 'Processed successfully';
} catch (Exception e) {
output.isSuccess = false;
output.message = e.getMessage();
}
results.add(output);
}
return results;
}
public class InputWrapper {
@InvocableVariable(required=true label='Order ID')
public Id orderId;
}
public class OutputWrapper {
@InvocableVariable(label='Success')
public Boolean isSuccess;
@InvocableVariable(label='Message')
public String message;
}
}
Rule of Thumb: If you're building workarounds for Flow limitations, use Apex. Flow should feel natural—if it doesn't, escalate.
4. Flow Element Organization
Structure your flow elements in this sequence for maintainability:
| Order | Element Type | Purpose |
|---|---|---|
| 1 | Variables & Constants | Define all data containers first |
| 2 | Start Element | Entry conditions, triggers, schedules |
| 3 | Initial Record Lookups | Retrieve needed data early |
| 4 | Formula Definitions | Define calculations before use |
| 5 | Decision Elements | Branching logic |
| 6 | Assignment Elements | Data preparation/manipulation |
| 7 | Screens (if Screen Flow) | User interaction |
| 8 | DML Operations | Create/Update/Delete records |
| 9 | Error Handling | Fault paths and rollback |
| 10 | Ending Elements | Complete flow, return outputs |
Why This Order Matters
- Readability: Reviewers can follow the logical flow
- Maintainability: Easy to locate elements by function
- Debugging: Errors trace back to predictable locations
5. Using $Record in Record-Triggered Flows
When your flow is triggered by a record change, use $Record to access field values instead of querying the same object again.
⚠️ CRITICAL: $Record vs $Record__c
Do NOT confuse Flow's $Record with Process Builder's $Record__c.
| Variable | Platform | Meaning |
|---|---|---|
$Record |
Flow | Single record that triggered the flow |
$Record__c |
Process Builder (legacy) | Collection of records in trigger batch |
Common Mistake: Developers migrating from Process Builder try to loop over $Record__c in Flows. This doesn't work because:
$Record__cdoes not exist in Flows$Recordin Flows is a single record, not a collection- The platform handles bulk batching automatically - you don't need to loop
Correct Approach: Use $Record directly without loops:
Decision: {!$Record.StageName} equals "Closed Won"
Assignment: Set rec_Task.WhatId = {!$Record.Id}
Create Records: rec_Task
Anti-Pattern (Avoid)
Trigger: Account record updated
Step 1: Get Records → Query Account where Id = {!$Record.Id}
Step 2: Use queried Account fields
Problems:
- Wastes a SOQL query (you already have the record!)
- Adds unnecessary complexity
- Can cause timing issues with stale data
Best Practice
Trigger: Account record updated
Step 1: Use {!$Record.Name}, {!$Record.Industry} directly
Benefits:
- Zero additional SOQL queries
- Always has current field values
- Simpler, more readable flow
When You DO Need to Query
Query the trigger object only when you need:
- Related records (e.g., Account's Contacts)
- Fields not included in the trigger context
- Historical comparison (
$Record__Prior)
6. Querying Relationship Data
⚠️ Get Records Does NOT Support Parent Traversal
Critical Limitation: You CANNOT query parent relationship fields in Flow's Get Records.
What Doesn't Work
Get Records: User
Fields: Id, Name, Manager.Name ← FAILS!
Error: "The field 'Manager.Name' for the object 'User' doesn't exist."
The Solution: Two-Step Pattern
Query the child object first, then query the parent using the lookup ID:
Step 1: Get Records → User
Fields: Id, Name, ManagerId
Store in: rec_User
Step 2: Get Records → User
Filter: Id equals {!rec_User.ManagerId}
Fields: Id, Name
Store in: rec_Manager
Step 3: Use {!rec_Manager.Name} in your flow
Common Relationship Queries That Need This Pattern
| Child Object | Parent Field | Two-Step Approach |
|---|---|---|
| Contact | Account.Name | Get Contact → Get Account by AccountId |
| Case | Account.Owner.Email | Get Case → Get Account → Get User |
| Opportunity | Account.Industry | Get Opportunity → Get Account by AccountId |
| User | Manager.Name | Get User → Get User by ManagerId |
Why This Matters
- Flow's Get Records uses simple field retrieval, not SOQL relationship queries
- This is different from Apex where you can write
SELECT Account.Name FROM Contact - Always check for null on the parent record before using its fields
7. Query Optimization
Use 'In' and 'Not In' Operators
When filtering against a collection of values, use In or Not In operators instead of multiple OR conditions.
Best Practice:
Get Records where Status IN {!col_StatusValues}
Avoid:
Get Records where Status = 'Open' OR Status = 'Pending' OR Status = 'Review'
Always Add Filter Conditions
Every Get Records element should have filter conditions to:
- Limit the result set
- Improve query performance
- Avoid hitting governor limits
Use Indexed Fields for Large Data Volumes
For orgs with 100K+ records on an object, filter on indexed fields to ensure fast query performance.
Always Indexed Fields
| Field Type | Examples | Notes |
|---|---|---|
| Id | Record ID | Primary key, fastest |
| Name | Account Name, Contact Name | Standard name field |
| CreatedDate | - | Useful for recent records |
| SystemModstamp | - | Last modified timestamp |
| RecordTypeId | - | If using Record Types |
| OwnerId | - | User lookup |
Custom Indexed Fields
| Field Type | Notes |
|---|---|
| External ID fields | Automatically indexed |
| Lookup/Master-Detail fields | Relationship fields are indexed |
| Custom fields with indexing | Request via Salesforce Support |
| Unique fields | Automatically indexed |
Performance Impact
┌─────────────────────────────────────────────────────────────────┐
│ QUERY PERFORMANCE: Indexed vs Non-Indexed │
└─────────────────────────────────────────────────────────────────┘
❌ NON-INDEXED FILTER (Slow on large objects):
Get Records: Account
Filter: Custom_Text__c = "ValueABC"
→ Full table scan = slow + timeout risk
✅ INDEXED FILTER (Fast at any scale):
Get Records: Account
Filter: External_Id__c = "ValueABC"
→ Index lookup = milliseconds
When to Request Custom Indexing
Contact Salesforce Support to request indexing when:
- Object has 100K+ records
- Query frequently filters on a specific field
- Flow timeouts occur with non-indexed filters
Tip: Use
SELECT Id FROM Object WHERE Field = 'value'in Developer Console to test query performance before building the Flow.
Use getFirstRecordOnly
When you expect a single record (e.g., looking up by unique ID), enable getFirstRecordOnly:
- Improves performance
- Clearer intent
- Simpler variable handling
Avoid storeOutputAutomatically
When storeOutputAutomatically="true", ALL fields are retrieved and stored:
Risks:
- Exposes sensitive data unintentionally
- Impacts performance with large objects
- Security issue in screen flows (external users see all data)
Fix: Explicitly specify only the fields you need in the Get Records element.
8. Transform vs Loop Elements
When processing collections, choosing between Transform and Loop elements significantly impacts performance and maintainability.
Quick Decision Rule
Shaping data → Use Transform (30-50% faster) Making decisions per record → Use Loop
When to Use Transform
Transform is the right choice for:
| Use Case | Example |
|---|---|
| Mapping collections | Contact[] → OpportunityContactRole[] |
| Bulk field assignments | Set Status = "Processed" for all records |
| Simple formulas | Calculate FullName from FirstName + LastName |
| Preparing records for DML | Build collection for Create Records |
When to Use Loop
Loop is required when:
| Use Case | Example |
|---|---|
| Per-record IF/ELSE | Different processing based on Amount threshold |
| Counters/flags | Count records meeting criteria |
| State tracking | Running totals, comma-separated lists |
| Varying business rules | Different logic paths per record type |
Visual Comparison
❌ ANTI-PATTERN: Loop for simple field mapping
Get Records → Loop → Assignment → Add to Collection → Create Records
(5 elements, client-side iteration)
✅ BEST PRACTICE: Transform for field mapping
Get Records → Transform → Create Records
(3 elements, server-side bulk operation, 30-50% faster)
Performance Impact
Transform processes the entire collection server-side as a single bulk operation, while Loop iterates client-side. For collections of 100+ records, Transform can be 30-50% faster.
XML Recommendation
⚠️ Create Transform elements in Flow Builder UI, then deploy. Transform XML is complex with strict ordering—do not hand-write.
See Transform vs Loop Guide for detailed decision criteria, examples, and testing strategies.
9. Collection Filter Optimization
Collection Filter is a powerful tool for reducing governor limit usage by filtering in memory instead of making additional SOQL queries.
The Pattern
Instead of multiple Get Records calls, query once and filter in memory:
┌─────────────────────────────────────────────────────────────────────────┐
│ ❌ ANTI-PATTERN: Multiple Get Records calls │
└─────────────────────────────────────────────────────────────────────────┘
For each Account in loop:
→ Get Records: Contacts WHERE AccountId = {!current_Account.Id}
→ Process contacts...
Problem: N SOQL queries (one per Account) = Governor limit risk!
┌─────────────────────────────────────────────────────────────────────────┐
│ ✅ BEST PRACTICE: Query once + Collection Filter │
└─────────────────────────────────────────────────────────────────────────┘
1. Get Records: ALL Contacts WHERE AccountId IN {!col_AccountIds}
→ 1 SOQL query total
2. Loop through Accounts:
→ Collection Filter: Contacts WHERE AccountId = {!current_Account.Id}
→ Process filtered contacts (in-memory, no SOQL!)
Benefits
| Metric | Multiple Queries | Query Once + Filter |
|---|---|---|
| SOQL Queries | N (one per parent) | 1 |
| Performance | Slow | Fast |
| Governor Risk | High | Low |
| Scalability | Poor | Excellent |
Implementation Steps
- Collect parent IDs into a collection variable
- Single Get Records using
INoperator with ID collection - Loop through parents, using Collection Filter to get related records
- Process filtered subset in each iteration
When to Use
- Parent-child processing (Account → Contacts, Opportunity → Line Items)
- Batch operations where you need related records
- Any scenario requiring records from the same object for multiple parents
Governor Limit Savings
With Collection Filter, you can process thousands of related records with a single SOQL query instead of hitting the 100-query limit.
10. When to Use Subflows
Use subflows for:
1. Reusability
Same logic needed in multiple flows? Extract it to a subflow.
- Error logging
- Email notifications
- Common validations
2. Complex Orchestration
Break large flows into manageable pieces:
- Main flow orchestrates
- Subflows handle specific responsibilities
- Easier to test individually
3. Permission Elevation
When a flow running in user context needs elevated permissions:
- Main flow runs in user context
- Subflow runs in system context for specific operations
- Maintains security while enabling functionality
4. Organizational Clarity
If your flow diagram is unwieldy:
- Extract logical sections into subflows
- Name subflows descriptively
- Document the orchestration pattern
Subflow Naming Convention
Use the Sub_ prefix:
Sub_LogErrorSub_SendEmailAlertSub_ValidateRecordSub_BulkUpdater
11. Custom Metadata for Business Logic
Store frequently changing business logic values in Custom Metadata Types (CMDT) rather than hard-coding them in Flow. This enables admins to change thresholds, settings, and routing logic without modifying the Flow.
Why Use CMDT for Business Logic
| Benefit | Description |
|---|---|
| No deployment needed | Change values in Setup, no Flow modification |
| Environment-specific | Different values per sandbox/production |
| Audit trail | Changes tracked in Setup Audit Trail |
| Admin-friendly | Non-developers can update business rules |
| Testable | CMDT records are accessible in test context |
Two Access Patterns
| Pattern | Syntax | SOQL Count | Use When |
|---|---|---|---|
| Formula Reference | $CustomMetadata.Type__mdt.Record.Field__c |
0 | Single known record, simple value |
| Get Records Query | Get Records → CMDT object | 1 | Multiple records, dynamic filtering |
Formula Pattern (Preferred for Single Values)
- No SOQL consumed - platform resolves at runtime
- Direct reference in conditions/assignments
- Syntax:
{!$CustomMetadata.Flow_Settings__mdt.Discount_Threshold.Numeric_Value__c}
Decision: Check_Threshold
├── Condition: {!$Record.Amount} >= {!$CustomMetadata.Flow_Settings__mdt.Discount_Threshold.Numeric_Value__c}
└── Outcome: Apply_Discount
Get Records Pattern (For Dynamic Queries)
- Consumes 1 SOQL per query
- Enables filtering, multiple record retrieval
- Visible in Flow debug logs for troubleshooting
- Useful when CMDT record name is dynamic or you need to iterate
Get Records: Flow_Settings__mdt
├── Filter: Category__c = "Discount"
├── Store All Records: col_DiscountSettings
└── Use in Loop or Transform
Rule of Thumb: Use Formula Reference when you know the exact CMDT record at design time. Use Get Records when the record selection is dynamic or you need multiple records.
What to Store in CMDT
| Value Type | Example CMDT Field | ⚠️ Key Guidance |
|---|---|---|
| Business Thresholds | Discount_Threshold__c, Max_Approval_Amount__c |
Ideal for values that change quarterly or less |
| Feature Toggles | Enable_Auto_Assignment__c |
Boolean flags for gradual rollouts |
| Record Type Names | RecordType_DeveloperName__c |
Store DeveloperName, NOT 15/18-char IDs |
| Queue/User Names | Assignment_Queue_Name__c |
Store DeveloperName, resolve ID at runtime |
| Email Recipients | Notification_Email__c, Template_Name__c |
Store template API names, not IDs |
| URLs/Endpoints | External_API_Endpoint__c |
Enables sandbox vs production differences |
| Picklist Mappings | Source_Value__c → Target_Value__c |
Great for value translations |
⚠️ CRITICAL: Never Store Salesforce IDs in CMDT
Salesforce 15/18-character IDs (RecordTypeId, QueueId, UserId, ProfileId) are org-specific. The same Queue has different IDs in sandbox vs production. Storing IDs in CMDT causes deployment failures.
❌ Wrong:
Queue_Id__c = '00G5f000004XXXX'✅ Right:Queue_Name__c = 'Support_Queue'→ Resolve ID at runtime with Get Records
Runtime ID Resolution Pattern
When you need to route to a Queue, User, or RecordType stored in CMDT:
1. Get CMDT Value:
Formula: {!$CustomMetadata.Flow_Settings__mdt.Support_Queue.Queue_Name__c}
→ Returns: "Support_Queue" (DeveloperName)
2. Get Records: Group (Queue)
Filter: DeveloperName = {!var_QueueName} AND Type = 'Queue'
Store: rec_Queue
3. Assignment:
Set {!$Record.OwnerId} = {!rec_Queue.Id}
Common Use Cases
| Use Case | CMDT Field Example | Flow Usage |
|---|---|---|
| Discount thresholds | Discount_Threshold__c = 10000 |
Decision: Amount > {!$CustomMetadata...} |
| Feature toggles | Enable_Auto_Assignment__c = true |
Decision: Feature enabled? |
| Approval limits | Max_Approval_Amount__c = 50000 |
Route based on amount threshold |
| Email recipients | Notification_Email__c |
Send email to CMDT value |
| SLA thresholds | SLA_Warning_Hours__c = 24 |
Decision: Hours > threshold |
| API endpoints | External_API_Endpoint__c |
HTTP Callout URL |
Implementation Pattern
Step 1: Create Custom Metadata Type
Object: Flow_Settings__mdt
Fields:
├── Setting_Name__c (Text, Unique)
├── Numeric_Value__c (Number)
├── Text_Value__c (Text)
├── Boolean_Value__c (Checkbox)
└── Description__c (Text Area)
Step 2: Create Records
Record: Discount_Threshold
├── Setting_Name__c = "Discount_Threshold"
├── Numeric_Value__c = 10000
├── Text_Value__c = null
├── Boolean_Value__c = false
└── Description__c = "Minimum order amount for automatic discount"
Step 3: Reference in Flow
Decision Element: Check_Discount_Eligibility
├── Condition: {!$Record.Amount} >= {!$CustomMetadata.Flow_Settings__mdt.Discount_Threshold.Numeric_Value__c}
│ └── Outcome: Apply_Discount
└── Default: No_Discount
Best Practices
| Practice | Reason |
|---|---|
| Use descriptive DeveloperNames | Discount_Threshold not Setting_1 |
| Document in Description field | Future maintainers understand purpose |
| Group related settings | One CMDT type per domain (Sales, Service, etc.) |
| Include in deployment packages | CMDT records are metadata, deploy with code |
| Test with realistic values | Verify Flow behavior with production thresholds |
Identifying Hard-Coded Candidates (Migration Checklist)
Review existing flows for these hard-coded patterns that should migrate to CMDT:
HARD-CODED PATTERN AUDIT
═══════════════════════════════════════════════════════════
📋 CHECK YOUR FLOWS FOR:
□ 15/18-character Salesforce IDs
└─ RecordTypeIds, QueueIds, UserIds, ProfileIds
└─ Example: OwnerId = '005...' → Store Queue DeveloperName
□ Hardcoded URLs or endpoints
└─ HTTP callout URLs, redirect paths
└─ Example: endpoint = 'https://api.prod...' → Store in CMDT
□ Magic numbers (thresholds, limits, percentages)
└─ Discount rates, approval limits, SLA hours
└─ Example: Amount > 10000 → Use CMDT threshold
□ Email addresses in Send Email actions
└─ Notification recipients, CC lists
└─ Example: To = 'admin@company.com' → Store in CMDT
□ Profile/Permission Set names
└─ Used in Decision conditions
└─ Store as text, query Profile/PermissionSet at runtime
□ Object API names used in dynamic references
└─ Hard-coded object strings for generic patterns
□ Picklist values used in conditions
└─ Values that might change across regions/deployments
═══════════════════════════════════════════════════════════
Validator Note: The sf-flow validator automatically flags
HardcodedIdandHardcodedUrlpatterns during analysis.
When NOT to Use CMDT
| Scenario | Better Alternative |
|---|---|
| User-specific preferences | Custom Settings (Hierarchy) |
| Frequently changing data | Custom Object with query |
| Large datasets (1000+ records) | Custom Object |
| Binary file storage | Static Resource or Files |
Tip: CMDT is ideal for business rules that change quarterly or less. For daily-changing values, use Custom Objects or Custom Settings.
Deployment vs Data Load Distinction
CMDT records are metadata, not data. This has important implications:
| Aspect | Custom Metadata Type | Custom Object / Custom Setting |
|---|---|---|
| Move between orgs | Change Sets, Metadata API, sf deploy | Data Loader, sf data import |
| Update in production | Setup → Custom Metadata Types | Data operations (update records) |
| Included in packages | ✅ Yes (managed/unmanaged) | ❌ No (data must be seeded separately) |
| Test context access | ✅ Accessible without @TestSetup |
Requires test data creation |
Common Mistake: Trying to use Data Loader to update CMDT values. CMDT records are deployed as metadata—use Change Sets, Metadata API (
sf project deploy), or Setup UI to modify values between environments.
12. Three-Tier Error Handling
Implement comprehensive error handling at three levels:
Tier 1: Input Validation (Pre-Execution)
When: Before any DML operations What to Check:
- Null/empty required values
- Business rule prerequisites
- Data format validation
Action: Show validation error screen or set error output variable
Tier 2: DML Error Handling (During Execution)
When: On every DML element (Create, Update, Delete) What to Do:
- Add fault paths to ALL DML elements
- Capture
{!$Flow.FaultMessage}for context - Include record IDs and operation type in error messages
Action: Route to error handler, prepare for rollback
Tier 3: Rollback Handling (Post-Failure)
When: After a DML failure when prior operations succeeded What to Do:
- Delete records created earlier in the transaction
- Restore original values if updates failed
- Log the failure for debugging
Action: Execute rollback, notify user/admin
Error Message Best Practice
Include context in every error message:
"Failed to create Contact for Account {!rec_Account.Id}: {!$Flow.FaultMessage}"
"Update failed on Opportunity {!rec_Opportunity.Id} during {!var_CurrentOperation}"
13. Multi-Step DML Rollback Strategy
When a flow performs multiple DML operations, implement rollback paths.
Pattern: Primary → Dependent → Rollback Chain
Step 1: Create Primary Record (e.g., Account)
- On success → Continue to step 2
- On failure → Show error, stop flow
Step 2: Create Dependent Records (e.g., Contacts, Opportunities)
- On success → Continue to step 3
- On failure → DELETE primary record, show error
Step 3: Update Related Records
- On success → Complete flow
- On failure → DELETE dependents, DELETE primary, show error
Implementation Pattern
1. Create Account → Store ID in var_AccountId
2. Create Contacts → On fault: Delete Account using var_AccountId
3. Create Opportunities → On fault: Delete Contacts, Delete Account
4. Success → Return output variables
Error Message Pattern
Use errorMessage output variable to surface failures:
"Failed to create Account: {!$Flow.FaultMessage}"
"Failed to create Contact: {!$Flow.FaultMessage}. Account rolled back."
14. Transaction Management
Understanding Flow Transactions
- All DML in a flow runs in a single transaction (unless using async)
- If any DML fails, all changes roll back automatically
- Use this to your advantage for data integrity
Save Point Pattern
For complex multi-step flows where you need manual rollback control:
- Create primary records
- Store IDs of created records in a collection
- Create dependent records
- On failure → Use stored IDs for manual rollback
Transaction Limits to Consider
| Limit | Value |
|---|---|
| DML statements per transaction | 150 |
| SOQL queries per transaction | 100 |
| Records retrieved by SOQL | 50,000 |
| DML rows per transaction | 10,000 |
Document Transaction Boundaries
Add comments in flow description:
TRANSACTION: Creates Account → Creates Contact → Updates related Opportunities
15. Screen Flow UX Best Practices
Progress Indicators
For multi-step flows (3+ screens):
- Use Screen component headers to show "Step X of Y"
- Consider visual progress bars for long wizards
- Update progress on each screen transition
Stage Resource for Multi-Screen Flows
The Stage resource provides visual progress tracking across multiple screens, showing users where they are in a multi-step process.
When to Use Stage
- Flows with 3+ screens that represent distinct phases
- Onboarding wizards (Identity → Configuration → Confirmation)
- Order processes (Cart → Shipping → Payment → Review)
- Application workflows with logical sections
How Stages Work
- Define stages in your flow resources (Stage elements)
- Assign current stage using Assignment element at each phase transition
- Display progress using the Stage component in screens
Stage Example
Flow: New Customer Onboarding
Stages:
1. Identity (collect customer info)
2. Configuration (set preferences)
3. Payment (billing details)
4. Confirmation (review and submit)
Each screen shows visual indicator: ● ○ ○ ○ → ● ● ○ ○ → ● ● ● ○ → ● ● ● ●
Benefits
| Feature | Benefit |
|---|---|
| Visual progress | Users know how far along they are |
| Reduced abandonment | Clear expectation of remaining steps |
| Better UX | Professional wizard-like experience |
| Navigation context | Users understand their position |
Implementation Tips
- Keep stage names short (1-3 words)
- Use consistent naming pattern (nouns: "Identity", "Payment" vs verbs: "Collect Info", "Enter Payment")
- Consider allowing users to click back to previous stages (if safe)
Button Design
Naming Pattern
Use: Action_[Verb]_[Object]
Action_Save_ContactAction_Submit_ApplicationAction_Cancel_Request
Button Ordering
- Primary action first (Submit, Save, Confirm)
- Secondary actions next (Save Draft, Back)
- Tertiary/Cancel last (Cancel, Exit)
Navigation Controls
Standard Navigation Pattern
| Button | Position | When to Show |
|---|---|---|
| Previous | Left | After first screen (if safe) |
| Cancel | Left | Always |
| Next | Right | Before final screen |
| Finish/Submit | Right | Final screen only |
When to Disable Back Button
Disable "Previous" when returning would:
- Cause duplicate record creation
- Lose unsaved complex data
- Break transaction integrity
- Confuse business process state
Screen Instructions
For complex screens, add instruction text at the top:
- Use Display Text component
- Keep instructions concise (1-2 sentences)
- Highlight required fields or important notes
Example: "Complete all required fields (*) before proceeding."
Performance Tips
- Lazy Loading: Don't load all data upfront; query as needed per screen
- Minimize Screens: Each screen = user wait time; combine where logical
- Avoid Complex Formulas: In screen components (impacts render time)
- LWC for Complex UI: Consider Lightning Web Components for rich interactions
16. Bypass Mechanism for Data Loads
When loading large amounts of data, flows can cause performance issues. Implement a bypass mechanism using Custom Metadata.
Setup Pattern
Step 1: Create Custom Metadata Type
Create Flow_Bypass_Settings__mdt with fields:
Bypass_Flows__c(Checkbox)Flow_API_Name__c(Text) - optional, for granular control
Step 2: Add Decision at Flow Start
Add a Decision element as the first step after Start:
Condition: {!$CustomMetadata.Flow_Bypass_Settings__mdt.Default.Bypass_Flows__c} = true
- If true → End flow early (no processing)
- If false → Continue normal processing
Use Cases
- Data migrations
- Bulk data loads via Data Loader
- Integration batch processing
- Initial org setup/seeding
Best Practice
- Document which flows support bypass
- Ensure bypass is disabled after data load completes
- Consider logging when bypass is active
17. Flow Activation Guidelines
When to Keep Flows in Draft
- During development and testing
- Before user acceptance testing (UAT) is complete
- When dependent configurations aren't deployed yet
Deployment Recommendation
- Deploy flows as Draft initially
- Validate in target environment
- Test with representative data
- Activate only after verification
- Keep previous version as backup before activating new version
Scheduled Flow Considerations
Scheduled flows run automatically without user interaction:
- Test thoroughly before activation
- Verify schedule frequency is correct
- Ensure error notifications are configured
- Monitor first few executions
18. Variable Naming Conventions
Use consistent prefixes for all variables:
| Prefix | Purpose | Example |
|---|---|---|
var_ |
Regular variables | var_AccountName |
col_ |
Collections | col_ContactIds |
rec_ |
Record variables | rec_Account |
inp_ |
Input variables | inp_RecordId |
out_ |
Output variables | out_IsSuccess |
Why Prefixes Matter
- Clarity: Immediately understand variable type
- Debugging: Easier to trace values in debug logs
- Maintenance: New developers understand intent quickly
- Consistency: Team-wide standards reduce confusion
Element Naming
For flow elements (decisions, assignments, etc.):
- Use
PascalCase_With_Underscores - Be descriptive:
Check_Account_TypenotDecision_1 - Include context:
Get_Related_ContactsnotGet_Records
19. Flow & Element Descriptions
Clear descriptions are essential for maintenance, collaboration, and Agentforce integration. AI agents use Flow descriptions to understand and select appropriate automations.
Flow Description (Critical for Agentforce)
Why This Matters
| Consumer | How They Use Descriptions |
|---|---|
| Agentforce Agents | AI uses descriptions to understand what automation does and when to invoke it |
| Future Developers | Quick understanding without readin |
…(truncated)