When Apex and when Flow?
Most organisations never make this choice deliberately. A few flows get added, later a trigger, and three years on nobody dares remove anything. This article gives you the order in which the choice should be made.
The question almost always comes too late
Apex or Flow is rarely the first question. Something has usually been built already, the org already behaves a certain way, and the question only surfaces once something breaks or slows down. The conversation then turns to the tool, when it should be about the order.
That order is: first check whether you need to build anything at all, then whether configuration covers it, then Flow, and only after that Apex. Every step down costs you maintenance, in-house knowledge and a release process. This is not a matter of principle but of arithmetic. Whatever an admin can change themselves tomorrow, you do not have to schedule.
Start below Flow, not inside it
Before you open Flow Builder, the question is whether standard functionality already solves it. Plenty of orgs run a record-triggered flow that does exactly what a formula field would have done for free, including the storage, the recalculation and the error email that comes with it.
Frequently skipped
- βFormula fields: always current, no storage, no automation that can fail.
- βRoll-up summary fields on master-detail: counting and totalling without a line of logic.
- βValidation rules: stop bad data instead of repairing it afterwards.
- βApproval processes: approval with steps, delegation and history, out of the box.
- βCustom Metadata: values that differ per environment or period, so they are not welded into your logic.
Only when none of these covers it does the choice between Flow and Apex really begin.
What Flow can actually do now
The image of Flow as the simple sibling of code is a few years out of date. Workflow Rules and Process Builder have been retired, so everything declarative now runs through Flow. Two things determine what a flow costs you.
Before-save or after-save
A before-save record-triggered flow changes fields on the record that is being written anyway. No extra DML, no second save. An after-save flow setting the same field performs a full update on top of that and kicks off the save cycle again. That difference is orders of magnitude, not percentages. It is also the most common mistake in orgs complaining that Flow is slow.
Flow is bulkified after all
The most persistent misconception is that Flow cannot handle volume. Get Records and Update Records work on collections: two hundred records in one transaction produce one query and one DML. What does fall over is a Get or Update inside a loop. That is not a shortcoming of Flow, it is the same mistake that is just as fatal in Apex. It is simply easier to miss in Flow Builder.
What remains after that is a short list of real limits. And it is a good deal more specific than "complex logic".
Where Flow hits a wall
Five limits no design gets around. Hit one of them and the choice is made for you.
- 1
Error handling
If a record-triggered flow fails, the whole transaction rolls back and the flow owner receives an email with a stack trace. The user sees a message they can do nothing with. You cannot let one record fail and let the rest through, and you cannot catch an error cleanly and carry on.
- 2
Transaction control
No savepoints, no partial commits, no deliberate choice to process five out of ten records. In Apex you arrange that with a savepoint and Database.update with partial success. In Flow it is all or nothing.
- 3
External integrations
A callout cannot run from a before-save flow, nor after a DML in the same transaction without an asynchronous detour. If you want to parse a response, handle a timeout or retry, you end up in Apex either way.
- 4
Volume beyond one transaction
As soon as you have to process more than fits in one transaction, you need Batch or Queueable Apex. Scheduled flows process records in batches too, but you have no control over chunk size, no restart and no usable error log.
- 5
Testing and collaboration
Apex enforces test coverage before you can deploy to production. Flow Tests exist, but only for record-triggered flows and without the assertion power of an Apex test. On top of that, a flow is XML you cannot merge sensibly: two people working on the same flow simply overwrite each other.
And what Apex costs you
This side usually gets less attention in comparisons like this, while it is exactly what determines whether code is the cheaper option.
- Every change goes through a deployment. Adjusting one sentence in an email then takes a release rather than two minutes.
- At least 75 percent test coverage across the whole org before you can deploy. Someone writes those tests, and someone maintains them.
- The admin who understands the process best can no longer change anything about it. Every adjustment goes through a developer.
- If the builder leaves, someone else has to be able to read the code. A flow can at least still be followed by someone who is not a developer.
So the honest rule of thumb is not that code is better. Choose code where it gives you something Flow cannot do, not because it feels tidier.
Governor limits apply to both
Flow and Apex share the same limits within a single transaction. A flow that hands off to Apex does not get a fresh budget. Once you know these numbers, you can see from a design where it is going to pinch.
| Limit | Per transaction | Where it goes wrong |
|---|---|---|
| SOQL queries | 100 | Every Get Records counts, including those in a subflow or in a trigger your flow fires |
| DML statements | 150 | Every Create, Update and Delete, including the saves your own automation causes |
| Records per DML | 10,000 | Applies across the whole transaction, not per element |
| CPU time | 10 sec. synchronous, 60 sec. asynchronous | Flow elements are generally more CPU-expensive than the same logic in Apex |
| Callouts | 100, 120 sec. combined | Not allowed after a DML in the same transaction without an asynchronous step |
| Heap | 6 MB synchronous, 12 MB asynchronous | A large collection in a flow counts just as heavily as in Apex |
The practical conclusion: if your design comes anywhere near these numbers, the question is no longer Flow or Apex, but synchronous or asynchronous.
The decision tree
Five questions, in this order. Stop at the first one you answer yes to.
- 1
Can it be done without automation?
Formula field, roll-up summary, validation rule or approval process. If so: build nothing. That is the cheapest automation there is.
- 2
Does a user need to step through it?
Screen Flow. If there is heavy or branching calculation involved, have the flow call an invocable Apex class and keep the screen itself empty of logic.
- 3
Are you only setting fields on the record that was just saved?
Before-save record-triggered flow. No trigger, no after-save flow, no extra DML.
- 4
Do you need a callout, transaction control, processing across multiple transactions or per-record error handling?
Apex. Invocable, Queueable or Batch, depending on the volume and when it has to run.
- 5
Still no on everything?
Then it is an after-save record-triggered flow. That is the default choice, not the fallback.
Screen (Start)
User enters data
Get Records
Retrieve data for the flow
Apex Action
Run the heavier logic
Screen (Result)
Shows the outcome to the user
Update Records
Write the data back
Screen (Start)
User enters data
Get Records
Retrieve data for the flow
Apex Action
Run the heavier logic
Screen (Result)
Shows the outcome to the user
Update Records
Write the data back
Three real-world situations
The same question, three different outcomes.
Welcome process for a new customer
- Situation
- A service provider wants a welcome email to go out automatically when a new customer is created, and the account manager to get a task after seven days to follow up by phone.
- Choice
- After-save record-triggered flow on Account, with a scheduled path for the task.
- Why
- Other records are being created, so before-save is out. The logic is a single decision, and the email text changes a few times a year. You want the marketing manager doing that, not a release.
- How this goes wrong
- The same logic in an Apex trigger, after which every text change takes three weeks.
Quote with a calculation model
- Situation
- An installation company has its back office fill in a number of values and calculates the expected yield and price from them. The model consists of tiers, correction factors and exceptions, and changes every season.
- Choice
- Screen Flow for the input, invocable Apex class for the calculation, tiers in Custom Metadata.
- Why
- The flow is where the admin adjusts screens and fields. The calculation does not belong in decision elements and formula variables: it is too branched to keep following and too important to leave untested. In an Apex class it is readable, version-controlled and covered by real test cases. The tiers themselves belong in Custom Metadata, so a rate change does not cost a deployment.
- How this goes wrong
- The calculation model inside the flow itself. Two seasons on you have a forty-element flow nobody dares touch.
Nightly synchronisation with the ERP
- Situation
- A wholesaler receives thousands of stock and status updates from an external system every night. Records that cannot be matched have to be logged, not silently skipped.
- Choice
- Batch Apex from a scheduled job, with a custom object as the error log.
- Why
- The volume does not fit in one transaction, there is an integration with an external system, and the requirement that one bad record must not block the rest is precisely what Flow cannot do. Database.update with partial success plus a log record per failure gives the admin a workable list in the morning instead of a mailbox full of system messages.
- How this goes wrong
- A scheduled flow might just about manage a thousand records, but falls over as soon as the volume grows. And then without a usable trace of what actually went wrong.
The two orgs you do not want to end up in
Everything in Flow
Eleven record-triggered flows on Account, all after-save, in an order nobody can predict. A field update in one flow triggers another. The org hits the CPU limit on an import of three hundred records. Nobody removes anything, because it is unclear what depends on what.
Everything in Apex
Every automation sits in triggers and handler classes. Tidy on paper. In practice the admin queues up at a developer for every text change and the lead time on a small adjustment is three weeks. Meanwhile the business builds its own spreadsheet alongside it.
Both orgs have the same problem: a tool was chosen once, and after that nothing was chosen again.
If you have several automations on the same object, it pays to know the order in which Salesforce executes them. Read how the order of execution works.
In short
| Situation | Choose |
|---|---|
| Field update on the record that was just saved | Before-save record-triggered flow |
| Follow-up actions: task, email, creating a related record | After-save record-triggered flow |
| User has to step through something | Screen Flow |
| Heavy or branching calculation in a screen process | Screen Flow with invocable Apex |
| Integration with an external system | Apex, asynchronous |
| More records than fit in one transaction | Batch or Queueable Apex |
| Logging errors per record without blocking the rest | Apex |
| Values that differ per environment or period | Custom Metadata, not hardcoded |
Frequently asked questions
Per element yes, but that is rarely what breaks. A before-save flow setting one field is faster than an after-save trigger doing the same with an extra DML attached. Performance problems almost always come from the design: queries in a loop, stacks of flows on the same object, or after-save where before-save would have done.
Within one transaction yes, because the elements work on collections. Where it goes wrong is a Get or Update inside a loop, and volumes that do not fit in one transaction. For the latter you need Batch Apex.
Yes, Salesforce has retired both and supplies a migration tool. Just do not treat it as a one-to-one conversion. It is the moment to look at what is still needed, what can move to a before-save flow and what can simply go.
There is no hard technical limit, but in practice keep one per object per timing: one before-save and one after-save, with the logic ordered inside them. Since Spring '22 you can enforce an order with flow trigger order, but that is a plaster. It does not solve having your logic spread across multiple flows.
Yes. An Apex method with the @InvocableMethod annotation appears as an action in Flow Builder. That is the hybrid form and usually the best outcome: the admin keeps the flow, the developer keeps the logic.
Gerelateerde artikelen
Rather have the choice reviewed?
Do you know whether your automation is still heading the right way?
Book a Quick Scan: thirty minutes over screenshare, walking through your org together. How your automation is holding up, where it becomes hard to manage and which choices will catch up with you later. A conversation, not a report.
