[object Object]

Flow recursion is not “a bug in the Flow runtime.” It is what happens when a record update re-enters the same flow that just updated the record. The runtime is doing exactly what you told it. You forgot to tell it to stop.

In 2026 you have better tools for this than the Boolean-on-record hack everyone used in 2022. Here is the current playbook.

The shapes of recursion that bite

Four common shapes. Diagnose before you guard.

  • Self-loop. Flow A on Opportunity update sets a field on the same Opportunity. The update fires Flow A again.
  • Cross-flow loop. Flow A updates Contact, which triggers Flow B that updates Account, which has a rollup that updates Opportunity, which fires Flow A.
  • Apex-flow loop. Apex trigger updates the record, fires record-triggered flow, flow calls invocable Apex, Apex updates the record again.
  • Async re-entry. Flow runs in async path on initial save. The async update fires the same flow’s “any update” entry.

The guard pattern is different for each. Stop reaching for the same hammer.

Guard 1: change-tracking entry condition

The simplest guard. In the flow start element, require that the field you’re about to modify has actually changed in a meaningful way.

Entry Condition (formula):
ISCHANGED({!$Record.Stage})
AND NOT(ISCHANGED({!$Record.LastFlowTouchVersion__c}))

LastFlowTouchVersion__c is a hidden number field your flow increments at the end of its run. If the only thing that changed since last save is that version field, the entry condition rejects — recursion stops.

Cost: one custom field per object, one extra update at end of flow. Cheap. Survives Apex co-execution because the field is real.

Guard 2: Custom Permission as a static lock

For invocable-Apex callouts that themselves write the same record, set a temporary Custom Permission via a Permission Set assignment, then strip it. The flow’s entry condition checks the permission state on the running user.

This survives across transaction boundaries (unlike static Apex variables, which die after the transaction).

@InvocableMethod(label='Sync Opportunity to External')
public static void sync(List<Id> ids) {
  // We are about to write the record back. Suppress the flow.
  FlowGuard.acquire('OpportunitySync');
  try {
    // ...do external sync, update record...
  } finally {
    FlowGuard.release('OpportunitySync');
  }
}
public class FlowGuard {
  // Transactional. For multi-transaction guards use Custom Permission flips.
  public static Set<String> active = new Set<String>();
  public static void acquire(String name) { active.add(name); }
  public static void release(String name) { active.remove(name); }
  public static Boolean isHeld(String name) { return active.contains(name); }
}

The flow exposes FlowGuard.isHeld via an invocable method and checks it in the entry decision.

Guard 3: the “true change” custom formula

For numeric or text fields where you only want the flow to fire on a meaningful change (not a no-op write), build a HasMeaningfulChange__c formula field and put it in the entry condition.

HasMeaningfulChange__c =
  AND(
    ISCHANGED(Amount),
    ABS(Amount - PRIORVALUE(Amount)) > 100
  )

Filters out 1-cent rounding writes from the billing integration. Often eliminates 60-80% of useless flow runs without any code.

Guard 4: the static variable trick for in-transaction recursion

If you have Apex-triggered flow recursion within a single transaction, an Apex helper holds the state.

public class FlowRecursionGuard {
  private static Map<String, Set<Id>> processed = new Map<String, Set<Id>>();

  public static Boolean shouldRun(String flowName, Id recordId) {
    if (!processed.containsKey(flowName)) {
      processed.put(flowName, new Set<Id>());
    }
    Set<Id> seen = processed.get(flowName);
    if (seen.contains(recordId)) return false;
    seen.add(recordId);
    return true;
  }
}

Expose shouldRun as an invocable method and use it as the first decision in the flow. After the flow runs, the record id is in the set; subsequent re-entries within the same transaction return false.

Limitation: dies at transaction boundary. Doesn’t help for async re-entry or batch.

Guard 5: async path discipline

Run the heavy work in an async path, run the recursion-protection check on the synchronous path, and never let the async path call back into the synchronous trigger.

The async path in record-triggered flow runs in a separate transaction. The static guard above doesn’t carry over. Use the version-field pattern (Guard 1) instead, since it persists to the database.

When to abandon the flow and rewrite in Apex

You are over-fitting if you stack three or more guards on a single flow. At that point the flow is doing real engineering work that deserves real engineering practices — unit tests, peer review, versioning.

If you’re rewriting in Apex, the Apex triggers bulkification checklist covers the patterns to copy and the ones to avoid.

Monitoring recursion

Pull this report monthly. It surfaces flows that fire on the same record more than three times in a single transaction — the signature of an undetected loop.

List<FlowInterview> chatty = [
  SELECT InterviewLabel, CurrentElement, CreatedDate,
         RelatedRecordId__c, COUNT(Id) ct
  FROM FlowInterview
  WHERE CreatedDate = LAST_N_DAYS:1
  GROUP BY InterviewLabel, CurrentElement, CreatedDate, RelatedRecordId__c
  HAVING COUNT(Id) > 3
];

UX note

If a recursion guard short-circuits a flow, log it to a FlowGuardEvent__e platform event with the flow name and record id. Surface a stale-data warning on the record page if guard events exceed a threshold for that record in 24h. Tells users their data may be in a partial state and offers a “Recalculate” action.

Bottom line

  • Pick the guard pattern that matches your recursion shape — self-loops, cross-flow, Apex-flow, and async-reentry each need a different one.
  • Prefer the version-field entry condition for cross-transaction guards.
  • Use Apex static helpers only for in-transaction recursion; they don’t survive async boundaries.
  • Three guards on one flow means the flow has become an undocumented service — rewrite it in Apex.
  • Monitor flow interview frequency per record-day; it is the cleanest signal of an undetected loop.
[object Object]
Share