Flow or Apex? Write the decision down before you build
The automation argument is never really about Flow or Apex. It is about who maintains the thing in eighteen months, and whether anyone can reconstruct why it exists.
Platform & ArchitectureEvery org we inherit has the same archaeological layer: a stretch of automation nobody will touch. Three record-triggered flows and a trigger all fire on Opportunity. Two of them update the same field. One was built by a partner who left in 2022. Nobody can say which one owns the value, so the team works around all three.
That is not a Flow problem. It is not an Apex problem either. It is what happens when a build decision gets made in a stand-up and never written down.
The question people argue about
The public version of this argument is about capability. Flow is declarative, so admins can maintain it. Apex is code, so it handles complexity. Both statements are true and neither helps at 4pm on a Thursday when you have to decide where the discount approval logic goes.
The capability line has also moved. Before-save record-triggered flows update fields on the same save without a second DML operation, which removed the old "Apex is faster" reflex for simple field derivation. Flow handles collections, loops, subflows, callouts via invocable actions, and fault paths. The set of things that only Apex can do has shrunk considerably.
So capability alone stopped being a useful tiebreaker. What did not change is everything downstream of the build.
The question that actually decides it
Ask this instead: who is on the hook when this breaks at month-end, and what will they be looking at?
That single question sorts most cases within a minute.
- If the answer is an admin, in the org, during business hours, build it declaratively and keep it legible. An admin debugging Apex they cannot deploy to is not a support model.
- If the answer is an engineer, from a repo, with tests, write Apex. Do not hand someone a 40-element flow as their production surface area and call it low-code.
Notice that neither answer is about elegance. It is about who holds the pager.
Where automation actually fails
In the incidents we get called into, the root cause is almost never the choice of tool. It is one of four things, and both tools are perfectly capable of getting all four wrong.
1. Bulk behaviour
A data load, a mass update from an integration, or a Data Loader run pushes 200 records into a transaction that was only ever tested with one. In Apex this is the classic query-inside-a-loop. In Flow it is a Get Records or an action inside a loop element, which is exactly the same mistake wearing a different hat.
The synchronous limits are unforgiving and shared across the whole transaction: 100 SOQL queries, 150 DML statements, 50,000 records returned, 10 seconds of CPU time. Your automation does not get its own budget. It shares one with every managed package, trigger and flow that runs on the same save.
2. Error handling
Apex without a considered exception path throws and rolls back the transaction, sometimes in front of a user who has no idea what happened. Flow without a fault connector produces an unhandled-fault email to the last modifier of the flow, and a user-facing error that reads like a stack trace.
Neither is a strategy. Decide, at build time, what happens when the downstream call fails: does the record still save, does someone get a task, does it land in a retry queue, does the user see something honest?
3. Recursion and order
The order of execution is one document that every person building automation on the platform should have read at least once. Multiple record-triggered flows on the same object and timing do not run in a guaranteed order unless you set trigger order values, and even then you are declaring a dependency you now have to maintain.
The rule that has survived every org we have cleaned up is boring: one record-triggered flow per object, per timing. Before-save for field derivation on the same record. After-save for related records, callouts and notifications. Branch inside it. When it gets too big to hold in your head, that is the signal to move logic into an invocable Apex action, not to add a fourth flow.
4. Nobody knows why it exists
This is the expensive one. Logic gets built for a reason, a compliance requirement, a finance rule, one director's request in 2023. The reason lives in someone's head, that person leaves, and the logic becomes load-bearing folklore. It cannot be removed because nobody can prove it is safe to remove.
The decision record
The fix is four lines, written when the decision is made, in a place the next person will find. We put it in the flow description field, the Apex class header, and the ticket.
WHY: Finance needs the discount band recalculated before approval routing.
Source: FIN-224, approved by [role], 2026-06-02.
OWNER: Revenue Ops admin team. Escalation: platform engineering.
CHOICE: Before-save flow. No callout, no cross-object write, admin-maintained.
RISK: Shares the Opportunity before-save path with the territory assignment
step. Any new before-save logic goes in that same flow, not a new one.Four lines. It takes about ninety seconds. It is the difference between a future engineer deleting dead logic with confidence and a future engineer routing around it forever.
A working rule of thumb
If you want something shorter than a framework, this is what we give teams on day one.
| Situation | Build it as |
|---|---|
| Set fields on the record being saved | Before-save record-triggered flow |
| Create or update related records, send notifications | After-save record-triggered flow |
| Multi-step business process with human steps | Flow, orchestrated, with fault paths |
| Complex branching finance or pricing logic | Apex service, called from Flow |
| Anything needing real unit tests and version control | Apex |
| High-volume async processing | Queueable or Batch Apex |
| Integration callout with retry and idempotency | Apex, exposed as an invocable action |
The pattern underneath it: Flow orchestrates, Apex calculates. Flow is where the business process lives and where an admin can see the shape of it. Apex is where the logic that needs tests, error handling and a code review lives. The invocable action is the seam between them, and it is a good seam, it keeps the process visible without pretending that a pricing engine belongs in a decision element.
What we would change first in your org
If you inherited an org and had one afternoon, spend it here:
- List every record-triggered flow and trigger by object and timing. Anywhere you find more than one at the same timing, you have an ordering dependency you did not choose.
- Find every Get Records or action element inside a loop. Those are your next bulk incident.
- Find every flow with no fault path on a callout or DML element.
- Pick the three pieces of automation nobody can explain and write the WHY line for them, or get agreement to retire them.
None of that requires a rebuild, a budget, or a migration. It requires an afternoon and someone willing to write things down.
The tooling argument will keep moving, it has moved twice in the last five years and it will move again. The maintenance argument does not move. Build for the person who has to understand this without you in the room.



