A Salesforce Flow enters an infinite recursive loop when an update initiated by the Flow triggers the very same Flow—or another automated process—to execute again in an endless cycle. Because modern Record-Triggered Flows run within the platform’s core save sequence, a recurring trigger rapidly consumes system resources. Salesforce eventually terminates the transaction by throwing errors such as Apex CPU time limit exceeded, CANNOT_EXECUTE_FLOW_TRIGGER, or a governor limit fault when the transaction exceeds the maximum depth of 16 recursive loop iterations.
Fast Fix
Open your Record-Triggered Flow in Flow Builder. If the Flow only modifies fields on the record that started it, change the Flow type from an After-Save Flow to a Fast Field Updates (Before-Save) Flow, which updates values directly on
$Recordwithout calling an Update Records element. For After-Save Flows, select “Only when a record is updated to meet the condition requirements.”
Diagnostic Snapshot
- Error Signatures:
Apex CPU time limit exceeded,CANNOT_EXECUTE_FLOW_TRIGGER: Maximum stack depth reached: 16,Number of DML statements: 151 out of 150. - Primary Trigger: An After-Save Flow executing an “Update Records” element on
$Record, or circular updates between two related objects (Object A updates Object B, which updates Object A). - Execution Boundary: Salesforce limits recursive save sequences to 16 automated iterations before throwing an uncatchable runtime fault.
- Access Needed: Salesforce Administrator or “Manage Flows” permission to modify flow triggers and conditions.
Understanding the Recursion Mechanism
To fix a recursive flow, you must understand where it sits in the Salesforce Order of Execution:
- Before-Save (Fast Field Updates): Modifies the record’s in-memory values before the database commit. Because no database write occurs, it does not re-fire triggers, validation rules, or workflow cycles.
- Database Commit: The record is written to the database.
- After-Save (Actions and Related Records): Executes after the initial record commit.
If an After-Save Flow contains an Update Records element targeting $Record (the triggering record), Salesforce initiates a brand-new save sequence. It re-evaluates Apex triggers, Validation Rules, and Record-Triggered Flows from step one. If the Flow does not have strict entry conditions, it fires again, updates the record again, and loops until the platform forcibly kills the thread.
Diagnostic Path
Flow Fails with Recursion or Governor Limit
├── Is the Flow updating the record that triggered it?
│ ├── Yes ──► Move logic to "Fast Field Updates" (Before-Save)
│ └── No (Updating related records)
│ ├── Check Entry Criteria: Is "Only when record is updated to meet requirements" checked?
│ └── Check Cross-Object Loop: Does related object trigger an update back to this object?
└── Did the failure manifest as a general CPU timeout?
└── Inspect Apex Debug Logs: Search for FLOW_START and FLOW_END occurrences
Step 1: Trace the Loop in Debug Logs
- Go to Setup > Debug Logs and create a Trace Flag for the user encountering the error. Set Workflow to
FINESTand Apex Code toFINE. - Trigger the failing record edit.
- Open the resulting log in the Developer Console and press
Ctrl + F(orCmd + F). - Search for:
FLOW_START - If the same Flow API Name appears 10 to 16 times in succession within a single execution block, you have confirmed an automated recursion loop.
Step-by-Step Resolution
Method 1: Convert to Fast Field Updates (Before-Save)
If your Flow updates fields on the same record that initiated the process:
- Open the Flow in Flow Builder.
- Click the Start element and click Edit.
- Under “Optimize the Flow for:”, select Fast Field Updates.
- Click Done.
- Remove the downstream Update Records element entirely. Instead, use an Assignment element:
- Variable:
{!$Record.Status__c} - Operator:
Equals - Value:
Closed
- Variable:
- Save and activate the Flow as a new version.
Because Fast Field Updates execute before data is committed to the database, values are set in memory. No secondary DML event is generated, entirely eliminating recursion.
Method 2: Enforce “Updated to Meet Criteria”
If your Flow must run in the After-Save context (for instance, to send an email alert, post to Chatter, or update related child records):
- In the Start element, verify your Entry Conditions.
- Ensure you specify explicit field values (e.g.,
Status Equals 'Approved'). - Under “When to Run the Flow for Updated Records”, select:
“Only when a record is updated to meet the condition requirements”
This setting prevents the Flow from re-firing if an update occurs while the record already meets the criteria. It ensures the Flow executes only once: at the exact moment the field transitions from another value into the target value.
Method 3: Break Circular Cross-Object Loops
A common architectural pitfall occurs between two related objects:
- A Flow on
Contactupdates the parentAccountwhen a phone number changes. - A separate Flow on
Accountsyncs its phone number down to all childContactswhenever the Account updates. - Result: Contact updates Account -> Account updates Contact -> Contact updates Account.
To resolve circular dependencies:
- Establish a clear System of Record direction. Dictate that child records inherit values from the parent, but updates originating from the child do not trigger a cascading sync back down.
- Implement an entry guard condition using a timestamp or checkbox (e.g.,
Last_Synced_From_Parent__c) to prevent secondary flows from firing during automated sync passes.
Method 4: Use Custom Apex Invocable Bypass for Complex Logic
When complex orchestrations require an After-Save Flow to update multiple related records that might call custom triggers, declarative tools may struggle to suppress recursion.
- Build an Invocable Apex Action that sets a static bypass variable:
public class FlowRecursionControl {
public static Boolean isAlreadyRun = false;
@InvocableMethod(label='Check and Set Recursion Flag')
public static List<Boolean> checkRecursion() {
if (isAlreadyRun) {
return new List<Boolean>{ true }; // Stop execution
} else {
isAlreadyRun = true;
return new List<Boolean>{ false }; // Allow execution
}
}
}
- Call this action as the first step in your Flow to exit cleanly if the thread has already processed.
Common Misdiagnoses
- Misattributing CPU Limits to Apex Code: Administrators often see
System.LimitException: Apex CPU time limit exceededand assume a developer wrote bad Apex code. However, a recursive Flow executing 15 times in the background will exhaust the 10,000 ms CPU limit through platform overhead alone. If you need to isolate Apex overhead from Flow execution, see Fix: Salesforce “Apex CPU Time Limit Exceeded” (Error 1001). - Row-Level Lock Errors: If two users edit records that trigger recursive cross-object updates, the secondary loop will frequently crash with
UNABLE_TO_LOCK_ROWbefore the 16-iteration limit is reached. If lock timeouts dominate your logs, review Troubleshooting: Salesforce “Record Locked” – Resolving Row-Level Lock Conflicts.
Hard Stops
- Do not build recursive loop counters using custom number fields. Attempting to control loops by incrementing a custom field (
Loop_Count__c = Loop_Count__c + 1) still incurs database DML costs and exhausts governor limits. Correct the entry conditions or switch to Before-Save logic instead. - If a Flow recursion involves managed package objects where you cannot view or edit the underlying package triggers, halt configuration changes. Contact the vendor to identify the exact field updates their triggers listen for to avoid setting off their internal handlers.