Docs menu

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_rule DEFAULT ‘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_type NOT NULL - the DB CHECK constraint was dropped; the application’s own document-type catalog is authoritative
  • is_active, created_at

approval_template_originators

Zero rows = applies to every originator (wildcard). One or more rows restricts to those users.

  • id, approval_template_id CASCADE, 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_id CASCADE, approval_stage_id, sequence_order - UNIQUE (approval_template_id, sequence_order)

The executable workflow graph

  • approval_workflow_nodes - id, approval_template_id CASCADE; node_type - CHECK IN (‘start’,‘amount_gate’,‘stage’,‘end_approved’,‘end_rejected’). 'stage' wraps an approval_stages row (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_id NULL; position_x/position_y DEFAULT 0 (canvas layout); created_at; index on template
  • approval_workflow_edges - id, approval_template_id CASCADE; from_node_id, to_node_id CASCADE; branch DEFAULT ‘default’ - CHECK IN (‘default’,‘yes’,‘no’); indexes on template and on from_node_id

approval_requests

One row per document that actually entered approval.

  • id, business_id
  • document_type NOT NULL; document_id uuid 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 forever
  • approval_template_id NOT NULL, originator_id NOT NULL
  • status DEFAULT ‘pending’ - CHECK IN (‘pending’,‘approved’,‘rejected’)
  • current_stage_order DEFAULT 1
  • cycle DEFAULT 1 - incremented when a post-approval edit restarts the workflow
  • resume_status DEFAULT ‘’ - the status to restore after a fresh cycle approves (e.g. ‘posted’); empty = first-ever approval
  • pending_payload jsonb NULL - the validated request body held until approval, for the no-draft-state document types
  • created_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_id CASCADE, cycle, stage_order, approval_stage_id NOT NULL
  • UNIQUE (approval_request_id, cycle, stage_order)

approval_request_decisions

  • id, approval_request_id CASCADE, cycle, stage_order, user_id NOT NULL
  • decision - 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