Building a Visual Approval Workflow Engine
Every ERP ends up needing approvals: purchase orders above a limit, price discounts, work-order release, credit notes. Buying a full BPM suite is heavy; hand-coding if (amount > 50000) sendTo(manager) for every case rots quickly. The middle path is a small workflow engine with a visual designer. This article describes the model we build, deliberately pragmatic.
Scope it down
Resist the temptation to build a general-purpose BPM engine. In our experience, approval flows in ERP need exactly four node types:
- Start / end — bookends of the definition.
- Approval — a task assigned to a person or role (department head, finance controller), with a timeout and an escalation path.
- Condition — a boolean expression on document fields (amount, department, item category) deciding which branch to take.
- Countersign group — multiple approvers in and (all must approve) or or (first decides) mode.
Four node types cover essentially every real approval we have met. Anything fancier — loops, subprocesses, message events — is where general BPM tools spend 80% of their complexity budget for 5% of the value.
Definitions, versions and instances
A process definition is a versioned graph: nodes, edges, and per-node configuration. An instance is one run attached to one business document (a purchase order, a discount request). The rules that keep this safe:
- Definitions are immutable once published. Editing a published flow creates a new version.
- An instance binds to the definition version it was started with, and stores a snapshot. A flow started under v3 keeps running under v3 even after v4 publishes — mid-flight documents never silently change rules.
- Only one version is the default for new instances; old versions retire naturally as their instances finish.
The instance state machine
Keep the instance state model embarrassingly simple. Each instance has a current node; each node task has a state of pending / approved / rejected. Transitions:
- Start → first node task created, assigned per node config (role resolution happens at assignment time, so a vacant position does not stall the flow silently — it escalates).
- Approver acts → task resolved, engine evaluates the node's completion rule (countersign mode, vote thresholds).
- Next node chosen by evaluating outgoing condition edges in order.
- Reject → instance goes to a terminal
REJECTEDstate, optionally with a "resubmit" path that restarts the flow. - No more nodes →
APPROVED; the engine fires an event the business module listens for.
Every action — assignment, approval, rejection, escalation — is an append-only log entry on the instance. The approval trail a manager sees is just this log rendered; so is the audit evidence.
Integration with business modules
The engine owns orchestration, never business data. Business modules attach a workflow to a document type and subscribe to the APPROVED / REJECTED events. This keeps the engine a pure component of the modular monolith: sales, purchasing, production and finance all reuse the same engine without the engine knowing what a "sales order" is. Timeouts and escalations run on a scheduled sweeper that scans pending tasks past their deadline.
The visual designer
The designer is a canvas where consultants drag the four node types, connect them, and configure each node in a side panel. Under the hood it edits the same JSON graph the engine executes — no model-to-model translation. Validation before publish checks the boring-but-fatal things: unreachable nodes, conditions without else branches, approval nodes without assignees. Keep the canvas simple (grid snap, straight edges); you are drawing approval chains, not circuit boards. For the rendering side of similarly dense enterprise screens, see Enterprise Web UI Patterns.
Conclusion
A workflow engine earns its keep by being smaller than people expect: four node types, versioned immutable definitions, a per-node state machine, an append-only action log, and event-based integration. Build that well and every approval in the ERP — present and future — becomes configuration instead of code.