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 write to 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, and it is not an Apex problem. In most of these orgs both tools were used competently. What is missing is one sentence explaining why each thing was built the way it was, written at the only moment anybody knew the answer.
The choice itself has been argued to exhaustion, and we have worked the criteria through elsewhere, applied to real requirements, in Flow or Apex: the criteria applied to ten real requirements. This piece is about the part that never gets written: the record of what was decided and why. It costs about ninety seconds at build time, and it is what stands between the next engineer and a forensic afternoon.
What actually happens when the reasoning is not recorded
The failure is not that the org becomes undocumented. It is that the same decision gets made repeatedly, each time with worse information, and that it eventually gets made backwards.
The cheap version is re-litigation. A requirement extends six months later. Whoever picks up the ticket cannot tell whether the before-save flow was a deliberate choice or a habit, so they start the analysis again, with less context than the first team had, because the person who knew about the finance constraint has moved on. The best outcome is that they land on the same answer and the organisation has paid twice for one decision. That is the good day.
The expensive version is a quiet reversal. Invisible reasoning looks like an arbitrary constraint, and arbitrary constraints get removed by competent people acting in good faith. Pricing logic goes into Apex because finance needs the calculation covered by tests that run on every deployment. Two years later a small rounding change arrives, and reimplementing it as a formula inside a flow is genuinely faster, so that is what happens. The behaviour is correct. What has quietly stopped existing is the assurance that the calculation is exercised at every release, and nobody involved knew that was the trade. Nothing breaks. The reversal surfaces at an audit eighteen months later.
The version that costs most is that nothing happens at all. Because nobody can prove a piece of automation is safe to remove, it stays, and the next requirement gets built alongside it rather than through it. That is how one object ends up carrying four things that all fire on save. We have watched a capable team spend most of a day reconstructing why an Apex class existed, then agree to leave it alone. A day spent, no decision reached, class still there.
The record is a decision, not a description
Most attempts at this fail because the team writes documentation instead. Documentation describes what the automation does, and the org already tells you that far more reliably than a page written a year ago does. You can open the flow. You can read the class.
What you cannot get from the org is the option that was rejected and the constraint that ruled it out. That is the record.
So the rule is: if the org can tell you, do not write it. Step-by-step descriptions of flow elements, field mapping tables, screenshots of the canvas. Each is stale the first time somebody edits the flow, and a stale description is worse than none, because the next reader trusts it for a few minutes before discovering they should not have. Written intent does not decay the same way: the reason a thing was built either still holds or has visibly stopped holding, and both are useful findings.
What the four lines have to contain
The record we ask teams to write is four lines, and each one earns its place by answering a question that will actually be asked in two years.
| Line | What it has to say |
|---|---|
| WHY | The requirement and a reference that can be checked later: a ticket, a policy, or the role that approved it. |
| OWNER | The role accountable for it, plus the escalation path. Never a person's name. |
| CHOICE | What was built, and explicitly what was rejected and on what grounds. |
| RISK | What else runs on the same object at the same timing, and the assumption that would change the answer. |
WHY exists to make future deletion possible. In two years the only question that matters is whether the requirement behind this logic still stands, and that is answerable only if it can be traced to something outside somebody's memory. "Finance asked for it" fails that test. "FIN-224, approved by the Financial Controller" passes, because a successor in that role can be asked whether it still applies.
CHOICE is the line that stops the re-litigation, and the half people skip is the rejected option. "Before-save flow, maintained by Revenue Ops. Apex rejected: no callout, and nothing to test beyond a single derivation" tells the next reader in one clause both what the constraints were and what would have to change for the answer to move.
OWNER is a role because names leave. Automation whose owner has left the organisation is unowned no matter what is written down, and role-level ownership is the only version that survives a reorganisation.
RISK is the line that prevents the fourth flow. It names the neighbours, so the next person adding logic at the same object and timing knows there is already a place for it. It also carries the assumption most likely to expire, usually volume: logic that is correct at a handful of records a day may belong in asynchronous Apex at thousands, and the volume assumed at build time is invisible in the finished artefact.
Where the record lives, which is the part everyone gets wrong
This is where good intentions usually die. A team writes genuinely useful records, puts them in a wiki, and eighteen months later nobody has opened it. That is not a discipline failure but a placement failure, and it is predictable: finding a wiki page requires knowing it exists, which is precisely the knowledge the next person lacks.
The test is simple. The record has to be in front of somebody's eyes at the moment they open the thing to change it, without them having gone looking for it.
Placements that pass are unglamorous. The flow description field, which shows in the flow list and again in the editor. The Apex class header comment, which travels with the file into version control and appears in the diff when the class changes. The pull request description. The ticket that authorised the change. All of these sit on a path somebody is already walking.
Placements that fail are the ones that feel most organised: a wiki space, a spreadsheet inventory, an architecture document, a message in a channel. Each requires a deliberate act of retrieval by a person who does not yet know there is anything to retrieve.
Repeating the same four lines in the flow description and in the ticket is not duplication worth eliminating. Redundancy across two places on the path is cheap, and it is what makes the record findable from either direction.
Introducing this to a team that will call it bureaucracy
The objection arrives immediately and it is not unreasonable. Teams have been asked to fill in artefacts before, and most were read by nobody. Four things separate adoption from polite compliance.
Do not announce a policy. Write the records for the next three things the team builds, then write them for the three worst pieces of legacy automation and hand that second set to the people who maintain them. The argument lands far better from the receiving end than from a governance slide.
Cap the length and mean it. Four lines. If the record is longer than the ticket summary it will not get written, and a record with no size limit turns into a design document nobody finishes.
Do not create a gate. No new approver, no new review step, no new tool. The moment a record needs sign-off it becomes something to be got past: resisted, then bypassed, then written after the fact by somebody guessing. Put it in the definition of done for the change that already exists.
Make the argument selfishly. Governance framing invites a governance response. The framing that lands is that the person handed this at five o'clock on a Friday in two years is statistically likely to be someone already in the room.
What it changes in a code review and in a release
A reviewer with no record can only answer whether a change works. A reviewer with one can answer whether the change belongs there, which is a different and more valuable question.
In practice the review question becomes: which line of the record does this change invalidate? A pull request that adds a callout to automation whose CHOICE line says "no callout, admin-maintained" is a visible contradiction rather than merely a change. Sometimes the contradiction is right and the record needs updating; either way it gets discussed, which is the point. The corollary is a hard rule: a record not updated in the same change as the thing it describes is worse than no record, because it is now confidently wrong.
At release, the RISK line is the regression scope. Deploying into a shared before-save path carries a different risk from deploying into an isolated one, and the record is usually the only artefact stating which you have. The OWNER line tells the release manager who to notify, and who to call when the first support ticket lands on Monday.
When the record should be revisited rather than obeyed
A decision record is not a rule, and treating it as one produces the opposite failure: an org that preserves constraints long after the reasons for them expired. There are four honest reasons to reopen.
- The stated assumption is no longer true. Volume has grown past what the record assumed, or the maintaining team no longer exists in the shape the OWNER line describes.
- The platform moved. This has happened more than once in recent years and will again. A decision made against a constraint that no longer exists should be reopened rather than defended, and the current Salesforce developer documentation is the authority on which limits are shared across a transaction and which declarative capabilities now exist.
- The owner is gone. Automation with no living owner has no record, whatever is written down. Reassign it or retire it.
- The rejected option has become the cheaper one. The CHOICE line names it, which is what makes this checkable rather than a matter of taste.
The bad reason is that the new person would have chosen differently. Preference is not new information, and a record exists partly to make that distinction possible: the bar for reopening is a changed fact, and the record is what lets you name which fact changed.
When you do reopen, add to the record with a date rather than overwriting it. The superseded reasoning is what tells the next reader what the org used to be optimising for, and that is often the most useful thing in the file.
The decisions already made and never written down
Do not attempt to backfill the estate. A project to document every existing piece of automation produces a large artefact, consumes a quarter, and is out of date before it is finished. Triage instead, in three passes.
- Write a record for anything you touch. The moment automation is opened for a change is the cheapest possible time to record it, and it aims the effort at exactly the parts of the org that are still in use.
- Write records for what already blocks work. Every org has three or four pieces of automation that come up in every scoping conversation and that nobody can explain. Those are costing you now, and they are worth an hour each.
- Give everything else an owner or a date. One line per artefact: a role, or a candidate retirement date. An estate where every automation has a named owner and nothing else recorded is in far better shape than one with an immaculate wiki and no owners.
Where the reasoning is genuinely unrecoverable, do not reconstruct it. Write that it is unrecoverable, note when the thing first appeared, and mark it a retirement candidate. An honest gap is a finding somebody can act on; a plausible guess is a fabrication the next reader will believe and build on.
The default the record starts from
None of this replaces having a sensible default. This is the one we hand teams on day one, and the record's job is to capture the cases where the default was departed from, and why.
| 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 asynchronous processing | Queueable or Batch Apex |
| Integration callout with retry and idempotency | Apex, exposed as an invocable action |
The pattern underneath it is that Flow orchestrates and Apex calculates. Flow is where the business process lives and where an admin can see its shape. Apex is where logic that needs tests and a code review lives. The invocable action is the seam between them, and a good one, because it keeps the process visible without pretending a pricing engine belongs in a decision element.
The tooling argument will keep moving. The maintenance argument does not. Build for the person who has to understand this without you in the room, and give them the ninety seconds of writing that makes it possible.



