Use Flow when the standard elements can express the requirement and the person who will maintain it works in Setup. Use Apex when the work needs code: complex data handling, tight control over a transaction, or logic that a flow canvas would turn into something nobody wants to open. When one automation needs both, build it as a flow and call Apex from an Action element. That is the short answer to the Apex vs Flow question, and the rest of this article shows how to test it against your own requirement.
The decision matters more than it used to. Salesforce ended support for Workflow Rules and Process Builder on December 31, 2025, so teams are rebuilding that automation, and each rebuild raises the Flow or Apex question. SF Ben's 2026 Salesforce Admin Survey found that almost 43% of respondents work in admin-only teams. On those teams, the person who picks the tool is the person who maintains it for years.
If you build with an AI client, the same decision applies. The Cirra AI Salesforce Admin MCP Server can create flows and Apex in your org from Claude, ChatGPT, Cursor and other MCP clients. It builds whichever one you ask for, so the judgment about which to ask for stays with you.
What Flow and Apex each do
Flow is Salesforce's declarative automation tool. You build it on a canvas from elements such as Get Records, Decision, Loop, Assignment, Create Records, Update Records and Action. The main flow types are record-triggered flows (which run before or after a record is saved), screen flows, schedule-triggered flows and autolaunched flows that other automation calls.
Apex is Salesforce's programming language. It shows up as triggers that fire on record changes, classes that hold reusable logic, and asynchronous jobs such as Batch Apex, Queueable Apex and Scheduled Apex. A trigger and a record-triggered flow can react to the same save, which is why the two tools overlap so often.
Choosing between them is the declarative vs code decision every Salesforce team makes. Both tools run on the same multi-tenant platform and answer to the same per-transaction limits, and each fits a different shape of problem.
Six questions that decide when to use Apex vs Flow
Ask these in order. The first question with a firm answer settles it.
1. Can the standard elements do it? Write the requirement as plain steps. If every step maps to a Flow element, such as finding records, branching on a condition, changing fields, creating a record or sending a notification, build a flow. Salesforce's own guidance in Planning for Flow Success is to start with the simplest tool that meets the requirement. The same article names a formula field as the better choice over a flow when you only need to show cross-object data. A validation rule is the simpler choice when you only need to block a bad save.
2. How many records move at once? A data load, an integration or a scheduled job can save many records in one transaction. Record-triggered flows handle that volume when they are built well. The trouble starts when a Get Records or Update Records element sits inside a loop, because each pass issues another query or DML statement. If the requirement means processing very large sets of records, Apex gives you tools built for it, including Batch Apex and Queueable Apex.
3. Who will maintain it in a year? An admin can open a flow and follow it. An Apex class needs someone who reads code, writes tests and deploys. Salesforce requires at least 75% of your Apex to be covered by unit tests before you deploy to production. If your team has no developer, treat every class as a commitment that someone has to review, test and own.
4. Does it need transaction control or a data structure that Flow handles awkwardly? Apex has Maps and Sets for keyed lookups, and Database.insert(records, false) lets some records succeed when others fail. A flow handles the same jobs with loops and fault paths, which works until the canvas grows past what one person can hold in their head.
5. Does it call another system? A simple request to one endpoint can live in a flow. Multi-step handshakes, retries, signature checks and response parsing belong in Apex, where you can unit test each piece.
6. What else runs on the object? Salesforce Admins guidance warns that mixing Apex triggers, record-triggered flows and legacy Process Builder on the same object invites execution order problems and technical debt. Before you add anything, list what already runs on the object and in what order. Our article on why Salesforce flow debugging is so hard explains what makes that audit slow when an org has years of layered automation.
Governor limits that shape the decision
Limits apply to the transaction. A save that fires a before-save flow, an Apex trigger and an after-save flow spends from a single budget. These are the per-transaction Apex limits in the Apex Developer Guide for Winter '27 (API version 68.0):
| Limit | Synchronous | Asynchronous |
|---|---|---|
| SOQL queries issued | 100 | 200 |
| Records retrieved by SOQL | 50,000 | 50,000 |
| DML statements issued | 150 | 150 |
| Records processed by DML | 10,000 | 10,000 |
| Maximum CPU time | 10,000 ms | 60,000 ms |
Salesforce Ben's guide to Flow limits lists the same per-transaction numbers for flows: 100 queries, 150 DML statements, 50,000 records retrieved, 10,000 records processed and 10,000 ms of CPU time.
Both tools spend from the same budget. In Apex you can read the code and count the queries. In a flow you trace every element, and loops multiply whatever sits inside them. Asynchronous Apex gets a higher ceiling for queries and CPU time, which is where long-running or heavy work moves.
A worked example where Flow is the right choice
Requirement: When an Opportunity moves to Closed Won, create a follow-up task for the Opportunity owner due 30 days after the close date.
Every step maps to a standard element, volume is low, and an admin can maintain it. Build a record-triggered flow with these settings:
Flow type: Record-Triggered Flow
Object: Opportunity
Trigger: A record is created or updated
Entry condition: Stage Equals Closed Won
Run condition: Only when a record is updated to meet the condition requirements
Optimize for: Actions and Related Records (after save)
Formula resource (Date): DueDate
{!$Record.CloseDate} + 30
Create Records element: Task
Subject: Follow up on closed deal
OwnerId: {!$Record.OwnerId}
WhatId: {!$Record.Id}
ActivityDate: {!DueDate}
An after-save flow is the right setting here because the flow creates a record other than the one that triggered it. Before-save flows are meant for changing fields on the triggering record. The "only when a record is updated to meet the condition requirements" setting keeps the task from being created again every time someone edits a closed deal.
A worked example where Apex is the right choice
Requirement: A record-triggered flow on Opportunity needs the largest open Opportunity amount for each Account touched by the save, so it can set a priority field.
For a single Account, a flow can answer this without code: a Get Records element filters open Opportunities by the triggering record's Account, sorts them by Amount in descending order and stores only the first record. The example here goes one step further. A reusable invocable action takes a list of Accounts and returns the largest open amount for each one, so several flows can call the same logic. In Apex, one aggregate query answers for every Account in the list at once, and the method returns one result per request. The flow stays the entry point and calls this invocable method through an Action element:
public with sharing class LargestOpenOpportunity {
public class Request {
@InvocableVariable(required=true)
public Id accountId;
}
public class Result {
@InvocableVariable
public Id accountId;
@InvocableVariable
public Decimal largestOpenAmount;
}
@InvocableMethod(
label='Largest open opportunity by account'
description='Returns the largest open Opportunity amount for each Account'
)
public static List<Result> run(List<Request> requests) {
Set<Id> accountIds = new Set<Id>();
for (Request r : requests) {
accountIds.add(r.accountId);
}
Map<Id, Decimal> maxByAccount = new Map<Id, Decimal>();
for (AggregateResult ar : [
SELECT AccountId a, MAX(Amount) m
FROM Opportunity
WHERE IsClosed = false AND AccountId IN :accountIds
WITH USER_MODE
GROUP BY AccountId
]) {
maxByAccount.put((Id) ar.get('a'), (Decimal) ar.get('m'));
}
List<Result> results = new List<Result>();
for (Request r : requests) {
Result res = new Result();
res.accountId = r.accountId;
res.largestOpenAmount = maxByAccount.get(r.accountId);
results.add(res);
}
return results;
}
}
Three details in this class come straight from Salesforce's documentation for the InvocableMethod annotation. The method takes a single list parameter. The returned list matches the input list in size and order, so the i-th result belongs to the i-th request. And the running user needs access to the Apex class through a profile or permission set, or the flow cannot call it.
The query runs once, outside any loop, so it uses one SOQL query however many Accounts a save touches. One aggregate query does not guarantee the class stays inside every limit: the Opportunity records it processes can still count toward the 50,000-record limit on records retrieved by SOQL queries, and other automation in the same transaction spends from the same budget. Filter the query tightly and test it with realistic data volumes. The WITH USER_MODE clause runs the query with the running user's object permissions, field-level security and sharing, which the with sharing keyword alone does not cover. You still need a test class with real assertions before this can reach production.
Combining the two: Flow as the front door
The most maintainable setups use Flow as the entry point and Apex for the one step that needs code. The flow holds the trigger, the entry conditions and the business logic an admin should be able to read. The Apex class holds the piece that is easier to test as code.
Four habits keep that arrangement healthy:
- Keep one automation entry point per object and event. If a flow already handles Opportunity updates, call Apex from it instead of adding a separate Apex trigger.
- Give the Apex action a clear label and description, because those are what the admin sees in Flow Builder.
- Make the Apex method bulk-safe by accepting a list and returning a list, because a record-triggered flow can call it for many records at once.
- Wire a fault path from the Action element, so an exception in the class produces a message you can act on instead of a silent failure.
When to move a flow to Apex, and when to go the other way
Several signs suggest a flow has outgrown the canvas:
- Fault emails that mention too many SOQL queries or too many DML statements.
- A Get Records or Update Records element inside a loop that you cannot remove.
- Nested loops that nobody wants to edit.
- Logic you need to call from several places and test on its own.
The reverse move happens too. An Apex trigger that only sets fields on the record being saved, written years ago by a developer who has since left, is a candidate for a before-save flow that your admin team can read. Test the replacement in a sandbox against the same records before you switch the trigger off.
Legacy Workflow Rules and Process Builder processes still run today. Salesforce recommends moving them to Flow, so treat that as the default home for them and reach for Apex only where the six questions above point there.
How Cirra AI helps with either choice
Once you know which tool fits, building it is still work. Cirra AI connects your AI client to your org through OAuth, and from there you can ask in plain English. For flows, Cirra AI can create record-triggered, screen, scheduled and autolaunched flows and edit the logic of existing ones. For code, it can generate Apex classes and triggers, including invocable methods for use in Flow, and review existing Apex for issues. A prompt like this is a good start:
Review the Apex triggers and record-triggered flows on Opportunity.
For each one, tell me what it does, which fields it writes, and whether
it could be a flow or should stay as Apex.
Cirra AI runs as the connected user and cannot go beyond that user's Salesforce permissions. It documents each change it makes, and you can set per-org guardrails on the kinds of data and metadata it may touch. Our buyer's checklist for AI that changes your org covers what to ask any vendor about that.
Make changes in a sandbox first, run the Apex tests and a few flow debug runs there, then deploy to production through your usual process. The code and flow settings in this article follow Salesforce's documented syntax. Test them in a sandbox before you rely on them.
Cirra AI supports Salesforce Enterprise Edition and up, and the Free plan includes 100 credits a month so you can try it on a real org. Ready to see it on yours? Choose Start Free Trial, or Book a Demo to walk through your own automation with us.
Frequently asked questions
Should I use Apex or Flow?
Start with Flow. Choose Apex when the requirement needs complex data handling, tight transaction control, very large record volumes or multi-step integrations that the standard Flow elements cannot express cleanly. When a single automation needs both, build it as a flow and call an invocable Apex method from an Action element.
Is Flow faster than Apex?
Neither is faster by definition. Flow and Apex share the same per-transaction governor limits, so efficient design matters more than the tool. A flow that queries once and updates a collection uses far fewer queries than Apex that queries inside a loop.
Can a Flow call Apex?
Yes. A flow calls Apex through an Action element. The class exposes a method with the InvocableMethod annotation, takes a list as its single input parameter and returns a list in the same order. The user running the flow needs access to the Apex class through a profile or permission set.
Can Flow replace Apex triggers?
For many requirements it can. Before-save record-triggered flows cover field updates on the triggering record, and after-save flows cover related records, notifications and actions. Apex triggers remain the better fit for complex logic, heavy volume and anything you need to unit test as code.
Can I use Apex triggers and record-triggered flows on the same object?
You can, but Salesforce Admins guidance warns that mixing them invites execution order problems and technical debt. If you do mix them, document the order they run in and keep one entry point for each event.
Do Workflow Rules and Process Builder still work?
Existing Workflow Rules and Process Builder processes keep running. Salesforce ended support for them on December 31, 2025, which means no customer support or bug fixes, and it recommends migrating them to Flow.
Does Apex need test coverage?
Yes. Salesforce requires at least 75% of your Apex to be covered by unit tests before you can deploy it to production. Write tests with real assertions, because coverage alone does not show that the code works.


