Appearance
ADR-008 — Approval Request Lifecycle
Status: Accepted
Date: 2026-05-21
Decision
Schema
approval_requests
────────────────────────────────────────────────
id uuid
tenant_id → tenants.id
type 'reply' | 'refund' | 'cancel' | 'note' | 'resolve' | 'assign'
channel 'chatwoot' | 'freshchat' | null
conversation_id string (channel-side conversation ID, nullable)
payload jsonb — full action parameters
drafted_by 'agent'
status pending | approved | rejected | failed
approved_by → users.id (null until actioned)
actioned_at timestamp (null until approved or rejected)
executed_at timestamp (null until ExecuteApprovalJob completes)
error text (null unless status = failed)
expires_at timestampState Machine
┌─ approved ──→ ExecuteApprovalJob ──→ executed_at set
pending ──────┤ └──→ status = failed, error set
├─ rejected (no job fired)
└─ [expires_at < now] → auto-rejected by schedulerExpiry
24 hours. A pending approval older than 24 hours is auto-rejected. A reply stale by a day should not be sent — the conversation context will have changed.
Notifications
When the agent creates a pending approval:
- The draft reply is posted as an internal note on the Chatwoot conversation (visible to agents only, not the customer)
- The Opsome approval dashboard shows the pending queue
No email or SMS notifications in v1. The internal note acts as the push notification to the store owner who is already working in Chatwoot.
Consequences
- A scheduler job runs periodically to expire stale approvals
ExecuteApprovalJobchecksstatus === pendingbefore executing to prevent double-execution- Failed executions set
status = failed+errortext; the approval can be retried manually from the dashboard