A while after I built the discount approval matrix for TechnoStore — a fictional B2B retailer I run in a real Developer Edition org — I asked myself a small maintenance question. Suppose the business wanted one threshold moved. Not redesigned. Moved.
The honest answer was: a developer, a sandbox, a deployment, and a release window. For a number. That is when I saw what I had missed while designing the thresholds themselves. I had written about how to choose those tiers and I stand behind that thinking. What I had not thought about was the matrix six months later, when nobody remembers the reasoning and the business has moved on.
Approval matrices rarely fail on day one. They age badly. This post is about why, and about the structural decision that fixes most of it.
Two clocks, running at different speeds
Every approval matrix sits between two clocks.
The first is the business cadence. Margins shift. A new product line arrives with a different cost structure. Finance gets a new director with a different appetite for risk. A quarter ends and a temporary exception is agreed in a meeting. None of this consults your release calendar.
The second is the release cadence. Code is written, reviewed, tested and deployed on a schedule that exists for very good reasons — it is what stops Friday afternoon from becoming an incident.
Now put a discount threshold inside the second clock and ask it to keep up with the first. It cannot. And here is the part that matters: it does not fail loudly. The rule becomes wrong slowly, while running perfectly. Nobody gets an error. The approvals still route — just according to a picture of the business that stopped being accurate some time ago.
A rule that changes on a business cadence and ships on a release cadence will always be behind. The question is only how far.
Where the rule usually ends up living
There are three common homes for a threshold, and two of them are traps.
In Apex. A comparison inside a class. Fast to write, invisible to anyone who is not a developer, unchangeable without a deployment.
In a Flow’s branches. This feels better, because a Flow is declarative and an admin can open it. But a decision element with a nested ladder of conditions is only theoretically readable. Try answering “what happens to a large discount on a renewal for a partner account?” from a branching diagram — you will find yourself tracing lines with your finger, and it takes longer every month.
In Custom Metadata. A table. One row per rule. Readable in Setup, queryable in Apex, deployable like any other metadata, and — the crucial part — separable from the logic that reads it.
The third is the answer, and the reason is less about elegance than about who is allowed to look.
A rule nobody can read is a rule nobody can audit
This is the sentence I would put on the wall.
Approval matrices exist to be defended. Someone will eventually ask why a particular quote was approved, or why one was not. Finance will want to see the policy, and so may an auditor. A new sales manager will want to explain it to a rep who is annoyed by it.
None of those people can read Apex. Most cannot read a twelve-branch Flow either, and — this is the honest part — after three months, neither can you, not quickly and not reliably. When the only way to answer “what is our policy?” is to ask a developer to read the implementation, you do not have a policy. You have a behaviour, and you are reverse-engineering it on request.
A table changes that. Setup shows the rules as rows, and anyone with access can read the current policy in a minute.
What a rule row should hold
Once you decide the rules live in a table, the design question becomes what belongs in a row. I keep four kinds of column.
The condition. The threshold itself, and whatever else qualifies the rule — a discount band, a deal size.
The scope. Which slice of business this row governs: a product family, a segment, a region, a deal type. Without it, every change becomes a global change — which is how a sensible rule for one product line throttles another.
The approver, named by role. Not by person. People change jobs and leave companies, and a matrix that hardcodes a name becomes a matrix that routes into an empty inbox. Store the role; resolve the human at run time.
A short, plain-language reason. One sentence: why this threshold, decided when. It is the column people skip and the one that saves most time later — the only defence against a threshold everybody obeys and nobody can justify. (I write about custom metadata as a pattern here; this is my favourite use of it.)
With those four columns, changing policy means editing a row. The logic that reads the table does not change, because it was never the thing that needed to.
What still does not belong in the table
Two things, so this does not get oversold.
The first is judgment. A table can route a request to the right person; it cannot make that person read it. If your approvers are rubber-stamping, no data model will fix it — that is a problem in the tiers themselves.
The second is the engine. The code or Flow that reads the rows is still code, and still deserves tests, review, and a proper deployment. What you have separated is the part that changes weekly from the part that changes rarely. That separation is the whole benefit.
Four signs your matrix has already aged
You can diagnose this without opening anything technical. Ask around and listen.
Exceptions travel by email. When people route around the process for anything unusual, the matrix has stopped describing the business.
Nobody can say when a threshold last changed. If there is no answer, it is longer ago than anyone thinks.
A “temporary” tier is over a year old. Temporary rules are the most permanent objects in enterprise software.
Reps predict outcomes better than the rules explain them. When the team’s folklore is more accurate than the configuration, the configuration is out of date.
Your next step
Open the approval logic in an org you have access to and answer two questions in writing. First: if the business wanted one threshold moved tomorrow, what would have to happen? Count the steps and the people — that number is your real cadence, and it is usually a surprise. Second: could a colleague who does not write code read the current policy and explain it back correctly? Ask one; do not guess on their behalf.
If either answer is uncomfortable, you know the shape of the work. Move the rules into rows, leave the engine where it is, and add the plain-language reason column while you still remember the reasons. Your matrix will still age — it will just age at the speed of the business rather than your release pipeline.