Suthradhaari multi-level approval engine with dynamic routing, escalation, and delegation
Suthradhaari is InfraTraq's configurable multi-level approval engine. It handles all document approvals — Purchase Orders, Work Orders, Material Requests, RA Bills, Change Orders, Leave Requests, Advance Requests, and more — with condition-based routing, delegation, escalation, and SLA tracking. Built on 10 workflow tables, the engine is linked to every transactional module in the system.
| Status | Description | Allowed Actions | Next States |
|---|---|---|---|
| Draft | Document created but not yet submitted for approval | Edit, Submit, Delete | Submitted |
| Submitted | Document submitted and awaiting first-level review | View, Recall | Pending Approval |
| Pending Approval | Awaiting action from the current-level approver | Approve, Reject, Return, Delegate | Approved, Rejected, Returned |
| Approved | All required approval levels have signed off | View, Audit | — |
| Rejected | Approval denied by an approver at any level | View, Resubmit, Audit | Draft |
| Returned | Returned for revision by an approver; requires resubmission | Edit, Resubmit | Draft, Submitted |
| Escalated | SLA breached; escalated to next-level authority | Approve, Reject, Return | Approved, Rejected, Returned |
| Delegated | Approval authority temporarily transferred to a delegate | Approve, Reject, Return | Approved, Rejected, Returned |
matrix_id — PK, configurable approval rules per modulemodule — target module (procurement, subcontractor, HR, etc.)document_type — PO, WO, MR, RA Bill, Change Order, etc.condition_field — field to evaluate (amount, project, category)condition_operator — =, >, <, BETWEEN, INcondition_value — threshold or match valuelevels — number of approval levels requiredid — PK, individual level config within a matrixmatrix_id — FK → master_data.approval_matrixlevel_number — sequence number (1, 2, 3, 4)approver_type — role, designation, or specific usermin_amount, max_amount — amount range for this levelauto_approve_days — auto-approve after N days if no actionwf_id — PK, master workflow templatewf_code — unique workflow codemodule — target moduledocument_type — document type this workflow handlestrigger_event — event that initiates the workflow (e.g., submitted)node_id — PK, individual node in the workflow graphwf_id — FK → workflow.workflow_definitionnode_type — approval, notification, condition, endaction_type — action required at this nodeapprover_config — JSON config for approver resolutiontimeout_hours — SLA hours before escalation triggeron_approve_node_id — FK → next node on approveon_reject_node_id — FK → next node on rejectinstance_id — PK, runtime instance tied to a specific documentwf_id — FK → workflow.workflow_definitiondocument_type — document typedocument_id — FK to the source documentcurrent_node_id — FK → workflow.workflow_nodestatus — pending, approved, rejected, returnedaction_id — PK, audit log of every approval actioninstance_id — FK → workflow.workflow_instancenode_id — FK → workflow.workflow_nodeactor_id — FK → user who performed the actionaction — approve, reject, return, delegatecomments — approver's remarkssla_met — boolean flag for SLA compliancerequest_id — PK, pending approval task assigned to an approverinstance_id — FK → workflow.workflow_instanceapprover_id — FK → assigned approver (or delegate)document_summary — quick-view summary for the approverdue_at — SLA deadline timestampstatus — pending, approved, rejected, returned, escalatedid — PK, escalation config per workflowwf_id — FK → workflow.workflow_definitionhours_before_escalation — hours after SLA breach to triggerescalate_to_type — next-level, manager, specific usermax_escalations — cap on escalation attemptsdelegation_id — PK, temporary delegation of approval authoritydelegator_id — FK → user delegating authoritydelegate_id — FK → user receiving authoritymodule_scope — modules covered by delegationvalid_from — delegation start datevalid_to — delegation end dateid — PK, queued notifications for approval eventsrecipient_id — FK → target usernotification_type — approval_request, escalation, delegation, completionreference_id — FK → source recordchannel — email, push, in-appsent_at, statusWhen a document (PO, WO, MR, RA Bill, Change Order, etc.) is submitted, the system creates a workflow_instance linked to the document. The instance references the matching workflow_definition and sets its initial status to pending.
The engine evaluates approval_matrix conditions — matching module, document type, and condition fields (e.g., amount ≥ ₹5,00,000, project = Highway). The condition_operator supports =, >, <, BETWEEN, and IN for flexible rule matching.
Based on the matched matrix, the system resolves required approval_level entries. Approvers are identified by role (e.g., Finance Head), designation (e.g., GM-Projects), or specific user ID. Delegation rules from admin.delegation are checked to substitute approvers on leave.
An approval_request is created for the current level's approver with a due_at timestamp calculated from the node's timeout_hours. The request includes a document_summary for quick review. Push and email notifications are queued.
The approver reviews the document and takes one of four actions: Approve, Reject, Return for Revision, or Delegate. Each action is recorded in workflow_action with comments and sla_met flag.
On Approve, if more levels remain, the engine follows on_approve_node_id to route to the next level. If this was the final level, the workflow instance status is set to approved and the source document status is updated accordingly.
A scheduled job (checkEscalation) runs periodically to identify overdue approval_request records. Per escalation_rule configuration, the system notifies the next-level approver, re-assigns the request, or auto-approves if auto_approve_days is set. Maximum escalation attempts are capped by max_escalations.
class WorkflowEngine { /** Initiate a new approval workflow for a submitted document */ async initiateWorkflow(documentType, documentId, submittedBy) { const wfDef = await WorkflowDefinition.findOne({ document_type: documentType, trigger_event: 'submitted' }); const matrix = await this.findApprovalMatrix(wfDef.module, documentType, documentId); const instance = await WorkflowInstance.create({ wf_id: wfDef.wf_id, document_type: documentType, document_id: documentId, current_node_id: wfDef.start_node_id, status: 'pending' }); // Route to first approval level await this.routeToApprover(instance.instance_id, 1); return instance; } /** Find the matching approval matrix based on module, type, and conditions */ async findApprovalMatrix(module, documentType, documentId) { const doc = await DocumentRegistry.findById(documentId); const matrices = await ApprovalMatrix.findAll({ module, document_type: documentType }); // Evaluate condition_field / condition_operator / condition_value for (const m of matrices) { if (this.evaluateCondition(doc[m.condition_field], m.condition_operator, m.condition_value)) { return m; } } throw new NoApprovalMatrixError(module, documentType); } /** Route the workflow instance to the approver at the given level */ async routeToApprover(instanceId, levelNumber) { const instance = await WorkflowInstance.findById(instanceId); const level = await ApprovalLevel.findOne({ matrix_id: instance.matrix_id, level_number: levelNumber }); const approver = await this.resolveApprover(level); // Check delegation — substitute if delegator is on leave const delegate = await Delegation.findActive(approver.id); const actualApprover = delegate ? delegate.delegate_id : approver.id; const request = await ApprovalRequest.create({ instance_id: instanceId, approver_id: actualApprover, document_summary: await this.buildSummary(instance), due_at: new Date(Date.now() + level.timeout_hours * 3600000), status: 'pending' }); await NotificationService.send(actualApprover, 'approval_request', request); return request; } /** Process an approver's action on a pending request */ async processAction(requestId, actorId, action, comments) { const request = await ApprovalRequest.findById(requestId); // Validate: cannot approve own submission const instance = await WorkflowInstance.findById(request.instance_id); if (instance.submitted_by === actorId) throw new SelfApprovalError(); const slaMet = new Date() <= request.due_at; await WorkflowAction.create({ instance_id: request.instance_id, node_id: instance.current_node_id, actor_id: actorId, action, comments, sla_met: slaMet }); if (action === 'approve') { const nextNode = await WorkflowNode.findById(instance.current_node_id); if (nextNode.on_approve_node_id) { await instance.update({ current_node_id: nextNode.on_approve_node_id }); await this.routeToApprover(instance.instance_id, instance.current_level + 1); } else { await instance.update({ status: 'approved' }); await DocumentService.updateStatus(instance.document_type, instance.document_id, 'approved'); } } else if (action === 'reject') { await instance.update({ status: 'rejected' }); await DocumentService.updateStatus(instance.document_type, instance.document_id, 'rejected'); } else if (action === 'return') { await instance.update({ status: 'returned' }); await DocumentService.updateStatus(instance.document_type, instance.document_id, 'draft'); } await request.update({ status: action }); } /** Scheduled job: check for overdue requests and escalate */ async checkEscalation() { const overdue = await ApprovalRequest.findAll({ status: 'pending', due_at: { lt: new Date() } }); for (const req of overdue) { const rule = await EscalationRule.findByWfId(req.wf_id); if (req.escalation_count >= rule.max_escalations) continue; const escalateTo = await this.resolveEscalationTarget(rule); await NotificationService.send(escalateTo, 'escalation', req); await req.update({ escalation_count: req.escalation_count + 1 }); } } /** Delegate a pending approval to another user */ async delegateApproval(requestId, delegateToUserId) { const req = await ApprovalRequest.findById(requestId); await req.update({ approver_id: delegateToUserId, status: 'pending' }); await NotificationService.send(delegateToUserId, 'delegation', req); } }
| Rule | Condition | Action |
|---|---|---|
| Self-Approval Block | Approver is the same as the document submitter | Block action, return error “Cannot approve own submission” |
| Delegation Date Range | Delegation valid_from / valid_to falls outside current date | Ignore delegation, route to original approver |
| SLA Breach Auto-Escalation | Approval request due_at has passed without action | Trigger escalation per escalation_rule configuration |
| Maximum 5 Levels | Approval matrix defines more than 5 levels | Reject matrix configuration, enforce max 5 levels |
| Parallel Unanimity | Parallel approval path with multiple approvers at same level | All approvers must approve; any rejection rejects the document |
| Minimum One Level | No approval level defined for a document type | Block submission, require at least one approval level in matrix |
| Event | Source | Auto Action |
|---|---|---|
| Document Submitted | Any transactional module | Create workflow_instance, route to L1 approver |
| Action Taken (Approve) | workflow.workflow_action | Advance to next node/level or complete workflow |
| Action Taken (Reject) | workflow.workflow_action | Set instance status to rejected, update document |
| Action Taken (Return) | workflow.workflow_action | Set instance status to returned, revert document to draft |
| SLA Breached | Scheduled checkEscalation job | Send escalation notification to next-level approver |
| All Levels Approved | workflow.workflow_instance | Update source document status to approved |
| Delegation Created | admin.delegation | Re-route all pending approval_requests to delegate |
| Delegation Expired | admin.delegation (valid_to passed) | Re-route pending requests back to original approver |
audit.audit_trailcondition_field = 'project_category' to differentiate highway vs. metro vs. building projectsvalid_from and valid_to — open-ended delegations create audit risksmodule_scope to specific modules rather than granting blanket delegationauto_approve_days sparingly and only for low-risk, low-value documents