The request is pending approval, but with whom? Hardcoded approval flows versus a workflow engine
There are three places approval logic can live. You can write the conditions directly into application code, you can model the flow as an explicit state machine and keep the approval rules as data, or you can run a workflow engine, either a BPMN engine or a durable execution platform. For most business applications the middle level is the right answer. An engine starts to pay off when the business changes the flow without you, when a request stays open for days, and when parallel branches, timers and compensation steps are the heart of the process. But the thing that actually sinks these projects is rarely the level you picked. It is that the current step was never stored as data, no screen answers "who is this sitting with", and six months later nobody can explain which rule produced a given approval.
Measure three things before you choose
Skip the technology argument until you have numbers about your own process.
How often does the flow change, and whose hands change it? When finance moves the approval threshold from 50,000 to 100,000, does that mean a release on your side? Count the changes from the last twelve months and note who asked for each one. Once or twice a year, always through the development team, means hardcoded logic is fine. Monthly, requested by someone who does not write code, means the rules have to come out of the codebase.
How long does one request live? An approval that closes within hours and an approval that waits three weeks for a signature, a budget line and a supplier reply do not want the same architecture.
Is the flow a single chain or does it branch? "Manager, then finance" and "any two of these three directors" are different problems. Settle all three answers while you are still writing the requirements, because this is the part that is most expensive to unpick later.
Store the state in a column, do not derive it from flags
The most common mistake we see is computing approval state from a handful of booleans: manager_approved, finance_approved, cancelled, in_revision. Four flags produce sixteen combinations and most of them describe situations that cannot exist. The first time production shows you a row that is both cancelled and finance-approved, finding the code path that wrote it will cost you days.
What works is one state column plus an explicit list of permitted transitions: draft, pending, revision_requested, approved, rejected, withdrawn. Add three more fields next to it: who it is waiting on (a role or a person), when the wait started, and when it is due. Without those three you can only answer "who has it" by trawling through records. Make the transitions part of the model too, so the question of which state can follow which is defined in exactly one place (common data model mistakes).
Move the approval matrix out of the code
This is the core of the middle level: the answer to "who approves this" is data, not code. Each row of the table pairs a condition with a result. Conditions cover the amount band, cost centre, category and requesting department. Results name the required approvers as roles rather than individuals, say whether they run in sequence or in parallel, and if parallel, how many approvals are enough.
That structure is a decision table, and it has a standard. The OMG's DMN specification reached version 1.6 in September 2026. You do not have to run a DMN engine, but borrow one idea from the spec: hit policy. Decide up front what happens when more than one row matches a request. Does the first match win, or do you collect the approvers from every matching row? Teams that skip this question are the ones who cannot explain why a 120,000 purchase request landed on four separate desks.
Versioning matters as well. A request in flight should stay bound to the rule set it started under. If a matrix change retroactively applies to open requests, the answer to "why did this close with only two approvals" disappears. The practical fix is small: stamp the matrix version onto the request and evaluate against that version.
Five situations that jam real approval flows
Approval systems do not break in the demo. They break in the second month, and they break the same way everywhere.
- The approver is on leave. You need delegation with a start and end date, recorded as a relationship rather than as a password handed over in a corridor. The log entry has to read "X approved on behalf of Y", not "X approved".
- The approver left the company. Routing to roles instead of named people solves most of this on its own (authorization models). Still, keep an admin screen that reassigns orphaned tasks in bulk, because some of them will be assigned to a specific person for good reasons.
- People approving their own requests. Holding the role is not enough. Whoever raised the request must not be able to approve it, even when the matrix says their role qualifies. Separation of duties only exists if the application enforces it.
- The amount changed after the fact. If the requester raises a line from 90,000 to 140,000 after collecting two approvals, those approvals no longer cover the request. Write the rule in advance: any revision that changes amount or scope resets the approvals the new threshold requires.
- Nobody is looking. Give every step a time budget. Reminders and escalations are timer-driven background jobs (queues and background work), and the notification has to reach the channel the person actually watches (notification system design). A director who opens your approval screen once a month will hold the process for weeks if no mail goes out.
"Reject" is not one action
Collapsing three different actions into one button is the fastest way to push users out of your system. Rejection closes the request. Requesting a revision sends it back to the requester and expects it to re-enter the flow. Withdrawal is the requester's own action, and it belongs to them alone.
Define the return point for each in writing. After a revision, does the request start from the top or resume at the step that asked for changes? In practice the healthiest rule looks at which fields changed and decides from that. Make the comment field mandatory rather than optional, because a record that says "rejected" without saying why guarantees the same request comes back next week unchanged.
The audit trail, the queue screen, and the exceptions
Write one row per transition: who, when, which action, which comment, which rule version, and on whose behalf if it was a delegation. Keep it append-only (audit trail design). Record the amount as it stood at the moment of the decision, because that is the only way to see later what an approval actually covered.
There is a subtlety worth naming. An audit trail proves the flow ran. It does not prove the control worked. Everyone may have clicked in order and nobody may have looked. You close that gap by requiring a written justification on exceptional approvals and by listing above-threshold approvals in a report somebody reviews.
One screen repays its cost faster than anything else here: pending requests grouped by approver, with the age of each. It is the screen that ends the argument when a department insists the delay is not on their side.
When a workflow engine genuinely wins
Judge the engine question against five criteria. The process stays open for days or weeks and has to survive restarts and deployments. It has parallel branches and join points. Timers, reminders and escalation sit at the centre rather than the edge. Abandoned processes need compensation steps that undo earlier work. And the business wants to change the flow itself, on a diagram, without you. Three of those being true makes an engine reasonable. One being true means your own state machine is cheaper.
Two distinct families compete here. BPMN engines such as Camunda and Flowable let you model the process as a diagram. The notation is standardised by the OMG, and the current formal version is still BPMN 2.0.2, published in January 2014, so the notation itself has been stable for over a decade. Durable execution platforms such as Temporal and AWS Step Functions take the opposite approach: you write the process as code, and the platform persists the state of each step so execution resumes where it stopped. If the business will maintain the diagram, take the first family. If the flow stays in developer hands, the second creates less friction.
An engine is a dependency too
"BPMN compliant" is not a portability guarantee. Camunda's own documentation states that a diagram modelled for Camunda 7 will not run on Camunda 8, and that the reverse is equally true. Camunda 7 Community Edition had its final feature release, 7.24, on 14 October 2025; no further community releases are coming, security patches included, and the repository has been archived. Enterprise support was extended from April 2027 to 9 April 2030, with paid extended support running to 2032. In other words, picking an engine is a decision that comes back to find you five years later (the risk of end-of-life software).
Platform limits are concrete too. On AWS Step Functions standard workflows, a single execution can run for at most one year, the execution history is capped at 25,000 events, that history disappears 90 days after the execution closes, and the data passed between states cannot exceed 256 KiB. Temporal caps a workflow's event history at 51,200 events or 50 MB, and warns you at 10,240 events or 10 MB. An approval flow that loops per line item on a hundred-line order approaches those numbers faster than you would guess.
Two practical conclusions follow. Keep the business data in your own database: let the engine drive the process, but do not let the truth about the record live inside it. And put the engine behind an interface of your own so application code never calls engine classes directly. Those two rules turn the migration you will face in five years from a rewrite into a port.
If the ERP already approves it, do not build a second approval
When purchase approval already runs in your ERP, building a parallel chain in the new application produces the familiar situation where two records disagree and nobody knows which to trust. Decide which system owns the record, then reflect status to the other side only. Write those status updates so they tolerate redelivery, because the same message arriving twice must not approve anything twice (ERP data synchronisation).
The built-in workflow module in an ERP or a low-code tool is a legitimate option too, especially when the process is single and standard. You find the ceiling when the flow becomes part of what differentiates your own product (where low-code stops being enough). And if you are promising every customer their own approval chain, know in advance that this has to be rule data rather than forked code (customisation versus configuration).
Three things you can do this week
Export your last 50 approval requests and measure how many hours each one waited at each step. Then count how many were settled outside the system, by phone or chat, and entered afterwards. That ratio tells you more than any architecture debate.
Draw the state field and the permitted transitions on a single page. Query production for records whose state combination does not appear on that page.
Pull the approval matrix out of the code into a table, add a version number, and write down the hit policy. Those three steps let you postpone the engine decision, because if you later do adopt an engine, this is exactly what it will ask for first.
If your approval process is pushing people out of the system, or if every change to the flow turns into a new development request, we can review what you have and work out which level you should be sitting at.
Need help with this topic?