I have reviewed hundreds of Salesforce orgs in my career, and the pattern that consistently causes the most production incidents is not bad Apex code — it is poorly designed record-triggered flows. They are easy to build, impossible to see in a stack trace, and capable of taking down an entire org under load.
This article covers the practices that actually matter in production environments. Not the basics you find in Trailhead, but the architectural decisions that determine whether your flows scale or explode.
Before-Save vs After-Save: The Decision That Changes Everything
The choice between before-save and after-save is the most consequential decision in record-triggered flow design, and most developers pick based on gut feel rather than understanding the execution model.
Before-save flows run before the record is committed to the database. They are synchronous, they can modify fields on the triggering record without consuming a DML statement, and they cannot create or update other records. This is not a limitation — it is a feature. Use before-save for:
- Calculating and stamping field values (concatenated names, fiscal periods, formatted identifiers)
- Enforcing business rules that override user input
- Setting default values that depend on other field values
After-save flows run after the record is committed. They consume DML operations, they can query related records, and they can trigger additional saves on other objects. The trade-off is governor limit exposure. Every after-save flow you add to an object is another potential source of governor limit errors under bulk operations.
The Rule I Follow
If your flow only needs to change fields on the record being saved, make it before-save. Full stop. The governor limit savings compound dramatically in bulk operations — a before-save field update costs zero DML operations regardless of whether you are saving 1 record or 200.
The Problem
Scenario: A flow on Opportunity sends a “Congratulations” notification email every time the record is saved — even when a rep just updates the description on an already-Closed Won opportunity. Customers receive duplicate emails and reps receive complaints.
The Solution
Change the flow’s “Run flow” condition from “Only when a record is created or updated” to “Only when a record is updated to meet the conditions” with the entry condition set to Stage = 'Closed Won'. The flow now fires exclusively on the transition into Closed Won — not on every subsequent edit.
Entry Conditions: Your Performance Contract
Entry conditions are the single most impactful performance setting in a record-triggered flow, and they are routinely ignored or set incorrectly.
Every record-triggered flow has a “Run flow” condition. The options are:
- Only when a record is created — runs once per new record
- Only when a record is created or updated — runs on every save
- Only when a record is updated to meet the conditions — runs only when the record transitions into the condition state
Option 3 is the one most developers overlook. It evaluates whether the record was NOT in the condition state before the save and IS in the condition state after. This is the equivalent of ISCHANGED() combined with a value check.
Without Proper Entry Conditions
- User updates Description field on a Closed Won opportunity
- Flow fires
- Notification sent again
- Customer confused
With Proper Entry Conditions
- Flow fires ONLY on the transition to Closed Won
- Subsequent updates to the record do not trigger the flow
- No duplicate notifications
- Uses “Only when updated to meet conditions” +
Stage = 'Closed Won'
Always ask yourself: “Should this flow run every time the record is saved, or only when a specific transition occurs?” If the answer is a transition, use option 3.
After setting your entry conditions, use the Flow Debugger with “Show Extended Details” to confirm the conditions are evaluated as expected before deploying to production. The debugger shows you the exact before/after field values that triggered the flow — saving hours of troubleshooting.
Avoiding Recursion: The Pattern That Works
Recursion in record-triggered flows happens when your flow updates a record, which triggers the same flow again. Salesforce has some built-in protections, but they are not foolproof, and they do not protect against cross-object recursion chains.
The reliable pattern is explicit loop prevention using a custom permission or a static boolean in an Apex-invocable method. However, the simpler approach that covers 90% of cases is correct entry condition design.
Consider an after-save flow on Account that updates a child field when the Account’s Status changes. Without entry conditions, every time any field on the Account is updated, the flow runs, potentially updating the child, which may trigger other flows. With a proper entry condition of Status ISCHANGED, the flow only runs when Status changes.
Recursion Guard Pattern (Apex Invocable)
For the 10% of cases where you genuinely need recursion protection, use this Apex invocable method:
public class FlowRecursionGuard {
private static Set<Id> processedIds = new Set<Id>();
@InvocableMethod(label='Check and Register Flow Execution')
public static List<Boolean> checkAndRegister(List<Id> recordIds) {
List<Boolean> results = new List<Boolean>();
for (Id recordId : recordIds) {
if (processedIds.contains(recordId)) {
results.add(false); // Already processed this transaction
} else {
processedIds.add(recordId);
results.add(true); // OK to proceed
}
}
return results;
}
}Call this at the top of your flow. If it returns false, end the flow immediately.
Bulkification: What Flow Does and Does Not Do For You
Salesforce Flow automatically bulkifies record-triggered flows — it collects all records in a transaction and processes them together. This is one of the genuine strengths of flow over Apex triggers for developers who do not understand Apex bulkification.
However, flow bulkification has limits you need to understand.
What is bulkified automatically:
- Record lookups within the flow (they are batched per 200 records)
- DML operations at the end of the flow (collected and committed together)
- Decision elements
What is NOT automatically optimized:
- Loops with DML inside them — this is the most common performance mistake
- Multiple Get Records elements that could be combined into one
- Subflows called in a loop
Anti-Pattern (N+1 Queries)
For Each record in collection:
→ Get Related Records ← This becomes N SOQL queries
→ Update Record ← This becomes N DML operationsCorrect Pattern (Bulk)
Get All Related Records (one query, filter by collection IDs)
For Each record in collection:
→ Assign/Calculate using in-memory collection
Bulk Update All Records (one DML, outside the loop)The One Flow Per Object Debate
Salesforce’s guidance has historically been “one flow per object per trigger event.” The reasoning is execution order — multiple flows on the same object can execute in any order, making the system unpredictable.
My practical take: consolidation is correct for flows that depend on each other’s results or that modify the same fields. Independent flows that do unrelated things can reasonably remain separate for maintainability reasons, as long as you document the execution order dependency (or lack thereof).
- Creating a monolithic flow with 40 decision branches trying to handle every scenario
- Mixing before-save and after-save logic in the same flow to avoid consolidation complexity
- Using separate flows on the same object that both update the same field (the last one wins, unpredictably)
Real Patterns for Common Scenarios
Pattern: Stamp Creating User's Role on a Record
Trigger: Before-save, Created only
Get Records: User where Id = {$Record.CreatedById}
Assignment: CreatedBy_Role__c = {UserFromFlow.UserRole.Name}This is a perfect before-save use case — zero DML cost, runs once, immutable historical data.
Pattern: Create Follow-Up Task When Case is Closed
Trigger: After-save, Updated to meet conditions
Entry Condition: Status = 'Closed' (only when updated to meet)
Create Records: Task (Subject = 'Follow up', WhatId = {$Record.Id}, ActivityDate = TODAY + 3)After-save because we are creating a related record. Entry condition ensures it fires only on the transition.
Pattern: Auto-Associate Contact to Account by Email Domain
Trigger: Before-save, Created or Updated
Entry Condition: Email ISCHANGED AND Email NOT NULL
Get Records: Account where Website CONTAINS {EmailDomain formula}
Assignment: AccountId = {FoundAccount.Id}Before-save because we are only modifying the record being saved.
Summary
Record-triggered flows are powerful, but power without discipline creates production incidents. The practices that matter most are not complex:
- Use before-save whenever you are only modifying the triggering record
- Set entry conditions that reflect the exact trigger condition, not a broad “always run”
- Keep DML and SOQL outside loops
- Protect against recursion with explicit guards when entry conditions are not sufficient
- Consolidate flows that share state, separate flows that are truly independent
The orgs that run well are not the ones that avoid flows — they are the ones where every flow has a clear, narrow purpose and explicit entry conditions that prevent it from firing unnecessarily.
What is the most complex record-triggered flow scenario you have encountered in production? I am curious whether the solutions people reach for match the patterns I have described here.
Knowledge Check
How did this article make you feel?
Comments
Salesforce Tip