# Flow or Apex: the criteria applied to ten real requirements

> Ten requirements taken from real backlogs, each walked to a verdict. In several the obvious answer is wrong, and in two the honest answer is that neither tool should be involved at all.

- Source: https://synconai.com/insights/flow-vs-apex-when-to-use-each
- Publisher: SynconAI (https://synconai.com)
- Desk: Platform & Architecture
- Author: SynconAI Architecture Team, Solution & technical architecture
- Published: 18 May 2026
- Updated: 29 August 2026
- Reading time: 10 minutes
- Topics: Flow, Apex, Automation, Architecture, Platform

## Key points

- Complexity is a poor predictor. Enumerability, transactional exactness and the identity of the maintainer decide almost every case.
- Several requirements that arrive as automation requests are not automation at all: a validation rule, a formula field or a different relationship answers them better.
- A routing matrix with dozens of branches still belongs in declarative automation, provided the rows live in custom metadata rather than in decision elements.
- Volume changes the verdict without the requirement changing, which is why the row count assumed at build time belongs in writing.

---
Ten requirements, each taken from a real backlog, each walked to a verdict. The interesting part is not the verdicts. It is that in several cases the obvious answer is wrong, and that the property doing the deciding is almost never how difficult the thing looks.

That the choice should be recorded as a standard before anyone opens a canvas we have argued in [Flow or Apex? Write the decision down before you build](/insights/flow-or-apex-write-the-decision-down). This is the other half of it: what the criteria actually produce when you point them at work somebody is waiting on.

## The three properties that decide it

Difficulty is a bad predictor. Some of the most branch-heavy requirements on this list are safely declarative, and some of the smallest belong in code. Three properties do the real work.

**Can the requirement be fully enumerated?** If the business can state every input and every branch, and the list is finite and stable, declarative automation holds it comfortably. If the rule is a judgement nobody can write down as a list, no number of decision elements will produce it, and building one produces something that runs without anyone being able to say what it does.

**Must it be transactionally exact?** Some requirements tolerate being right a moment later. Others must be right at the instant of commit, under concurrent saves from users, an integration and a data load at once. Exactness under concurrency is where declarative automation gets thin, because the guarantee you need belongs to the platform rather than to the tool.

**Who maintains it?** An admin working in the org, or an engineer working from a repository with tests and a deployment pipeline. This is the tiebreaker, not the opener.

Two things are the same either way. Both tools spend the same shared per-transaction budget for queries, record changes and processing time, and that budget is communal across every trigger, flow and managed package on the save rather than allocated per automation. Those values also move between releases, so check current [Salesforce developer documentation](https://developer.salesforce.com/) rather than a figure somebody remembers from a certification.

## Where the requirement is not automation at all

### 1. "Nobody should be able to save an opportunity without a primary contact."

Looks like automation, and usually arrives as a flow request because it starts with a condition.

The deciding property is that nothing happens. It is an assertion about one record at the moment of save, expressible as a single expression, and it never writes anything.

**Verdict: a validation rule.** Building it as a flow adds an artefact to the object's save path in order to enforce something the platform enforces natively, and gives the user a worse error message on the way. Not every requirement containing the word "when" is automation.

### 2. "Service needs the account's industry on the case so they can filter by it."

Looks like a field copy, so a flow with two elements.

The deciding property is that the value already exists. Nothing needs to happen, something needs to be visible in a second place.

**Verdict: neither.** A cross-object formula field, or a report built on the related object. Copying the value creates a synchronisation problem that did not previously exist: the copy is correct at creation and wrong from the first time somebody edits the account. A large share of the automation we delete in inherited orgs is exactly this, built once, never reconciled, quietly feeding wrong numbers into a dashboard.

## Where declarative wins despite looking hard

### 3. "When the opportunity closes, the contract should start on the first of the next month."

Looks like a small piece of code, because it involves a date calculation and developers do arithmetic.

The deciding property is that the rule is fully enumerable and it writes a field on the record being saved.

**Verdict: a before-save record-triggered flow.** The reason to resist Apex is not effort. It is that the person who will eventually change *first of the next month* to *first of the next quarter* is the revenue operations admin, and that change should not require a release.

### 4. "Route the case to the right queue based on product, region and entitlement."

Looks like Apex, because the matrix runs to dozens of combinations and dozens of combinations feel like code.

The deciding property is enumerability. The service manager can state every row. They already have, in a spreadsheet, and they will restate it every time a product launches.

**Verdict: declarative, with the matrix held as custom metadata rather than as branches.** Branch count measures size, not kind. The failure mode here is not choosing Flow, it is encoding forty outcomes as forty decision paths: unreadable, and every routing change becomes a deployment. Rows in custom metadata make the table data the service manager owns, and the flow stays a lookup with one path through it.

### 5. "Tell the account team when one of their cases is escalated."

Looks like a flow, and is one.

The deciding property is the maintainer. Nothing here is exact, everything is enumerable, and the person who will change the recipient list is an admin.

**Verdict: an after-save record-triggered flow.** It earns a place on the list because the counter-argument arrives anyway: we might send it to Slack later, so build it in Apex now. Later is not a property of the requirement. Build the flow, and when Slack arrives, put the delivery behind an invocable action and leave the trigger where an admin can still read it.

## Where exactness pushes it into code

### 6. "The quote total must always equal the sum of its lines."

Looks like a flow that sums the children on save.

The deciding property is exactness under concurrency. Two lines saved at the same instant, both automations read the parent, both write, one wins and the total is quietly wrong until somebody reconciles it against an invoice.

**Verdict: a roll-up summary where the relationship is master-detail, because the guarantee is then the platform's.** Where the relationship does not allow one, either change the relationship as a deliberate data model decision, or write Apex that recalculates from the children. Never increment a running total in automation. Reading a number and adding to it is not the same operation as recalculating it, and only one of them survives a data load.

### 7. "Discount eligibility has to match what finance calculates, to the cent."

Looks like a decision matrix, and matrices look declarative.

The deciding property is exactness plus testability. The phrase *to the cent* turns the requirement into a specification with an external oracle, and a specification with an oracle wants unit tests that run on every deployment rather than a canvas somebody eyeballs before release.

**Verdict: an Apex service, exposed as an invocable action and called from the flow that owns the process.** The seam is the point. The process stays visible to the admin accountable for it, and the arithmetic sits where it can be tested, reviewed and diffed. Separating orchestration from calculation this way is the pattern the [Salesforce Architects](https://architect.salesforce.com/) material keeps returning to, and it holds up well in practice.

## Where the rule cannot be written down

### 8. "Send the lead to the rep most likely to close it."

Looks like routing, and routing is declarative.

The deciding property is that it cannot be enumerated. Ask for the rules and you get *well, it depends*, then three worked examples that contradict each other.

**Verdict: neither tool yet.** This is a scoring problem wearing a routing problem's clothes, and the build decision sits downstream of a decision nobody has made. Either it becomes an enumerable rule, round robin within a territory with a named override list, which is declarative and honest about what it is, or it becomes a scoring model, which is a product choice rather than an automation choice. Building it as a flow full of somebody's guesses produces logic nobody can explain and sales will route around within a quarter.

::: warn
When a requirement cannot be stated as a finite list, the tool choice is not the blocker. Sending it back is not obstruction, it is the only way the thing gets specified while somebody still cares enough to specify it.
:::

## Where the platform decides for you

### 9. "The order must not activate until the ERP confirms it."

Looks like a flow with an HTTP callout action, and that is how it usually gets built first.

The deciding property is a platform rule rather than a preference. An outbound callout cannot be made once records have been committed in the same transaction, which is why record-triggered automation performs callouts on an asynchronous path. The sentence as written asks for a synchronous confirmation inside the save that activates the order, and the platform will not give it.

**Verdict: Apex, asynchronous, with an explicit state on the record and idempotent retry.** Pending, confirmed, rejected, each with an owner and a visible meaning. Then renegotiate the sentence, because what the business wants is not that the save blocks. It is that nothing downstream treats the order as active until confirmation lands, and a state field on the [platform](https://www.salesforce.com/platform/) delivers that without pretending a callout can happen where it cannot.

## Where volume changes the answer

### 10. "Recalculate renewal risk on every active account each night."

Looks like whatever it looked like on the day it was built, which is the problem.

The deciding property is volume, and nothing else. At a few thousand accounts a scheduled flow is entirely appropriate. At several hundred thousand it is not, because the work shares one transaction budget and the failure mode is a job that quietly stops finishing rather than one that raises its hand.

**Verdict: a scheduled flow while the row count is modest and the calculation is simple, Batch or Queueable Apex once volume passes what a single scheduled path carries.** The requirement did not change. The org did. Record the row count assumed at build time in the description, because it is the only way a future reader learns that the assumption has expired.

::: note
This is the one verdict on the list with a shelf life. Anything decided on volume belongs on a review list rather than in a build standard, and the review is triggered by the row count, not by the calendar.
:::

## The ten requirements in one table

| Requirement, as stated | Looks like | Deciding property | Verdict |
| --- | --- | --- | --- |
| Cannot save an opportunity without a primary contact | Flow | Assertion about one record, writes nothing | Validation rule |
| Show the account's industry on the case | Flow field copy | The value already exists elsewhere | Cross-object formula or report |
| Contract starts on the first of the next month | Apex | Enumerable, writes the record being saved | Before-save record-triggered flow |
| Route the case by product, region and entitlement | Apex | Enumerable, owned by the service manager | Flow, matrix in custom metadata |
| Notify the account team on escalation | Flow | Maintained by an admin | After-save record-triggered flow |
| Quote total equals the sum of its lines | Flow | Exactness under concurrent saves | Roll-up summary, else recalculating Apex |
| Discount eligibility matches finance to the cent | Flow decision matrix | Exactness with an external oracle | Apex service behind an invocable action |
| Send the lead to the rep most likely to close | Flow | Cannot be enumerated | Neither, specify it first |
| Order must not activate until the ERP confirms | Flow with a callout | Platform transaction rules | Async Apex plus an explicit state |
| Nightly renewal risk across active accounts | Either | Volume | Scheduled flow, then Batch Apex |

## When the criteria disagree

They will, on maybe one requirement in five. Apply them in order.

A platform constraint outranks everything, because preference does not survive contact with something the platform will not do. Exactness comes next, since a requirement that must be right at commit does not become less so because an admin would prefer to maintain it. Enumerability comes third: if the rule cannot be stated, no tool choice rescues it. The maintainer decides what is left, and what is left is most of the backlog.

Read in that order, the awkward cases resolve quickly. The nightly recalculation is a maintainer question until volume makes it a platform question. The quote total is a data model question first and a tool question second. The lead routing never reaches the tool question at all.

## Where this goes wrong

Three patterns, all of them common, none of them about syntax.

**Choosing on perceived difficulty.** The forty-row routing matrix goes into Apex because it felt complicated, and the org acquires a class nobody outside engineering can change. Meanwhile the quote total goes into a flow because it felt simple, and the org acquires numbers that drift.

**Treating neither as a non-answer.** Two of the ten here are answered by a validation rule and a formula field, and both would have shipped as automation if the question had been framed as Flow or Apex. The framing produces the artefact.

**Letting the volume assumption go unrecorded.** Every requirement decided on row count is a decision with an expiry date, and an expiry date nobody wrote down is not a decision, it is a delayed incident.

The conventions that keep these choices legible once a few dozen of them have accumulated are in [Flow in a large org: conventions that hold at a hundred flows](/insights/flow-best-practices-large-orgs). And when the flow you correctly chose starts taking several seconds to save a record, the usual causes are in [Why your flow is slow](/insights/flow-performance-optimisation).

Ten requirements, four properties between them, and not one of them decided by whether the builder preferred a canvas or an editor. That is the useful finding. The tool argument is mostly a preference argument in costume, and the requirement itself will tell you the answer if it is read closely enough.
