Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

The answer, briefly: put same-record field logic in a before-save record-triggered flow, and write Apex the moment the requirement involves real branching, recursion control, callout orchestration, logic you need to reuse, or anything you need to unit test. On a busy object, one Apex trigger with a handler class ages better than four flows. Choosing Apex costs you the ability for an admin to change it without a deployment. Choosing Flow costs you tests, reviewable diffs, and control over execution order.

Flow or Apex triggerA decision tree from what the automation has to do, not from who is building it.Does the automation need something Flow cannot express?No, and it only touches thetriggering recordBefore-save record-triggeredflow. No extra DML, fastestoption, and an admin canmaintain it.No, but it touches relatedrecordsAfter-save record-triggeredflow. Costs a second save,and can create, update anddelete other records.Yes — callout chaining,recursion control, complexdata structures, customerrors per recordApex trigger with a handlerclass. Version-controlled,unit-tested, and able toguard against re-entry.One more constraint: whatever you choose, one automation per object per type. Five flows on one object isthe problem this decision is meant to prevent.
Decide from what the automation has to do. The answer follows from the requirement, not the builder.

The rule I actually apply

Use a before-save record-triggered flow when the whole requirement is “set fields on the record being saved, from values on that record or its lookups” — stamping a region from a billing state, deriving a status, copying a value forward. No extra DML, no second save, and any admin can maintain it.

Use Apex when any one of these is true, not all of them, any one:

  1. The logic branches more than about three levels deep, or the same condition is evaluated in several places.
  2. You need recursion control you can reason about — a static flag, a set of already-processed IDs.
  3. You are making callouts, especially more than one, or need retry and timeout behaviour.
  4. The same logic has to run from a trigger, a screen, and a batch job.
  5. You need a test that fails in CI when someone breaks the rule.
  6. The object takes bulk loads and CPU time is already tight.

Everything else is a judgement call, and the deciding factor is who maintains it in two years.

Comparison

Criterion Record-triggered Flow Apex trigger
Maintainability Fine while small. Falls apart once it has a dozen decisions and nested loops — the canvas hides complexity rather than exposing it. Fine with a handler class and an owner. Bad if it is 800 lines in the trigger body.
Who can change it Any admin with Manage Flow. Editable directly in production. Developer only. Cannot be edited in production; needs a deployment and passing tests.
Testability No native unit tests. Debug an interview in Flow Builder with rollback, or exercise the flow indirectly from an Apex test that performs DML. Nothing enforces coverage. Unit tests with assertions, bulk tests at 200 records, negative tests. Coverage enforced at deployment.
Performance Higher CPU per unit of logic — every element carries interview overhead, one interview per record. Before-save avoids the extra save, which is the big win. Lower CPU for the same work. One query, one map, one pass over the collection.
Error handling Fault paths per element, plus the Custom Error element to block a save with a message on a field. An unhandled fault rolls back the transaction and emails an admin. try/catch, custom exception types, addError() on the record or field, partial-success DML via Database.insert(records, false).
Version control XML metadata. Deployable, but the diff is unreviewable in a pull request — element order churns and intent is invisible. Text. Reviewable line by line. Blame works.
Bulk behaviour Bulkified across interviews for elements in the same position, but a Get Records or Update Records inside a loop runs per iteration, the usual cause of a limit exception. Bulk-safe if you write it that way, and you can prove it with a 200-record test.
Complexity ceiling Low. No maps, no methods, no interfaces, no shared state. Reuse means subflows or an Apex action. High enough that the constraint becomes your own discipline.

Where Flow genuinely runs out

Branching that stops fitting on the canvas

A Decision element with four outcomes is fine. Three of those nested, with an Assignment on each branch and a Get Records in two of them, is a diagram nobody can hold in their head, and there is no way to pull the repeated condition out into something named. In Apex that is a private method whose name says what it checks. The complexity does not go away. It goes somewhere readable.

Recursion control

Flow prevents a flow trigger from re-firing itself on the same record, and that is all you get. Flow variables do not survive between interviews, so there is no equivalent of a static Set<Id> of records you have already handled. When your automation legitimately needs to update a related record that updates back, Apex is the only place you can hold that state:

public class AccountRollupGuard {
    private static Set<Id> processed = new Set<Id>();

    public static Boolean claim(Id recordId) {
        return processed.add(recordId);
    }
}

Callout orchestration

Flow can make an HTTP callout, and for one simple GET that is reasonable. What you do not get is control over what happens when the second callout fails after the first succeeded, or a way to manage the 120-second cumulative callout timeout per transaction with its 10-second default per call. You also still cannot call out after DML in the same transaction — the fix is an asynchronous path, which means giving up the guarantee that the caller sees the result. In Apex you can sequence a Queueable chain and decide what a partial failure means.

Logic that has more than one caller

The instant a rule has to run from a save, a screen, and a nightly job, Flow makes you either duplicate it or wrap it in a subflow with an awkward input contract. An Apex static method takes one argument and is called from all three. Duplicated logic in three flows is the defect that surfaces as “it works from the UI but not from the integration.”

Anything you need to prove still works

This is the one that decides most arguments. If the rule is load-bearing (revenue recognition, entitlement, anything an auditor asks about) you want a test that fails when someone edits the wrong thing. Flow gives you no such test, and an active flow can be changed in production by anyone with the permission, with no review.

What each choice costs you

Choosing Flow costs you tests, reviewable history, and ordering control. Trigger order values only sort flows against other flows on the same object with the same timing. They do nothing against Apex. You are also accepting higher CPU consumption, which matters on objects that get 200-record batches from an integration.

Choosing Apex costs you admin autonomy. Every change needs a developer, a sandbox, and a deployment, and the business feels that as slowness. It also risks nobody left on the team being able to read it. An Apex trigger written by a contractor who left is worse than a flow the admin understands, and I would take the flow in that situation.

The case where the obvious answer is wrong

The reverse is more damaging. “It is only three field updates, therefore Flow” is wrong when the object already has an Apex trigger doing related work. Adding a flow means two mechanisms with interleaved timing on one object, and you have just created the class of bug that passes every isolated test. Put the three field updates in the trigger you already have.

Answer three questions

The same decision as the diagram above, one question at a time. Nothing is sent anywhere — this runs in your browser.

Does the automation only touch the record being saved?

Frequently asked

Is Flow slower than Apex?

For equivalent logic, yes: the interview runtime costs CPU per element, and flows count against the same 10,000 ms synchronous CPU budget as everything else in the transaction. The exception matters though: a before-save flow beats an after-save flow or a trigger that issues its own update, because it avoids a whole additional save.

Should I convert existing flows to Apex?

Only where one of the six conditions above applies. Conversion is a rewrite with no test suite to prove equivalence, which is exactly the risk you are trying to reduce. Convert the flow that keeps breaking, not the flow that works.

Can I use both on the same object?

You can, and the platform documents the sequence, so it is not undefined. It is still the main source of order-dependent bugs, because each piece tests cleanly alone. Keep one mechanism per timing: flows for before-save, Apex for after-save, or the reverse, never both in the same slot.

What about invocable Apex called from Flow?

A good middle path. Keep the orchestration and the entry conditions in the flow where an admin can see them, and put the part that needs tests behind an @InvocableMethod. The tradeoff is that the flow now fails in a place the admin cannot debug, so the Apex needs to return errors the flow can present.


Reference: Triggers and Order of Execution, Apex Developer Guide, and Flow, Metadata API Developer Guide, Salesforce. Governor limits per Apex Developer Guide, “Execution Governors and Limits.”