Back to ER Diagram
Approval Workflows

Approval Workflows Logic

Suthradhaari multi-level approval engine with dynamic routing, escalation, and delegation

PostgreSQL
10 Tables
Schema: workflow
SLA Tracking

Overview

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.

10
Workflow Tables
Multi-Level
Approval Routing
SLA
Tracking & Escalation
All Modules
Transactional Coverage

Status States

StatusDescriptionAllowed ActionsNext States
DraftDocument created but not yet submitted for approvalEdit, Submit, DeleteSubmitted
SubmittedDocument submitted and awaiting first-level reviewView, RecallPending Approval
Pending ApprovalAwaiting action from the current-level approverApprove, Reject, Return, DelegateApproved, Rejected, Returned
ApprovedAll required approval levels have signed offView, Audit
RejectedApproval denied by an approver at any levelView, Resubmit, AuditDraft
ReturnedReturned for revision by an approver; requires resubmissionEdit, ResubmitDraft, Submitted
EscalatedSLA breached; escalated to next-level authorityApprove, Reject, ReturnApproved, Rejected, Returned
DelegatedApproval authority temporarily transferred to a delegateApprove, Reject, ReturnApproved, Rejected, Returned

Document Approval Lifecycle


Draft

Submitted

Pending Approval

Approved

Rejected

Returned

Escalated

Delegated

Approval Levels

L1 — Department Head First-level review & initial sign-off
L2 — Project Manager Project-level validation & budget confirmation
L3 — Finance Head Financial compliance & fund availability check
L4 — Director Final authority for high-value or exception approvals

Routing Modes

  • Serial Path: L1 → L2 → L3 → L4 (sequential sign-off)
  • Parallel Path: L1a + L1b + L1c (all must approve at the same level)

Process Flow


Document
Submitted
(PO/WO/MR/etc)

Find Approval
Matrix
(module + type + conditions)

Determine
Level(s)
(L1→L2→L3→L4)

Route to
Approver
(or delegate)

Wait for
Action
(SLA tracked)

Approve

Reject

Return

Delegate

Timeout

Next Level?
Yes → Route L+
No → COMPLETE
  

Escalate
(per escalation_rule config)

Database Schema

master_data.approval_matrix

  • matrix_id — PK, configurable approval rules per module
  • module — 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, IN
  • condition_value — threshold or match value
  • levels — number of approval levels required

master_data.approval_level

  • id — PK, individual level config within a matrix
  • matrix_id — FK → master_data.approval_matrix
  • level_number — sequence number (1, 2, 3, 4)
  • approver_type — role, designation, or specific user
  • min_amount, max_amount — amount range for this level
  • auto_approve_days — auto-approve after N days if no action

workflow.workflow_definition

  • wf_id — PK, master workflow template
  • wf_code — unique workflow code
  • module — target module
  • document_type — document type this workflow handles
  • trigger_event — event that initiates the workflow (e.g., submitted)

workflow.workflow_node

  • node_id — PK, individual node in the workflow graph
  • wf_id — FK → workflow.workflow_definition
  • node_type — approval, notification, condition, end
  • action_type — action required at this node
  • approver_config — JSON config for approver resolution
  • timeout_hours — SLA hours before escalation trigger
  • on_approve_node_id — FK → next node on approve
  • on_reject_node_id — FK → next node on reject

workflow.workflow_instance

  • instance_id — PK, runtime instance tied to a specific document
  • wf_id — FK → workflow.workflow_definition
  • document_type — document type
  • document_id — FK to the source document
  • current_node_id — FK → workflow.workflow_node
  • status — pending, approved, rejected, returned

workflow.workflow_action

  • action_id — PK, audit log of every approval action
  • instance_id — FK → workflow.workflow_instance
  • node_id — FK → workflow.workflow_node
  • actor_id — FK → user who performed the action
  • action — approve, reject, return, delegate
  • comments — approver's remarks
  • sla_met — boolean flag for SLA compliance

workflow.approval_request

  • request_id — PK, pending approval task assigned to an approver
  • instance_id — FK → workflow.workflow_instance
  • approver_id — FK → assigned approver (or delegate)
  • document_summary — quick-view summary for the approver
  • due_at — SLA deadline timestamp
  • status — pending, approved, rejected, returned, escalated

workflow.escalation_rule

  • id — PK, escalation config per workflow
  • wf_id — FK → workflow.workflow_definition
  • hours_before_escalation — hours after SLA breach to trigger
  • escalate_to_type — next-level, manager, specific user
  • max_escalations — cap on escalation attempts

