Glumes ITSalesforce Ecosystem Experts
Back to Blog
Development

Flow Builder vs. Apex: When to Use Which in 2026

Glumes TeamDecember 20, 20258 min read

The rule of thumb

Start in Flow. Drop to Apex when you need one of: complex loops on large collections, callouts with retry logic, dynamic SOQL, custom exceptions, or shared logic across multiple entry points.

Decision matrix

NeedFlowApex
Simple record update❌ overkill
Loop over 200+ records with calculation⚠️ hits limits
REST callout, no retry✅ (HTTP Callout)
REST callout, exponential backoff
Dynamic SOQL (fields chosen at runtime)
Reusable across triggers, LWC, REST⚠️
Business rules a BA should own
Formula field limits exceeded✅ (Formula elements)

Flow patterns that work

Record-Triggered Flow — one per object, with prioritized entry criteria:

Priority 10: Set defaults on Insert
Priority 20: Route to queue on Insert (High Priority cases)
Priority 30: Escalation timer on Update (Status → Escalated)

Use {!$Record.IsClone} and {!$Record__Prior.OwnerId} to skip re-runs correctly.

Sub-flows — extract shared logic once, invoke everywhere:

Sub-flow: Calculate_Health_Score
  Inputs:  AccountId
  Outputs: HealthScore, TopReason

Invoke from Flow, from Apex (Flow.Interview), from LWC (lightning/flowSupport).

Apex patterns Flow can't replace

Trigger framework:

public abstract class TriggerHandler {
  public void run() {
    switch on Trigger.operationType {
      when BEFORE_INSERT { beforeInsert(); }
      when AFTER_UPDATE  { afterUpdate();  }
      // ...
    }
  }
  protected virtual void beforeInsert() {}
  protected virtual void afterUpdate()  {}
}

Callout with retry:

public class ResilientHttp {
  public static HttpResponse send(HttpRequest req, Integer max) {
    Integer attempt = 0;
    while (true) {
      HttpResponse resp = new Http().send(req);
      if (resp.getStatusCode() < 500 || ++attempt >= max) return resp;
      Long backoff = (Long)Math.pow(2, attempt) * 200;
      Datetime until = Datetime.now().addMilliseconds(backoff.intValue());
      while (Datetime.now() < until) { /* CPU-safe wait */ }
    }
  }
}

(For real backoff, delegate to Queueable + System.enqueueJob with a scheduled retry.)

Migrating Process Builder

Every Process Builder should be gone. Migration script:

  1. Export PB metadata (SFDX force:source:retrieve -m ProcessBuilder)
  2. Recreate as Record-Triggered Flow with matching entry criteria
  3. Migrate one PB per PR, deploy, verify, then delete the PB
  4. Run Optimizer report weekly to catch newly created PBs

Testing

  • Flows: Flow Test framework (native, JSON test cases, CI-runnable)
  • Apex: ApexTest + assertion libraries, aim 85%+ meaningful coverage
  • End-to-end: Provar / UTAM for UI, drives both

Anti-patterns

  • One Flow per record change (fragmentation) — consolidate per object
  • Apex for record updates that a Flow does in one node
  • Formula fields hiding as Flow — if it's a value derived from same-record fields, use a Formula
  • "Just this once" hardcoded IDs — always Custom Metadata
Flow BuilderApexAutomation

Related articles