Decision tables for business rules in approval flows
Decision tables for business rules help teams map approval logic, policy exceptions, and edge cases before engineers automate the workflow.

Why approval rules break so often
Approval rules usually fail before anyone writes code. A company thinks it has one policy, but people actually follow several slightly different versions of it.
That gap shows up fast. One manager says travel over a certain amount always needs finance review. Another says client travel is exempt. A third person remembers a one-time exception from last year and treats it like a standing rule. Everyone sounds sure. Everyone conflicts.
Then similar requests get different answers. Two employees submit almost the same expense. One gets approved in ten minutes. The other gets rejected because a different approver reads the situation differently. After a while, people stop trusting the policy and start guessing who is likely to say yes.
Engineers inherit that mess when they automate approvals. If the written policy leaves gaps, the system still has to decide something. So the team fills in the blanks. Sometimes they ask a product manager. Sometimes they read old tickets. Sometimes they copy what seems most common. That is how assumptions become approval rules.
A lot of those hidden rules live in places nobody audits. They sit in old email threads, chat messages, and comments on past requests. Someone once wrote, "For contractors, send anything unusual to legal first," and now that sentence controls real work even though it never made it into the policy. The rule exists, but only in memory and inbox search.
A simple expense process shows how quickly this breaks. If meals under $50 are auto-approved, what happens when alcohol is on the receipt? What if the employee is with a client? What if the trip was pre-approved? If nobody writes those exceptions down, each approver creates a private version of the rule.
That is why decision tables help. They force teams to name exceptions, define gaps, and stop treating scattered emails as policy. Until that happens, the software does not automate the process. It automates confusion.
What a decision table does
A decision table turns messy approval logic into a grid people can scan quickly. Instead of pulling rules from policy files, chat threads, and old emails, the team puts each condition in a separate column and each decision in a separate row. That simple format forces everyone to say what actually changes the outcome.
In practice, the columns hold the facts that matter: request type, amount range, department, region, vendor status, customer status, or an exception flag. Each row combines those facts and ends with one clear result - approve, reject, send to finance, or ask for director review. If two people read the same row, they should reach the same answer.
This works because normal cases and odd cases sit in the same place. A routine request can appear right next to a rare exception that needs extra review. You do not bury that exception in a paragraph nobody reads. You put it where everyone can see it.
That matters when a company starts automating approvals. Engineers rarely get rules in one clean package. One person mentions a threshold. Another remembers a special case from last year. Someone else adds a note about a customer group that skips the usual path. A table turns those fragments into a rule set people can review line by line.
It also makes conflicts obvious. If one row says a request should pass and another row covers the same facts but sends it to legal, the clash shows up before anyone writes code. Missing rows show up too. If nobody knows what should happen in a certain case, the gap is right there in front of the team.
A decision table does not make rules more complicated. It removes guesswork. That alone can save weeks of rework after the workflow reaches real users.
Gather the real rules first
Most approval flows fail because the source material is bad. The official policy says one thing, while daily work follows a mix of email replies, chat messages, spreadsheet notes, and memory.
If you want a useful table, collect that mess first. A cleaned-up document written after the fact is often less useful than the actual trail of decisions people made last month.
Start with the obvious sources: policy documents, handbooks, email threads about exceptions, chat messages where approvers explained a one-off choice, forms, spreadsheets, tickets, and any recent approvals that caused debate. Then talk to the people who approve requests every day.
Do not ask them for the ideal process. Ask for the last five strange cases they remember. Those cases expose the real rules much faster than a polished policy ever will.
A finance lead might say, "I approve anything under $2,000." That sounds clear until you ask about edge cases. Suddenly new conditions appear: a new vendor, a missing receipt, travel during a hiring freeze, or a request charged to a client project. Those details already affect decisions, so they belong in the rule set.
Write down the inputs approvers actually check, even if they seem small. Amount is obvious, but teams often look at requester role, department, budget status, vendor type, contract length, risk level, and whether the request is routine or unusual. If someone checks it before saying yes or no, it is an input.
Then separate hard rules from habits. A hard rule comes from policy, compliance, budget control, or legal limits. A habit is something like "I usually ask Sam if the number feels high" or "I like to review marketing spend myself." Habits matter because they affect work, but they should not slip into automation unnoticed.
A simple label next to each rule helps: policy, exception, or personal habit. That gives engineers less room to guess, and it gives the business a chance to decide which exceptions to keep, which to remove, and which to turn into explicit rules before anyone automates them.
Choose the columns with care
A decision table only works if each column means one thing and people can answer it without guessing. If a reviewer cannot look at a request and fill in the column with confidence, the column is too vague for automation.
Start with facts that already exist in the form, policy, or source system. "Amount over $500" works. "Urgent purchase" usually does not unless the policy defines urgent in plain language and the form captures it the same way every time.
The safest columns are usually the plain ones. Fixed categories such as department, expense type, contract type, and country work well. So do clear flags such as receipt attached, manager approved, vendor on approved list, or customer verified. Hard ranges also help because they leave less room for debate.
Missing information needs its own state. Do not leave blanks and hope the system will figure it out. Use a clear value such as "missing," "unknown," or "not provided." That one choice prevents a surprising number of bad approvals, especially when a request moves fast and nobody notices an empty field.
A small finance example makes the difference obvious. Suppose an expense request includes amount, department, travel related yes or no, receipt attached yes or no, and employee level. Those are solid columns. Now compare that with "high priority" or "reasonable cost." People use those labels differently, so engineers end up encoding personal guesses from old emails or one manager's habits.
If you find a fuzzy label, split it into something measurable. Replace "high priority" with "service outage yes or no" or "needed before event date yes or no." Replace "large expense" with a number range. The table gets longer, but it gets safer.
Plain columns are easier to build, easier to test, and much easier to defend when someone asks, "Why did this get approved?"
Build the table step by step
Most teams make this harder than it needs to be. They start with rare edge cases, old arguments, and half-remembered exceptions. Start with the path that handles most requests. That path gives the whole table its shape.
Write one row for the normal case first. If an expense is under the usual limit, filed on time, and tied to an approved category, the result should be clear. That first row gives everyone a baseline.
Then add confirmed exceptions one at a time. Pull them from real cases, not guesses. If finance made a special call for travel during a client emergency, or a manager approved a late request during system downtime, give that exception its own row instead of hiding it in notes.
Each row needs one outcome. Approve, reject, send to finance, escalate to a director, or ask for more information. Do not mix two results in one row, because engineers will still have to guess what happens first.
When the team cannot agree on a rule, assign an owner for the decision. That might be finance, legal, operations, or the policy writer. If nobody owns the gray area, it will come back later as a bug or a support ticket.
Duplicate rows create a quieter problem. Two rows may look different but still produce the same result under the same conditions. Merge them before handoff or the rules will drift as soon as someone edits only one copy.
One more thing matters here: rule order. If two rows can match the same request, define which one wins. Do not leave that choice to whoever implements the workflow.
When an engineer can read the table and build the flow without opening old email threads, the table is ready.
A simple example with expense approvals
A small expense claim can expose a lot of hidden policy. That is why decision tables work well in approval flows. They force each exception into a clear row instead of leaving it buried in email threads or someone's memory.
Imagine four claims that look similar at first glance. Each one is for a normal business expense, but the right action changes once you add a few columns.
| Submitter | Expense type | Amount | Receipt attached | In policy | Action |
|---|---|---|---|---|---|
| Employee | Meal | $300 | Yes | Yes | Send to manager for approval |
| Contractor | Meal | $300 | Yes | No standard reimbursement | Send to finance for review |
| Employee on travel | Meal | $300 | No | Exception | Hold and request receipt or written explanation |
| Employee | Hotel | $300 | Yes | No | Send to finance for policy review |
The first two rows show why amount alone is not enough. An employee and a contractor submit the same $300 meal expense, but the company may treat them differently. If engineers only hear "meals under $500 need manager approval," they might approve both. The table stops that mistake.
The missing receipt case matters just as much. Many teams treat it as a minor detail and try to fix it later. In practice, it changes the route. The system should pause the claim and ask for proof, not assume a manager will sort it out after approval.
The hotel charge shows another common issue. An out-of-policy item should go to the team that owns the rule. In many companies, that is finance, not the employee's manager. Managers know the trip. Finance knows the policy.
This kind of table gives engineers exact rules to implement. It also gives testers something concrete to check. When a new exception appears, you add a row or a column. You do not rewrite the whole flow.
Mistakes that create bad automation
Bad automation usually starts before anyone writes code. A team has a policy document, a few old email threads, and a manager who says, "We handle that case manually." Then engineers turn that mess into workflow logic, and the hidden gaps become bugs.
One common mistake is hiding exceptions in notes. If the rule says "CFO approval over $5,000" but a note says trade show travel can skip that step, the note is not a note anymore. It is a rule. Put it in the table as a condition and an outcome or someone will miss it.
Another problem is mixing policy with personal preference. A manager may prefer to review all software purchases, but that does not make it company policy. If the table blends formal policy with one person's habit, the process breaks when that person leaves or changes their mind.
Rule order causes a lot of damage too. Two rows can both match the same request and still produce different outcomes. If nobody defines which row wins, engineers guess. One team may code "first match wins," while another uses "most specific wins." Pick one approach and write it down.
Missing and conflicting data need their own path. Teams often act as if every request arrives with a department, amount, reason, and cost center. Real requests do not. Some fields are blank. Some values disagree. If the table ignores that, the system either approves bad requests or sends them into a dead end.
The worst outcome label is "use judgment." Software cannot apply judgment unless you turn it into named checks. Replace vague outcomes with clear actions such as:
- send back for missing information
- route to finance for conflict review
- escalate to a named approver
- reject with a stated reason
A decision table works only when each row can run without a side conversation. If someone still has to ask, "What do we usually do here?" the rule is not finished.
A quick check before engineers start
Before anyone turns the table into code, someone should read it like a stranger. Software will follow the table exactly, even when the table is vague. If one rule can mean two different things, the code will pick one and users will call it a bug.
A quick review catches most of the obvious problems:
- Each row ends with one clear action.
- The same set of inputs never points to two different actions.
- Blank cells mean "any value" only when that is intentional.
- A new teammate can explain the table back in plain English.
- Recent edge cases fit somewhere without a debate.
Then test it with real requests. Walk through one normal case, column by column, until you reach the action. Do it again with something awkward, like a missing document or an exception from a senior manager. If the path feels shaky on paper, it will feel shaky in the product too.
This review usually takes less than half an hour. It can save days of rework, awkward approval disputes, and the old habit of digging through email threads to learn what the rule "really" meant.
What to do next
A table can look neat and still fail in practice. Before you automate anything, run it against the last 20 approvals your team handled. Use the final outcome, the reason behind it, and any exception that showed up in email or chat.
Look for gaps, not perfection. If a past request fits more than one row, the table is still fuzzy. If someone still needs an old message to explain why a case passed or failed, the rule is not fully written.
When you find a mismatch, keep the fix simple. Find the row that almost worked. Write the missing condition in plain language. Decide whether it is a real policy or a one-off exception. Then add, merge, or remove the row and test the same case again.
This part can feel slow. It is still much cheaper than fixing automation after people stop trusting it. A line like "approve if finance already knows the vendor" is not a rule unless you define what that means and who decides it.
Fix every row that still depends on email context. If a manager says, "we usually allow this when the amount is small," turn "small" into a number. If the rule depends on history, name the system that holds that history. If the answer changes by team, add that condition to the table.
After that, set one owner for future changes. One person does not need to decide policy alone, but one person should keep the table current, record rule updates, and stop side-channel edits from creeping back in.
If your approval logic is messy or spread across several teams, an outside review can help before engineers build the workflow. Oleg Sotnikov at oleg.is works as a Fractional CTO and startup advisor, and reviewing unclear process logic before it turns into expensive automation mistakes fits naturally into that kind of work.