ESC

AI-powered search across all blog posts and tools

Architecture · January 25, 2026

Flow vs Apex - The Ultimate Decision Framework

A practical decision framework for choosing between Salesforce Flow and Apex, with performance comparisons, real examples, and an animated decision tree.

☕ 9 min read 📅 January 25, 2026
  • Prefer Flow for declarative logic that admins may need to maintain without a developer
  • Use Apex when you need complex logic, cross-object operations, or external callouts
  • Combining both is often the best architecture: Flow for orchestration, Apex for complexity

Every time I review a solution design with a customer, the same debate surfaces within the first ten minutes: “Should we build this in Flow or Apex?” I have been on both sides of bad decisions here. I have seen flows so complex they could only be debugged with a whiteboard and a prayer. I have also seen Apex triggers written to do things a three-node flow could handle in fifteen minutes.

The real answer is not “always use Flow” or “always use Apex.” The answer is a framework for making the right call based on the specific requirements in front of you. Let me share the framework I use.

The Problem

A developer builds a complex Opportunity automation in Apex — 300 lines of logic handling field updates, task creation, and notifications. Six months later the business wants to change the follow-up task subject. The admin cannot touch it, a dev ticket is filed, it sits in the backlog for three weeks, and the business is frustrated that such a simple change requires a deployment.

The Solution

Use a hybrid architecture: a Record-Triggered Flow handles the orchestration and simple field updates that admins may need to tweak, while an Apex Action (called from the Flow) handles any complexity that requires code. The admin owns the Flow canvas; the developer owns the Apex Action. Both can evolve independently.

The Animated Decision Tree

Flow vs Apex Decision Tree
Will an admin (non-developer)need to maintain this?YESNONeeds external HTTPcallout?Complex logic / OOP /unit testing required?YESNOYESNOFlow +Apex ActionFlow(declarative only)Flow(declarative only)Apex Trigger /ClassPerformance Comparison (per 200-record batch)Flow (Record-Triggered)~350ms avgApex Trigger (bulkified)~220ms avgFlow + Apex Action~450ms avgApproximate values — actual performance depends on org complexity and governor limit usageFlowApexHybridShorter bar = faster

The Core Principle: Maintainability First

Before I talk about technical criteria, I want to establish the most important principle: the best automation tool is the one that the people maintaining your org can actually support.

ℹ️ Architecture Insight

A perfectly architected Apex trigger that only two developers in the company understand is worse than a slightly less optimal Flow that your admin team can debug and update independently. When consulting on org architecture, always ask: “What does your team look like two years from now?”

Salesforce invested heavily in Flow because it genuinely democratizes automation. When I consult on org architecture, I ask: “What does your team look like two years from now?” If there is a good chance it is admin-heavy, Flow wins.

When Flow Is the Right Answer

Simple to Medium Field Updates and Validations

Record-Triggered Flows handle field updates, related record operations, and notification emails extremely well. The point-and-click interface means changes can be made during a Salesforce release without a development cycle.

Real scenario: An opportunity moves to “Closed Won.” The Flow should:

  • Set Opportunity.Close_Won_Date__c to today
  • Create a follow-up Task for the owner
  • Update the parent Account’s Last_Won_Opportunity_Date__c

All of this is straightforward in a Flow. No Apex needed. An admin can look at the flow, understand it, and change the task subject without filing a dev ticket.

When Admins Need to Make Regular Changes

If your automation involves thresholds, messages, or logic that business stakeholders tune regularly (“escalate after 3 days” might become “escalate after 2 days” next quarter), put that logic in a Flow where an admin can update it. Putting it in Apex means a deployment every time the business tweaks a rule.

Screen Flows for Guided Processes

Anything that requires user input mid-process belongs in a Screen Flow. Apex cannot render a UI. If the requirement is “ask the user a question and branch based on the answer,” that is a Flow.

When Apex Is the Right Answer

External Callouts

Apex with @future(callout=true) or Queueable with Database.AllowsCallouts is the only way to call external HTTP APIs from automation. Flows can call Apex Actions, but if you need an HTTP callout, Apex is unavoidable.

public class ExternalOrderService implements Queueable, Database.AllowsCallouts {
    private List<Id> orderIds;

    public ExternalOrderService(List<Id> ids) { this.orderIds = ids; }

    public void execute(QueueableContext ctx) {
        List<Order__c> orders = [SELECT Id, Name, Total__c FROM Order__c WHERE Id IN :orderIds];
        for (Order__c o : orders) {
            HttpRequest req = new HttpRequest();
            req.setEndpoint('callout:ERP_System/orders');
            req.setMethod('POST');
            req.setBody(JSON.serialize(new Map<String, Object>{
                'salesforceId' => o.Id,
                'orderName'    => o.Name,
                'total'        => o.Total__c
            }));
            new Http().send(req);
        }
    }
}

Complex Boolean Logic

Flows handle conditions well for simple branching. But when your logic has more than three levels of nesting, conditional groups with AND/OR across many fields, or dynamic conditions built at runtime, Apex is cleaner and safer. Visual complexity in a flow correlates directly with the likelihood of a bug.

