Workflow and Approvals
Approval workflow is the most-requested custom development in any ERP project. It is also where the company’s internal control framework becomes executable code.
What workflow is for
| Purpose | Example |
|---|---|
| Authorisation | Purchase over 500,000 needs CFO approval |
| Segregation of duties | Preparer ≠ approver |
| Notification | Tell the buyer their requisition was approved |
| Escalation | No response in 48h → go to the manager’s manager |
| Sequencing | Quality must release before shipping |
| Audit evidence | Prove who approved what, when, on what information |
That last one matters most. The approval log is audit evidence. If it’s incomplete or editable, the control doesn’t exist as far as an auditor is concerned.
The approval matrix
The core configuration object:
Document Condition Approvers (in order)
────────────────────────────────────────────────────────────────────────
PR value < 50,000 Cost centre owner
PR 50,000 ≤ value < 500,000 CC owner → Dept head
PR value ≥ 500,000 CC owner → Dept head → CFO
PR capex, any value CC owner → Dept head → CFO → CEO
PO change price increase > 5% Original approver + Purchasing mgr
Invoice variance beyond tolerance Requester → Buyer
Journal manual, value > 100,000 Controller
Journal any, posted to a bank account Controller → CFO
Payment any Two authorisers (dual control)
Master data Vendor bank detail change Requester → Finance → callback verified
Credit memo value > 25,000 Sales mgr → Finance
Discount > 15% off list Sales mgr; > 25% → Sales director
Design principle: the matrix is data, not code. Thresholds change quarterly; approver roles change with every reorg. If a threshold change needs a deployment, you’ve built it wrong.
Approver determination
| Method | Notes |
|---|---|
| Named user | Brittle — breaks when they leave |
| Role | Better — “whoever holds the Financial Controller role” |
| Position | Best in HR-integrated systems — the position’s holder, as of today |
| Manager of requester | Dynamic, from the org hierarchy |
| N levels up the hierarchy | Walks the org tree until the threshold is met |
| Cost object owner | Whoever owns the cost centre or project being charged |
| Rule-based | Category + region + value → a computed approver |
Always resolve to a person at runtime, never store a person on the workflow definition. And always have a fallback: what if the position is vacant, the role has no holder, or the manager is the requester?
The substitution problem
People go on leave. Your workflow must support:
- Delegation — “approve on my behalf while I’m away”, with a date range, logged as delegated
- Escalation on timeout — no response in N hours → next level
- Reassignment — an admin moves a stuck item, with a reason
- Out-of-office detection from the HR/leave system
Without delegation, approvals stop dead every holiday season. This is not a nice-to-have; it’s the first thing users complain about.
Workflow patterns
SEQUENTIAL A → B → C each waits for the previous
PARALLEL A ─┬─ B all at once
└─ C
▼ all must approve
QUORUM any 2 of {A,B,C}
CONDITIONAL if amount > X then add C
ESCALATION A, timeout 48h → A's manager
DELEGATION A is away → A's delegate
FOUR EYES preparer ≠ approver, enforced
DUAL CONTROL two independent approvers, neither aware of the other's decision
Parallel is underused. Sequential approval of five levels takes a week. If three of them are independent, run them in parallel and cut the cycle time by half.
Data model
workflow_definition(
id, name, entity_type, version, active,
trigger_condition, -- expression evaluated on the document
steps JSONB,
valid_from, valid_to
)
workflow_instance(
id, definition_id, definition_version,
entity_type, entity_id, tenant_id,
status, -- RUNNING | APPROVED | REJECTED | CANCELLED | ERROR
started_at, completed_at, started_by,
context JSONB -- snapshot of the values the decision was based on
)
workflow_task(
id, instance_id, step_number, step_name,
assigned_to, assigned_role, delegated_from,
action, -- APPROVE | REJECT | REQUEST_INFO | REASSIGN | ESCALATE
decision, comment,
due_at, reminded_at, escalated_at,
created_at, completed_at,
UNIQUE(instance_id, step_number, assigned_to)
)
workflow_audit( -- append-only, never updated
instance_id, sequence, event_type, actor, timestamp,
from_state, to_state, payload JSONB
)
Two design points that matter:
- Version the definition and pin the instance to a version. Changing the approval matrix must not alter in-flight approvals — an auditor asks “what rule was in force when this was approved?”
- Snapshot the decision context. Store the value, cost centre, and vendor as they were when approved. If the document is later changed, the approval is against the old values, and you need to detect that.
The change-after-approval problem
PR for 45,000 → approved by the cost centre owner (below the 50,000 threshold)
Someone edits it to 450,000 → ships without CFO approval
This is a real control failure and a real fraud vector. Mitigations:
- Lock the document once the workflow starts. No edits while pending.
- Re-trigger the workflow if any approval-relevant field changes after approval. Define “approval-relevant” explicitly (value, vendor, cost object, material) and store a hash of those fields.
- Show a diff to subsequent approvers if anything changed.
- Log every change with before/after values.
Implementation: store a hash of approval-relevant fields with the approval; on posting, recompute and compare. Mismatch → re-approval required.
Notifications
Immediate: a task is assigned to you
Reminder: due in 24h, still pending
Escalation: overdue, going to your manager
Digest: daily summary of everything pending
Outcome: your request was approved/rejected
Channels: email, mobile push, in-app inbox, Teams/Slack, SMS for urgent.
Approval by email reply is a security problem. Email is not authenticated. Use a signed deep link that requires SSO login, or an approval action inside an authenticated app. “Reply APPROVE” workflows have been the vector for real invoice fraud.
Mobile approval is the highest-value feature you can add to a workflow. Approval cycle times drop dramatically when executives can approve from a phone. Give them enough context on the screen to decide — amount, vendor, cost object, requester, business justification, and budget remaining — not just a document number.
Common workflow requirements
| Requirement | Why |
|---|---|
| Rejection with a mandatory reason | The requester needs to know what to fix |
| Request more information | Sends it back without rejecting; preserves the approvals already given |
| Withdraw | The requester cancels their own pending request |
| Bulk approve | Approvers with 200 items/day will not click 200 times |
| View history | The full trail on the document |
| Substitute/delegate | Holidays |
| Escalation | Nothing should sit forever |
| SLA reporting | Where is the bottleneck? |
| Emergency override | With heavy logging and post-hoc review |
That last one is worth a note: emergency overrides exist in reality (a plant is down at 2am and needs a part). Better to have a logged, reviewed break-glass path than to have people share credentials. Report every use to the audit committee.
Anti-patterns
| Anti-pattern | Why it’s bad |
|---|---|
| Approval levels for everything | Approval fatigue → rubber-stamping → the control is theatre |
| Seven sequential levels | Weeks of cycle time; nobody reads by level 4 |
| Named individuals in the definition | Breaks on every departure |
| No delegation | Stops every holiday |
| No escalation | Items sit forever, and the requester works around the system |
| Approval by email reply | Unauthenticated, spoofable |
| Editable audit log | Destroys the evidential value |
| Threshold changes require a deployment | Guarantees the thresholds are out of date |
| Approver = requester allowed | Defeats the entire purpose |
The most important one: too many approvals destroys the control. An approver reviewing 300 items a day approves everything without reading. Fewer, meaningful approvals with real thresholds are far more effective than blanket approval on everything.
Workflow engines
| Option | When |
|---|---|
| The ERP’s native engine | Default. Integrated with the documents and the org model |
| Low-code (Power Automate, SuiteFlow) | Simple flows, business-user maintained |
| BPMN engine (Camunda, Flowable, Temporal) | Complex, long-running, cross-system orchestration |
| Hand-rolled state machine | Only for genuinely simple, well-bounded cases |
If you hand-roll: it’s a state machine with persistence, timers, retries, and an audit log. That’s more work than it sounds, and the timers are the part people forget. Estimate accordingly.
Metrics
- Average approval cycle time by document type and by step
- Pending items by age — anything over 5 days is a problem
- Approval rate (a 100% approval rate means the control isn’t a control)
- Rejection reasons distribution
- Escalation rate — which step is the bottleneck
- Delegated approvals as a percentage
- Emergency overrides — count and review status