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:

  1. 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?”
  2. 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

Next