Approvals & Workflow
Read Conventions first.
Opt-in: a document only goes through approval if a matching active template exists for its type and originator.
approval_stages
A named group of approvers plus a rule for how many must approve.
id,business_id,name- UNIQUE (business_id, name)approval_ruleDEFAULT ‘any_one’ - CHECK IN (‘any_one’,‘all_required’)created_at
approval_stage_approvers
id, approval_stage_id CASCADE, user_id - UNIQUE (approval_stage_id, user_id)
approval_templates
id,business_id,name- UNIQUE (business_id, name)document_typeNOT NULL - the DBCHECKconstraint was dropped; the application’s own document-type catalog is authoritativeis_active,created_at
approval_template_originators
Zero rows = applies to every originator (wildcard). One or more rows restricts to those users.
id,approval_template_idCASCADE,user_id- UNIQUE (approval_template_id, user_id)
approval_template_stages
The original strictly-linear stage chain. Superseded by the node/edge graph below but deliberately left in place, unused, as a historical artifact.
id,approval_template_idCASCADE,approval_stage_id,sequence_order- UNIQUE (approval_template_id, sequence_order)
The executable workflow graph
- approval_workflow_nodes -
id,approval_template_idCASCADE;node_type- CHECK IN (‘start’,‘amount_gate’,‘stage’,‘end_approved’,‘end_rejected’).'stage'wraps anapproval_stagesrow (required iff node_type = ‘stage’).'amount_gate'is zero-config: its branch is always “does this amount exceed the originating user’s own effective limit” - never a fixed number stored on the node.'end_rejected'is decorative only (rejection is instant/global, never a real edge target);'end_approved'is the one real terminus.approval_stage_idNULL;position_x/position_yDEFAULT 0 (canvas layout);created_at; index on template - approval_workflow_edges -
id,approval_template_idCASCADE;from_node_id,to_node_idCASCADE;branchDEFAULT ‘default’ - CHECK IN (‘default’,‘yes’,‘no’); indexes on template and onfrom_node_id
approval_requests
One row per document that actually entered approval.
id,business_iddocument_typeNOT NULL;document_iduuid NULL - NULL while pending for document types with no draft state (Incoming/Outgoing Payment, A/R Credit Memo); the real document isn’t created until approval completes, and a rejected request leaves it NULL foreverapproval_template_idNOT NULL,originator_idNOT NULLstatusDEFAULT ‘pending’ - CHECK IN (‘pending’,‘approved’,‘rejected’)current_stage_orderDEFAULT 1cycleDEFAULT 1 - incremented when a post-approval edit restarts the workflowresume_statusDEFAULT ‘’ - the status to restore after a fresh cycle approves (e.g. ‘posted’); empty = first-ever approvalpending_payloadjsonb NULL - the validated request body held until approval, for the no-draft-state document typescreated_at,completed_at- UNIQUE (document_type, document_id); index
(document_type, document_id)
approval_request_stages
The stage order actually resolved for one submission, snapshotted at submit time by walking the graph – so editing a template never changes an in-flight request.
id,approval_request_idCASCADE,cycle,stage_order,approval_stage_idNOT NULL- UNIQUE (approval_request_id, cycle, stage_order)
approval_request_decisions
id,approval_request_idCASCADE,cycle,stage_order,user_idNOT NULLdecision- CHECK IN (‘approved’,‘rejected’)remarks,decided_at- UNIQUE (approval_request_id, cycle, stage_order, user_id) - the cycle in the key is what lets the same approver decide again in a later cycle
approval_field_exclusions
Fields exempt from re-triggering approval when edited (e.g. Remarks, Due Date). Existence of a row = excluded; no boolean flag.
id,business_id,document_type,field_name- UNIQUE (business_id, document_type, field_name)created_at