VibeWeek
Home/Grow/Approval Workflows & Multi-Step Routing: Chat Prompts

Approval Workflows & Multi-Step Routing: Chat Prompts

⬅️ Back to 6. Grow

The moment your product needs "the manager has to review this before it goes live," you've left simple-CRUD territory and entered the world of approval workflows. Common in: content publishing (drafts that need editorial review), expense management (purchase requests over $X needing manager sign-off), HR (PTO requests through chain-of-command), legal (contract redlines through legal review), enterprise admin (user-deletion requests requiring secondary approval), and B2B billing (invoices over $Y needing finance approval).

Naive implementation: a pending_approval boolean. Real implementation: a workflow engine that handles multi-step routes, conditional branching, parallel approvers, escalation timeouts, delegation when approvers are out, audit trail for compliance, and revocability when someone changes their mind.

This is the chat-prompt playbook for shipping an approval system that doesn't get rewritten 6 months later when product asks "can we add a third approval step depending on amount?"

When You Need a Real Workflow Engine

Use a workflow engine when:

  • 2+ approval steps required for a single action
  • Conditional logic ("if amount > $X, route to VP")
  • Parallel approvers (any one of N can approve; or all of N must approve)
  • Escalation needed (if approver doesn't respond in 48h, escalate)
  • Audit trail required for compliance
  • Approvals are core to the product (vs. an edge case)

Don't over-engineer when:

  • Single approver, single step (a pending_approval boolean works fine)
  • One-time use case
  • Pre-product-market fit (spec changes weekly)

Data Model

I'm building approval workflows for my SaaS. Help me design the data model.

Concepts:
- **Workflow Definition**: a template of steps (e.g., "Expense report > $5K needs Manager + Finance approval")
- **Workflow Instance**: a specific request flowing through a workflow
- **Step**: one node in the workflow (e.g., "Manager approval")
- **Step Instance**: a specific approver's pending or completed action on a request

Schema (Drizzle):

```sql
-- Workflow templates
workflow_definitions:
  id, name, description, version (int), entity_type (e.g. 'expense', 'content', 'user_deletion'),
  trigger_condition (JSON: rules for when this workflow applies),
  status (active / inactive), created_by, created_at

workflow_steps:
  id, definition_id, step_order, name, type (approval / notification / automation),
  approver_rule (JSON: who approves — by role, by user, by group, by formula),
  parallel_group (nullable; steps in same group are parallel),
  required_count (for parallel; 1 = any, N = all),
  timeout_hours (nullable; escalation timer),
  on_timeout (escalate / auto-approve / auto-reject),
  on_reject (cancel / send-back-to-prev-step)

-- Runtime instances
workflow_instances:
  id, definition_id, definition_version, entity_id, entity_type,
  initiator_id, status (pending / approved / rejected / cancelled), 
  current_step_id, started_at, completed_at, completed_by

step_instances:
  id, workflow_instance_id, step_id, assigned_to_user_id (nullable for parallel),
  status (pending / approved / rejected / skipped / timed_out),
  acted_by, acted_at, comment, escalated_at

approval_audit_log:
  id, workflow_instance_id, step_instance_id, event_type, actor_id, payload (JSON), created_at

Implement:

  1. Drizzle schema + types
  2. Functions: createWorkflowInstance(definitionId, entityId), recordApproval(stepInstanceId, userId, comment), recordRejection(...), escalateStep(...)
  3. Each function writes to audit_log atomically with the state change
  4. Versioning: when definition changes, in-flight instances continue with their original version

Stack: Next.js + Drizzle + Postgres.


## Defining a Workflow

Build the admin UI for defining workflows. Non-engineers should be able to author them.

Form structure:

  1. Name + description
  2. Trigger: what entity type + condition (e.g. "Expense reports where amount > $5000")
  3. Steps: ordered list with drag-to-reorder
    • Each step: name, approver rule, parallel-group, required-count, timeout, on-timeout action

Approver rules:

  • Specific user: pick from team
  • Role: e.g. "Finance Lead" (resolved per-instance to current holder)
  • Manager: the initiator's direct manager (resolved at instance time)
  • Manager's manager
  • Group: any user in a defined group
  • Formula: dynamic resolution (e.g. "department lead of initiator's department")

Output:

  • Form using react-hook-form + zod
  • Drag-drop with dnd-kit
  • Preview pane showing the workflow as a flowchart (using ReactFlow or similar)

Stack: Next.js + react-hook-form + zod + dnd-kit + shadcn/ui.


## Routing Logic: Resolving the Right Approver

When a workflow instance starts, I need to resolve who specifically gets the approval task at each step.

Implement resolveApprovers(stepDefinition, workflowContext):

  • Input: step definition + workflow instance context (initiator, entity, etc.)
  • Output: array of user IDs to assign

Cases:

  1. Specific user: return that user
  2. Role: return all users currently holding that role; for parallel steps, all; for serial, pick first by tenure or alphabetical
  3. Manager: look up initiator.managerId from HRIS or local user table
  4. Group membership: return all members of the named group
  5. Formula: evaluate the expression (e.g. "department(initiator).lead")

Edge cases:

  • Approver same as initiator (e.g. CEO submitting for approval) → auto-approve or escalate to board?
  • Approver no longer at company → use delegate or fall through to manager
  • No qualifying approvers → instance fails with explicit error; admin notified

Implement with TypeScript types that catch impossible role-rule combinations at compile time.

Stack: Next.js + TypeScript.


## Parallel vs. Serial Approval

Two patterns:

Serial: Step 1 completes → Step 2 starts → Step 3 starts. Linear chain.

Parallel: Multiple approvers act on the same step simultaneously.

  • "Any one approves": fastest path; e.g. "any director on a finance team can approve"
  • "All must approve": consensus required; e.g. "both engineering lead and security lead must sign off"

Build both behaviors:

Serial implementation:

  • step_instance is created for current step's approver
  • On approval, advance to next step; create next step_instance
  • On rejection, mark instance as rejected (or send back to prev step depending on on_reject)

Parallel implementation:

  • Multiple step_instances created at same time, sharing parallel_group
  • required_count determines how many approvals advance the workflow
  • If required_count = 1: first approval cancels remaining pending instances; advances workflow
  • If required_count = N: track approvals; advance when N reached
  • Any rejection (in some configurations) cancels the whole step

Show me both implementations + the test cases.

Stack: Next.js + Drizzle.


## Escalation & Timeouts

Approvers don't always respond. Implement escalation:

Per-step config:

  • timeout_hours: how long before escalation
  • on_timeout: 'escalate' | 'auto_approve' | 'auto_reject'
  • For escalation: who's the next-level approver?

Implementation:

  1. Cron job runs hourly: find step_instances pending > timeout_hours
  2. For each: apply on_timeout action
    • escalate: create new step_instance for the next-level approver; mark current as escalated
    • auto_approve: mark approved (with system actor)
    • auto_reject: mark rejected (with system actor)
  3. Notify the original approver they missed it; notify escalation recipient that they have a new task

Edge cases:

  • Approver was OOO (out-of-office) — should respect their delegation if set
  • Approver responded right when escalation fired — race condition; first writer wins
  • Escalation cascading 3+ levels — cap at 2-3 escalations to avoid runaway

Stack: Next.js + Vercel Cron + Drizzle.


## Delegation (Out-of-Office)

Approvers go on vacation. They should be able to delegate.

User-level setting: delegations table: user_id (delegator), delegate_user_id, scope ('all' | 'category:expense'), starts_at, ends_at

When resolving approvers:

  1. Resolve the canonical approver (e.g. manager)
  2. Check active delegations for that user
  3. If active delegation matches the workflow scope, route to delegate instead
  4. Audit log notes the delegation routing

UX:

  • "/settings/delegations" page where user sets up delegations
  • "X is acting as approver on behalf of Y" badge in approval UI
  • Email notifications go to delegate, not delegator

Stack: Next.js + Drizzle.


## Approval UX: For the Approver

When someone has a pending approval task, build the UX:

Inbox surface:

  • "/approvals" page listing all pending tasks for current user
  • Sortable / filterable by entity type, age, urgency
  • Each task: summary card with key context + Approve/Reject buttons + comment

Detail view:

  • Full context of the request (who initiated, what entity, attachments, prior approvals in the workflow)
  • Approval / rejection form with optional comment (required for rejection)
  • Timeline of the workflow (which steps done; what's next)
  • "Delegate" button (single-instance delegation; routes this one task to someone else)

Actions:

  • Approve → record approval; advance workflow
  • Reject → record rejection; usually cancels (or sends back); requires reason
  • Request more info → comment back to initiator without acting (creates a back-and-forth thread; doesn't advance)
  • Delegate → route to a single-time delegate

Mobile-friendly: approvals often come in via email/Slack; users tap a link on phone; experience must work.

Email/Slack notifications:

  • "[Name] requested your approval for [thing]"
  • Approve/Reject buttons inline (deep-link with secure token)
  • Reminder if not acted in 24h

Stack: Next.js + shadcn/ui + Resend (email) + Slack webhook.


## Approval UX: For the Initiator

When you submit a request through a workflow, you should see its progress.

Status surface:

  • "/my-requests" listing all in-flight + completed instances
  • Per-instance: progress bar showing which step is current
  • Timeline view: who acted when; pending steps with assigned approvers
  • "Cancel this request" button (cancels in-flight workflow if state allows)

Notifications:

  • Email/Slack on each step transition
  • "Your request was approved by X" / "rejected by X with reason: ..."

Edge cases:

  • Approver is on vacation; show delegate explicitly
  • Request stalled at step 2 for 3 days — let initiator nudge approver (sends a reminder)
  • Reject + resubmit: should rejected requests be re-openable, or always start a new instance?

Stack: Next.js + TanStack Query.


## Conditional Routing: Branching by Amount / Type

Real workflows have conditional branches. Examples:

  • "Expense > $5K → goes to VP; otherwise just manager"
  • "Content tagged 'sensitive' → goes to Legal first; otherwise editorial only"
  • "User from EU → triggers GDPR review; from US → standard"

Build conditional routing:

Step definition extension:

  • condition (JSON expression): when to apply this step
  • Example: { field: 'entity.amount', operator: '>', value: 5000 }
  • More complex: AND/OR/NOT combinations

When advancing the workflow, evaluate each step's condition against the workflow context. Skip steps where condition is false.

Implement:

  • Condition evaluation engine (use a safe expression evaluator, NOT eval())
  • Test conditions: simple comparisons, AND/OR groupings, field references, function calls (e.g. daysSince(entity.createdAt) > 30)

Don't roll a full DSL — match the constraints to your real conditions and stop. Most workflows need: comparison operators, AND/OR, field access, basic functions.

Stack: Next.js + json-logic-js or your safe-expression library.


## Audit Trail (For Compliance)

For SOC 2 / regulated industries, every approval action must be auditable.

Audit log entries (cannot be deleted; append-only):

approval_audit_log:

  • workflow_instance_id, step_instance_id
  • event_type: instance_created / step_advanced / approval_recorded / rejection_recorded / escalation_triggered / delegation_invoked / workflow_completed / workflow_cancelled
  • actor_user_id (or 'system' for automated events)
  • timestamp (server time, immutable)
  • payload (JSON: comment, prior values, IP address, user agent)

Surface:

  • Per-instance audit log view (timestamped feed of events)
  • Export to CSV for SOC 2 auditors
  • Tamper-evidence: hash the previous log entry into the next; auditor can verify chain integrity

Retention:

  • Per regulatory requirements (often 7 years for financial)
  • Older entries can be moved to cold storage but never deleted

Stack: Next.js + Drizzle + Postgres (consider an immutable append-only table approach: revoke DELETE/UPDATE permissions in the DB role).


## Workflow Versioning

Workflow definitions evolve. In-flight instances need to honor their original version, not the current.

Pattern:

  • workflow_definitions has version integer
  • On edit, increment version (don't mutate; create new row with same name + new version)
  • Mark old version inactive; new version active
  • workflow_instances stores definition_id AND definition_version_at_start
  • Step resolution + workflow advancement uses the version-locked definition

Migration:

  • "Migrate in-flight to new version" admin action — explicitly opts in; logs the migration in audit
  • Default: in-flight stays on old version

Stack: Next.js + Drizzle.


## Existing Tools vs. DIY

Many SaaS reach for an off-the-shelf workflow engine instead of building.

| Tool | Use When |
|---|---|
| **Temporal / Inngest / Trigger.dev** | Programmatic workflows; you control the steps in code |
| **Zapier / Make / n8n** | Cross-system workflows linking external apps |
| **Cadence / Camunda** | Heavy enterprise BPM workflows |
| **DIY (this guide)** | In-product approvals deeply tied to your data model |

For approvals on your own product's data, DIY is usually right — tools above are overkill or wrong shape. For cross-system orchestration (form → CRM → email → Slack), use Zapier / Make. For durable code execution (long-running async jobs), use Inngest / Trigger.dev. Workflow tools are for *workflows*; this guide is for *approvals* — overlapping but distinct.

## Common Pitfalls

**Hardcoded `pending_approval` booleans.** Won't scale past one workflow type. Use the data model.

**No versioning.** Workflow change retroactively breaks in-flight instances. Always version-lock.

**Race conditions on parallel approval.** Two approvers click "approve" simultaneously; both succeed; both notify the system; double-advancement. Use database transactions + version checks.

**Missing escalation.** Approver goes on vacation; the workflow sits forever. Always set timeouts + escalation actions.

**No delegation mechanism.** OOO approvers create bottlenecks. Build delegations.

**Email-only approval flow.** Mobile users can't approve from email if your link doesn't work on mobile. Test mobile.

**Approver same as initiator.** CEO submits an expense; the workflow routes to "CEO's manager" — there is none. Handle gracefully: auto-escalate to board chair, or auto-approve with audit trail flagging.

**No "request more info" path.** Approver has questions; their only options are Approve / Reject. Build a back-channel for clarification that doesn't advance the workflow.

**Audit log mutable.** A bad actor can edit the audit log. Use append-only tables; revoke UPDATE/DELETE in DB role.

**No way to cancel.** Initiator regrets their request; can't cancel mid-flight. Allow cancellation at certain states.

**Conditional logic written in unsafe eval().** Workflow conditions evaluated via JS eval()? Hello security hole. Use json-logic-js or a safe expression evaluator.

**Workflow definitions edited mid-run without notice.** In-flight instances confused about their state. Lock to version.

**Step transitions logged inconsistently.** Some events written to audit, some not. Centralize the audit-write through a wrapper that ensures it happens atomically.

**Notifications spam approvers.** "X needs your approval" → approver delays → "Reminder: X needs your approval" → "Reminder 2: ..." → "Reminder 3: ..." until they hate you. Cap reminders; smart escalation.

**No SLA visibility.** Initiator doesn't know how long approval typically takes. Show "Average approval time: 14h" so they know what to expect.

**Approval queue without prioritization.** Approver has 50 pending tasks; doesn't know what's urgent. Surface urgency / age / amount / requester.

**Forgetting permission checks.** Anyone with the URL can approve someone else's task. Always verify the actor matches the assigned step approver (or a valid delegate).

**Delegation chains untraced.** A delegates to B; B delegates to C; the audit log shows C approved without A's context. Trace delegation chain.

## See Also

- [Roles & Permissions](./roles-permissions-chat.md) — RBAC underlying approvers
- [Audit Logs](./audit-logs-chat.md)
- [Multi-Tenancy](./multi-tenancy-chat.md)
- [Workspace / Tenant Switcher](./workspace-tenant-switcher-chat.md)
- [Activity Feed / Timeline Implementation](./activity-feed-timeline-implementation-chat.md)
- [Notifications: In-App](./in-app-notifications-chat.md)
- [Notification Preferences & Unsubscribe](./notification-preferences-unsubscribe-chat.md)
- [Settings & Account Pages](./settings-account-pages-chat.md)
- [Internal Admin Tools](./internal-admin-tools-chat.md)
- [Background Jobs & Queue Management](./background-jobs-queue-management-chat.md) — for escalation timers
- [Email Template Implementation](./email-template-implementation-chat.md) — approval-action emails
- [Optimistic UI Updates](./optimistic-ui-updates-chat.md)
- [Form Validation UX](./form-validation-ux-chat.md)
- [Form Autosave & Draft Persistence](./form-autosave-draft-persistence-chat.md)
- [Multi-Step Forms / Wizards](./multi-step-forms-wizards-chat.md)
- [In-Product Workflow Automation Builder](./in-product-workflow-automation-builder-chat.md)
- [Diff Views & Change Tracking UI](./diff-views-change-tracking-ui-chat.md) — for content review workflows
- [Account Suspension & Fraud Holds](./account-suspension-fraud-holds-chat.md) — overlap with admin approval flows
- [Inline Editing Patterns](./inline-editing-patterns-chat.md)
- [In-App Status Banners & System Notifications](./in-app-status-banners-system-notifications-chat.md)
- [Toast Notifications UI](./toast-notifications-ui-chat.md)
- [Tooltip & Hint Systems](./tooltip-hint-systems-chat.md)
- [Sidebar Navigation Implementation](./sidebar-navigation-implementation-chat.md)
- [Workflow Automation Providers (VibeReference)](https://viberef.dev/devops-and-tools/workflow-automation-providers.md) — Zapier / Inngest / Trigger.dev