Workflow Template Configuration¶
Overview¶
Workflow templates define the approval chains for declarations within an organisation. Each template specifies an ordered list of approvers who must sequentially approve a declaration before it reaches Approved status. Templates are stored in the workflow_templates table and are materialised into workflow_instances when a declaration is submitted.
Templates are managed via the Workflow Management page (/admin/workflows) and through GraphQL mutations. They are per-organisation and per-form-type: the unique key is (repId, formType) on the workflow_templates table.
Template Data Model¶
workflow_templates¶
| Column | Type | Description |
|---|---|---|
id |
Int (auto-increment) | Primary key |
formType |
String (default: "DECLARATION_FORM_3A") |
Which declaration type this template applies to |
repId |
String | Scoping key -- either an org-level sentinel (e.g. "ORG-dbs") or a rep-specific identifier |
organisationId |
Int? | FK to organisations table |
name |
String? | Human-readable name (e.g. "DBS Bank Ltd - Form 3A Approval Chain") |
approverUserIds |
Json | Ordered JSON array of user ID strings representing the approval chain |
The approverUserIds column defines the approval order. At materialisation time (WorkflowService.createInstance()), each array entry becomes a WorkflowStep row with stepIndex = array_position - 1.
workflow_instances¶
Created when a declaration is submitted. One instance per declaration (linked via declarationId).
| Column | Type | Description |
|---|---|---|
id |
Int | Primary key |
declarationId |
Int | FK to declarations |
repId |
String | Representative identifier |
templateId |
Int? | FK to the template that defined this instance |
status |
String (default: "pending") |
pending, approved, rejected, sent_back |
currentStepIndex |
Int (default: 0) | Which step in the chain is currently active |
workflow_steps¶
Individual approval steps within an instance.
| Column | Type | Description |
|---|---|---|
id |
Int | Primary key |
workflowInstanceId |
Int | FK to workflow_instances |
stepIndex |
Int | 0-based position in the chain |
approverUserId |
String | FK to users -- the assigned approver for this step |
approverName |
String? | Cached approver name at instance creation |
approverEmail |
String? | Cached approver email at instance creation |
status |
String (default: "pending") |
pending, approved, rejected, sent_back |
comment |
String? | Approver's comment when acting on the step |
actedAt |
DateTime? | When the approver acted |
Approver Rank Hierarchy¶
Approvers are assigned to users who have isApprover = true and an approvalRank value. The hierarchy:
| Rank | Role | Description |
|---|---|---|
| 1 | Representative (FI_USER) | Submits the declaration; NOT an approver but the first actor in the chain |
| 2 | Supervisor | First-line approver; must be same domain, higher approval_rank, and isApprover = true |
| 3 | Manager | Second-line approver |
| 4 | CEO | Highest approval rank |
This rank hierarchy is stored in the users table (approvalRank column) and applies to the reporting-officer relationship, not directly to workflow template configuration. Template approvers are pre-selected by ORG_ADMIN and do not need to follow strict rank ordering through the chain -- the rank is a user attribute, not a template constraint.
Sequential Approval Constraint¶
Workflow steps execute in strict order. Step N+1 cannot be acted on until step N is approved. This is enforced by WorkflowService.approveStep():
- Validates the instance is in
pendingstatus (not alreadyapprovedorrejected). - Validates
instance.currentStepIndex === stepIndex-- only the current step can be approved. - Validates the calling user matches the step's
approverUserId(unlessapproverUserIdis not provided, in which case this check is skipped). - Marks the step as
approved, setsactedAt. - If this is NOT the last step: advances
currentStepIndextostepIndex + 1, instance stayspending. - If this IS the last step: sets instance status to
approvedAND auto-transitions the declaration toApproved(status = "Approved",statusLabel = "APPROVED",approvedAt = now()).
Rejection path: rejectWorkflowStep marks the step as rejected, sets instance status to rejected, and auto-transitions the declaration to Rejected.
Send-back path: sendBackWorkflowStep marks the step as sent_back, sets instance status to sent_back, and auto-transitions the declaration to Revision Requested. The rep then revises and calls resubmitDeclaration, which resets all workflow steps to pending and restarts from stepIndex = 0 (prior approval history is preserved in actedAt / comment but all steps restart as pending).
Template CRUD Operations¶
createWorkflowTemplate¶
mutation CreateWorkflowTemplate($data: CreateWorkflowTemplateInput!) {
createWorkflowTemplate(data: $data) { id name formType approverUserIds }
}
Roles: SYS_ADMIN, ORG_ADMIN
updateWorkflowTemplate¶
mutation UpdateWorkflowTemplate($id: Int!, $data: UpdateWorkflowTemplateInput!) {
updateWorkflowTemplate(id: $id, data: $data) { id name approverUserIds }
}
Roles: SYS_ADMIN, ORG_ADMIN
deleteWorkflowTemplate¶
mutation DeleteWorkflowTemplate($id: Int!) {
deleteWorkflowTemplate(id: $id) { id }
}
Roles: SYS_ADMIN, ORG_ADMIN
Query templates¶
query WorkflowTemplates($where: JSONObject) {
workflowTemplates(where: $where) { id name formType organisationId approverUserIds }
}
No role guard on read queries -- templates are visible to all authenticated users for the org scope.
Scope Rule: New Declarations Only¶
Template changes apply ONLY to new declarations created after the change. In-flight declarations (those already submitted) retain their original WorkflowInstance with the approval chain that was materialised at submission time. The materialisation happens once, in DeclarationService.submitDeclaration(), which:
- Looks up the org's template for the declaration's
formType. - Creates a
WorkflowInstancewithstepspopulated from the template'sapproverUserIdsarray. - The instance is permanently bound to that declaration and is NOT updated when the template changes.
This is a one-way materialisation: template -> instance at submit time, no back-propagation.
Per-Declaration-Type Templates¶
Different templates can exist for different formType values:
| Form Type | Typical Approver Chain | Notes |
|---|---|---|
DECLARATION_FORM_3A |
Supervisor -> HR -> Compliance (3-step) | Standard Form 3A approval. Seed creates one per FI with sup + hr + comp as approvers. |
DECLARATION_ATTESTATION_QUARTERLY |
Compliance only (1-step, implicit) | Attestation declarations DO NOT auto-create workflow instances on submit. The single-step approval is via approveAttestation, not through the workflow engine. Attestation templates exist as FKs for the launch mutation but their approverUserIds are not used for workflow-step creation. |
DECLARATION_INCIDENT_DISCLOSURE |
Compliance only (1-step) | Auto-created by ensureIncidentDisclosureTemplate() on first filing. Single-step "Review by Compliance" with the org's first active COMP user as approver. Uses sentinel repId = "ORG-INCIDENT-{orgId}" for global uniqueness. |
Admin Page¶
The Workflow Management page at /admin/workflows provides:
- List of all templates for the org, grouped by form type
- Create new template (select form type, add approver steps)
- Edit existing template (reorder/change approvers)
- Delete unused templates
Escalation and Reassignment¶
If an approver is unavailable (on leave, left the organisation):
- There is currently no API mutation for reassigning an approver on an in-flight workflow instance.
- The available workaround is to reject the current step via rejectWorkflowStep and re-submit with a different template configured with an available approver.
- Direct database-level reassignment is technically possible but not recommended for production use.
Seed Data¶
Five Form 3A templates (one per FI), each with a 3-step chain:
- DBS: dbs_sup -> dbs_hr -> dbs_comp
- UOB: uob_sup -> uob_hr -> uob_comp
- Great Eastern: ge_sup -> ge_hr -> ge_comp
- Manulife: manulife_sup -> manulife_hr -> manulife_comp
- AIA: aia_sup -> aia_hr -> aia_comp
Plus attestation templates per org (created via seed). Template id=7 is a RESERVED FIXTURE for chatbot test AT-001, permanently anchored as the UOB Q2 2026 Attestation Template with repId = "ATTESTATION_TEMPLATE_UOB".
Chatbot Guidance¶
When users ask about:
- Adding/removing approvers: Direct to Workflow Management page (/admin/workflows), explain that changes apply only to new declarations.
- Changing approval order: Explain sequential constraint and that reordering requires updateWorkflowTemplate.
- "Why is my approval stuck?": Check workflowInstances for the declaration -- the currentStepIndex tells which step is pending. The assigned approver at that step must act.
- "Can I skip an approver?": No. Sequential approval is enforced. The only workaround is to update the template to remove that step, which only affects future declarations.