admin.delegation

  • delegation_id — PK, temporary delegation of approval authority
  • delegator_id — FK → user delegating authority
  • delegate_id — FK → user receiving authority
  • module_scope — modules covered by delegation
  • valid_from — delegation start date
  • valid_to — delegation end date

workflow.notification_queue

  • id — PK, queued notifications for approval events
  • recipient_id — FK → target user
  • notification_type — approval_request, escalation, delegation, completion
  • reference_id — FK → source record
  • channel — email, push, in-app
  • sent_at, status

Step-by-Step Logic

1

Workflow Instance Creation

When 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.

2

Approval Matrix Evaluation

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.

3

Identify Approval Levels & Approvers

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.

4

Create Approval Request

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.

5

Approver Action

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.

6

Advance or Complete Workflow

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.

7

Timeout & Escalation

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.

Code Implementation

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);
  }
}

Validation Rules

RuleConditionAction
Self-Approval BlockApprover is the same as the document submitterBlock action, return error “Cannot approve own submission”
Delegation Date RangeDelegation valid_from / valid_to falls outside current dateIgnore delegation, route to original approver
SLA Breach Auto-EscalationApproval request due_at has passed without actionTrigger escalation per escalation_rule configuration
Maximum 5 LevelsApproval matrix defines more than 5 levelsReject matrix configuration, enforce max 5 levels
Parallel UnanimityParallel approval path with multiple approvers at same levelAll approvers must approve; any rejection rejects the document
Minimum One LevelNo approval level defined for a document typeBlock submission, require at least one approval level in matrix

Automated Actions & Triggers

EventSourceAuto Action
Document SubmittedAny transactional moduleCreate workflow_instance, route to L1 approver
Action Taken (Approve)workflow.workflow_actionAdvance to next node/level or complete workflow
Action Taken (Reject)workflow.workflow_actionSet instance status to rejected, update document
Action Taken (Return)workflow.workflow_actionSet instance status to returned, revert document to draft
SLA BreachedScheduled checkEscalation jobSend escalation notification to next-level approver
All Levels Approvedworkflow.workflow_instanceUpdate source document status to approved
Delegation Createdadmin.delegationRe-route all pending approval_requests to delegate
Delegation Expiredadmin.delegation (valid_to passed)Re-route pending requests back to original approver

Integration Points

Transactional Modules (Consumers)

  • Purchase Orders (PO) — approval before commitment
  • Work Orders (WO) — multi-level for high-value subcontracts
  • Material Requests (MR) — site-level to procurement routing
  • RA Bills — measurement verification + finance sign-off
  • Change Orders — scope change with budget impact review
  • Leave & Advance Requests — HR workflow with manager approval
  • Budget Revisions — finance head + director approval
  • Journal Entries — manual JE approval before posting

Connected Systems

  • notification_queue — email + push alerts for pending approvals, escalations, completions
  • Gamification Engine — approval speed scoring (fast approvers earn points)
  • Audit Trail — all workflow actions logged in audit.audit_trail
  • Dashboard Analytics — pending approval counts, SLA compliance rates
  • Mobile App — approve/reject from mobile with biometric confirmation

Best Practices

Matrix Configuration Tips

  • Define amount-based tiers: < ₹1L (L1 only), ₹1L–10L (L1+L2), ₹10L–1Cr (L1+L2+L3), > ₹1Cr (all 4 levels)
  • Use condition_field = 'project_category' to differentiate highway vs. metro vs. building projects
  • Keep matrix rules simple — prefer fewer conditions with more levels over complex condition trees
  • Review and audit approval matrices quarterly to reflect organisational changes

Delegation Best Practices

  • Always set valid_from and valid_to — open-ended delegations create audit risks
  • Limit module_scope to specific modules rather than granting blanket delegation
  • Notify both delegator and delegate via email when delegation is activated
  • Auto-expire delegations and re-route pending requests back to the original approver

SLA Calibration

  • Set realistic SLAs: 24h for L1, 48h for L2, 72h for L3/L4 as a starting baseline
  • Monitor SLA compliance weekly — consistently breached SLAs indicate approver overload
  • Use auto_approve_days sparingly and only for low-risk, low-value documents
  • Escalation should notify, not bypass — preserve accountability at each level

Avoiding Approval Bottlenecks

  • Do not route all documents to a single approver — distribute by project or department
  • Identify “approval hoarders” using dashboard analytics and redistribute workload
  • Use parallel approvals at the same level for cross-functional reviews (e.g., Finance + QA)
  • Avoid more than 3 levels for routine documents — reserve L4 for exceptions and high-value items
  • Implement “bulk approve” for repetitive low-value approvals to reduce fatigue