Flow vs Apex

A decision framework for when declarative automation breaks down

Posted by Syed Zain Raza

Every Salesforce admin eventually faces the question: should this automation be a Flow, or does it need Apex? The wrong answer in either direction is expensive. Flows that should have been code become unmaintainable spaghetti that times out on bulk operations. Code that should have been Flows becomes a black box only one developer can touch, and that developer left last year.

The Default: Start With Flow

Since Salesforce retired Workflow Rules and Process Builder, Flow is the declarative tool. And the default should genuinely be Flow, for reasons that have nothing to do with technical capability. A Flow is inspectable by any admin in the org. Its logic is visible in a diagram rather than buried in a class. Changes ship without a deployment pipeline. When the person who built it leaves, the next admin can open it and understand it.

That maintainability advantage is worth real performance tradeoffs. An automation that is 20 percent slower but that three people on the team can debug beats a perfectly optimized trigger that nobody understands.

When Flow Breaks Down

Complex loop logic over large datasets. Flows can loop, but loop-heavy logic over hundreds of records inside a transaction is where Flows hit element execution limits and become slow. If your Flow diagram has nested loops with decision elements inside them, that logic wants to be code.

Sophisticated queries. Flow's Get Records element handles simple filters well. But aggregations, subqueries, and dynamic query construction are painful or impossible declaratively. If you find yourself chaining multiple Get Records elements and cross-referencing collections to simulate a join, Apex with a single SOQL query is both faster and clearer.

Callouts with complex orchestration. Flow can make HTTP callouts now, but error handling, retry logic, response parsing beyond simple JSON, and multi-step API orchestration are all clumsy. Integration logic with real failure modes belongs in Apex where you can write proper exception handling and unit tests.

Anything requiring rigorous testing. This is the criterion most people underweight. Apex requires 75 percent test coverage to deploy, and good teams treat tests as the specification of the automation's behavior. Flows have no equivalent enforcement. For automation where a silent regression costs real money - quota calculations, commission logic, billing triggers - the testability of Apex is a feature, not a burden.

The Framework

Choose FLOW when:
- Logic fits in one screen of diagram without nested loops
- The team maintaining it is admin-heavy
- Requirements change often and speed of iteration matters
- Failure is visible and recoverable (a task not created, an email not sent)

Choose APEX when:
- Bulk performance matters (data loads, integrations, high-volume objects)
- Logic involves complex queries, math, or orchestration
- Failure is silent or expensive (money, quota, compliance)
- The logic is stable and worth the cost of a deployment pipeline

The Hybrid Pattern Nobody Talks About Enough

The best answer is often both: an invocable Apex method wrapped in a Flow. The heavy lifting - the complex query, the callout, the calculation - lives in a small, well-tested Apex class exposed with @InvocableMethod. The orchestration - when it fires, what happens with the result, who gets notified - lives in a Flow that admins can modify.

public class LeadScoringService {
    @InvocableMethod(label='Calculate Lead Score')
    public static List<Decimal> calculateScores(List<Id> leadIds) {
        // Complex, tested, bulk-safe logic here
    }
}

This splits the system along its natural seam: engineers own the logic that needs rigor, admins own the process that needs agility. Neither side blocks the other.

One Transaction, One Architecture

Whatever you choose, the real killer is mixing automation types on the same object without a plan. A record-triggered Flow, two legacy Process Builders, and an Apex trigger all firing on Lead update do not compose - they cascade unpredictably, re-trigger each other, and burn governor limits. Pick one automation entry point per object (a single record-triggered Flow, or a single trigger with a handler framework), and route all logic through it. The orgs that skip this rule are the orgs where every deploy is a surprise.