// This conditional logic is unreadable as a flow — use Apex
public static Boolean requiresEscalation(Case c, Account acct, List<Case> history) {
    Boolean isPremiumCustomer = acct.Support_Tier__c == 'Platinum' || acct.Annual_Revenue__c > 1000000;
    Boolean isStalled         = c.Status == 'In Progress' && c.Last_Modified_By_Customer__c < Date.today().addDays(-2);
    Boolean hasHistory        = history.size() > 3 && history[0].CreatedDate > Date.today().addDays(-30);
    Boolean isCriticalProduct = c.Product_Line__c == 'Core Platform' && c.Impact__c == 'High';

    return isPremiumCustomer && (isStalled || (hasHistory && isCriticalProduct));
}

Unit-Tested Business Logic

Apex has a full testing framework with @isTest, test factories, mocks, and code coverage requirements. Flow testing in Salesforce has improved, but Apex is still superior for scenarios requiring precise assertions, mock callouts, and complex data setup. Any logic that must be verified automatically in a CI/CD pipeline should live in Apex.

Batch Processing

Salesforce Batch Apex is the only way to process more than a few thousand records reliably. There is no Flow equivalent for iterating over millions of records in a governor-safe way.

The Hybrid Pattern: Flow Orchestrates, Apex Executes

My favorite architecture pattern is the hybrid: a Record-Triggered Flow handles the orchestration and simple field updates, and calls an Apex Action only when it encounters complexity that declarative tools cannot handle.

@InvocableMethod(label='Process Order Fulfillment'
                 description='Sends order to ERP and updates fulfillment status')
public static List<Result> processOrders(List<Request> requests) {
    List<Result> results = new List<Result>();
    List<Id> orderIds = new List<Id>();

    for (Request r : requests) {
        orderIds.add(r.orderId);
    }

    // Enqueue the callout work — cannot do callouts synchronously from Flow invocable
    System.enqueueJob(new ExternalOrderService(orderIds));

    for (Request r : requests) {
        Result res = new Result();
        res.success = true;
        res.message = 'Order queued for ERP sync';
        results.add(res);
    }
    return results;
}

public class Request {
    @InvocableVariable(required=true)
    public Id orderId;
}

public class Result {
    @InvocableVariable
    public Boolean success;
    @InvocableVariable
    public String message;
}

The Flow triggers on Order update, handles setting simple status fields declaratively, then calls this Apex Action for the ERP sync. The admin can see and understand the flow. The developer owns the Apex. Neither steps on the other.

Governor Limit Implications

Flow Limits

Flows share the same per-transaction governor limits as Apex. A Flow that has a “Get Records” element inside a loop is exactly as bad as SOQL in an Apex loop — it burns the same SOQL budget from the same 100-query allowance.

Since Spring ‘22, Salesforce enforces Flow DML bulkification automatically in most cases: if your flow updates 200 records, the DML happens in one bulk statement. But if you have a loop with an explicit DML action inside it, you are back to one DML per iteration.

When Both Fire

If you have both a Record-Triggered Flow and an Apex trigger on the same object, they share the same governor limits. A flow that uses 40 SOQL queries and a trigger that uses 40 SOQL queries together consume 80 of the 100-query budget. Review all automations on an object together, not in isolation.

💡 Pro Tip

Use Salesforce’s Flow Trigger Explorer (Setup > Flows > Flow Trigger Explorer) to see all Record-Triggered Flows on a given object in one view, alongside their order of execution. This is the fastest way to audit automation overlap and combined governor limit usage before deploying new automations.

My Decision Checklist

Full Decision Checklist (7 Questions)

I run through these questions in order when designing automation:

  1. Will an admin need to modify this without a deployment? YES → lean toward Flow.
  2. Does it require an external HTTP callout? YES → must involve Apex.
  3. Does it require complex conditional logic (3+ levels of nesting)? YES → Apex.
  4. Does it require unit tests in CI/CD? YES → Apex.
  5. Does it involve more than ~10,000 records? YES → Batch Apex.
  6. Is it a guided user interface (screen flow)? YES → Screen Flow.
  7. Is everything above answered NO? → Flow, probably.

One More Thing: The Overlap Zone

There is a category where either tool works, and your org context decides: moderate-complexity field updates with related record operations. Both a Flow and a trigger can do this. In this overlap zone, I default to Flow if the team has admins who will maintain the org, and default to Apex if the team is developer-heavy and uses version control and CI/CD.

⚠️ Consistency Over Perfection

An org with 40 flows and 5 triggers is more maintainable than one with a random mix of 20 flows and 25 triggers covering similar scenarios in different ways. Pick a default and stick with it.

What is the most complex Flow you have ever built before deciding it should have been Apex instead? Or the reverse — Apex that you eventually replaced with a clean Flow? I always find the war stories more instructive than the theory.


Knowledge Check

You need to build automation that sends order data to an external ERP system when an Opportunity closes. An admin should be able to change which fields are sent. What is the best architecture?
A Record-Triggered Flow and an Apex trigger both exist on the Account object. If the Flow uses 60 SOQL queries and the trigger uses 50, what happens?

How did this article make you feel?

Comments

Salesforce Tip

🎉

You finished this article!

What to read next

Contents