Approval Workflows & Multi-Step Routing: Chat Prompts
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_approvalboolean 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:
- Drizzle schema + types
- Functions:
createWorkflowInstance(definitionId, entityId),recordApproval(stepInstanceId, userId, comment),recordRejection(...),escalateStep(...) - Each function writes to audit_log atomically with the state change
- 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:
- Name + description
- Trigger: what entity type + condition (e.g. "Expense reports where amount > $5000")
- 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:
- Specific user: return that user
- Role: return all users currently holding that role; for parallel steps, all; for serial, pick first by tenure or alphabetical
- Manager: look up initiator.managerId from HRIS or local user table
- Group membership: return all members of the named group
- 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_countdetermines 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 escalationon_timeout: 'escalate' | 'auto_approve' | 'auto_reject'- For escalation: who's the next-level approver?
Implementation:
- Cron job runs hourly: find step_instances pending > timeout_hours
- 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)
- escalate: create new step_instance for the next-level approver; mark current as
- 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:
- Resolve the canonical approver (e.g. manager)
- Check active delegations for that user
- If active delegation matches the workflow scope, route to delegate instead
- 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
versioninteger - On edit, increment version (don't mutate; create new row with same name + new version)
- Mark old version
inactive; new versionactive - workflow_instances stores
definition_idANDdefinition_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