Approval Flow
The complete lifecycle of a HITL approval — from the moment it is created, through notification delivery, to how the human’s decision propagates downstream.
See also: HITL Index — all touchpoints | UI Patterns — how approvals are displayed | Governance — DB schema + resolution code
Full Lifecycle
Agent completes task / hits decision point / write-tool intercepted
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 1. APPROVAL CREATED │
│ approvals record inserted (status: pending) │
│ linked activities set to status: awaiting_approval │
│ expiry window set based on risk level │
└───────────────────────────┬───────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 2. NOTIFICATIONS SENT │
│ Web push (in-app bell) — immediate │
│ Push notification (mobile) — immediate │
│ Email — immediate (medium/high risk) or batched (low) │
│ SMS — high risk only │
│ WhatsApp — high risk only (Agency/Enterprise) │
└───────────────────────────┬───────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 3. HUMAN REVIEWS │
│ Approval visible in DM Portal · Dashboard · Mobile · CLI │
│ Reviewer sees: content, validator results, risk level │
│ Reviewer can: approve · reject · edit+approve · options │
└───────────────────────────┬───────────────────────────────────┘
│
┌────────────┴────────────┐
▼ ▼
APPROVED REJECTED
│ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────────┐
│ 4a. DOWNSTREAM — APPROVE │ │ 4b. DOWNSTREAM — REJECT │
│ All linked activities │ │ All linked activities │
│ re-enqueued with │ │ re-enqueued with │
│ wakeReason: approved │ │ wakeReason: feedback + │
│ + reviewer notes │ │ reviewer notes appended │
│ │ │ to agent prompt │
│ Channel action approvals │ │ │
│ → external API call │ │ content_review edit+approve │
│ executes immediately │ │ → edited content saved as │
└──────────────────────────┘ │ final output; no retry │
└──────────────────────────────┘Step 1 — Approval Created
System-created approvals (content_review, channel_action)
Triggered automatically by the platform when:
- An agent deliverable completes and passes output validators →
content_reviewcreated - A write-tool call is intercepted (e.g.
wordpress.createPost) →channel_actioncreated
The system sets expiry based on risk:
| Risk level | Expiry window |
|---|---|
| low | 72 hours |
| medium | 48 hours |
| high | 24 hours |
| urgent (campaign deadline within 6h) | 6 hours |
Agent-created approvals (content_direction, brand_direction, strategy_change, budget_authorization)
An agent mid-run uses the create_approval tool when it hits a decision it cannot resolve:
Agent encounters ambiguity
→ calls create_approval({ type, title, description, options?, linkedActivityIds })
→ approval record created
→ calling activity + all linked activities → status: awaiting_approval
→ agent's run exits cleanly (does not block or poll)The agent does not wait. It exits with a clean status and the BullMQ job completes. The linked activities remain in awaiting_approval state until the human resolves.
Step 2 — Notifications Sent
Immediately after an approval is created, the notifications service enqueues delivery jobs for every assigned reviewer.
Notification matrix by channel and risk
| Channel | low risk | medium risk | high risk |
|---|---|---|---|
| In-app bell (web SSE) | Yes — immediate | Yes — immediate | Yes — immediate |
| Mobile push | Yes — immediate | Yes — immediate | Yes — immediate |
| Batched (daily digest) | Yes — immediate | Yes — immediate | |
| SMS | No | No | Yes — if enabled |
| No | No | Yes — Agency/Enterprise |
Notification content
Each notification carries enough context for the reviewer to triage without opening the app:
[high risk] Email campaign ready for approval — Globex Corp
"Q2 Newsletter — April 2026" is ready for your review.
This is an email campaign (irreversible once sent). Approval expires in 24 hours.
[Review now →]For mobile push:
🔴 High-risk approval — Globex Corp
Q2 Newsletter ready. Tap to review.Notification routing
Notifications are sent to:
- All DM reviewers assigned to the tenant
- The tenant admin — for
strategy_change,budget_authorization, andcontent_reviewapprovals where the tenant has enabled “notify me on all approvals” in settings - Super admins — never notified automatically (they use
/status --allin the CLI or the Manage app)
If an approval is not actioned within 24h of expiry, a second escalation notification is sent to all reviewers marked urgent.
Batching (low risk only)
Low-risk content_review approvals (e.g. blog post drafts) are batched into a single daily digest email sent at 09:00 tenant-local time:
Subject: 4 items ready for your review — Acme Corp
Blog posts (3):
- "Why Local SEO Matters"
- "Top 5 Plumbing Tips"
- "GBP Profile Optimisation Guide"
Social posts (1):
- LinkedIn — Week 14
[Open approvals queue →]This prevents inbox flooding for high-volume content tenants.
Step 3 — Human Reviews
The reviewer sees the approval in their queue. Depending on the approval type, they can take different actions:
Available actions by approval type
| Approval type | Approve | Reject with feedback | Edit + approve | Choose option |
|---|---|---|---|---|
content_review | Yes | Yes | Yes (edit inline) | No |
content_direction | Yes | Yes | No | Yes (if options provided) |
brand_direction | Yes | Yes | No | Yes (if options provided) |
strategy_change | Yes | Yes | No | No |
budget_authorization | Yes (authorise spend) | Yes (decline) | No | No |
channel_action | Yes (execute action) | Yes (cancel action) | No | No |
Reviewer notes
Every decision — approve or reject — accepts an optional notes field. For rejections, notes are mandatory (enforced in the UI). The notes are appended to the agent’s prompt on retry so the agent understands what to change.
Good rejection note: "Title is too generic — needs a Brisbane-specific angle. Rewrite the intro to open with the Brisbane summer heat as the hook."
Poor rejection note: "Not great" — the UI shows a warning if notes are fewer than 20 characters on a rejection.
Dual approval (email campaigns, ad pushes)
Some approval types require sign-off from both a DM reviewer and the tenant admin:
Approval: "Q2 Newsletter — April 2026"
Type: email_campaign (content_review, risk: high)
Required sign-offs:
✅ DM Reviewer: Sarah K. — approved Apr 4, 09:14
⏳ Tenant Admin: pending
Gate remains open until both sign off.The second notified party sees the first sign-off in their review UI, providing full context.
Step 4a — Approved
Content approvals (content_review)
approvals.status → approved- All linked activities re-enqueued with
wakeReason: 'review_approved'+ reviewer notes - The next activity in the pipeline is spawned (e.g. “Publish to WordPress”)
- SSE event fires to all open Dashboard/DM Portal tabs:
approval_resolved - Notification sent to reviewer confirming resolution
Channel action approvals (channel_action)
approvals.status → approved- The blocked write-tool call is executed immediately (e.g.
wordpress.createPost) - The calling activity resumes and completes
- If the tool call itself fails (e.g. WordPress API error), the activity moves to
failed— a new HITL gate is not created; the failure is treated as a system error
Agent mid-run approvals (content_direction, budget_authorization, etc.)
approvals.status → approved- All linked activities re-enqueued with the decision context injected:
wakeReason: 'review_approved'reviewerFeedback: reviewer noteschosenOption: the selected option (if the approval had choices)
- The agents resume with this context prepended to their prompt
Step 4b — Rejected / Feedback
approvals.status → rejected- All linked activities re-enqueued with
wakeReason: 'review_feedback' - Reviewer notes are appended to the agent’s system prompt as a
REVIEWER FEEDBACKblock:
--- REVIEWER FEEDBACK ---
The previous output was rejected. The reviewer said:
"Title is too generic — needs a Brisbane-specific angle. Rewrite
the intro to open with the Brisbane summer heat as the hook."
Please revise your output accordingly.
--- END REVIEWER FEEDBACK ---- The agent re-runs with this feedback and produces a new output
- A new
content_reviewapproval is created for the revised output - The retry counter on the activity increments. After 3 failed retries, the activity moves to
blockedand an escalation is created for the DM team to intervene manually.
Edit + approve (content_review only)
Rather than rejecting and waiting for a retry, the reviewer can edit the content inline and approve the edited version directly:
- Reviewer edits the content in the review UI
- Clicks “Approve edited version”
- The edited content is saved as the final output — no agent retry
- The approval is marked
approvedwithreviewerNotes: 'Edited by reviewer' - Pipeline continues from the edited content (e.g. Publish to WordPress)
This is the fastest path for small corrections (typos, tone tweaks, factual fixes).
Step 5 — Expiry
If no decision is made before expires_at:
approvals.status → expired- Linked activities remain in
awaiting_approval— they do not auto-proceed - An escalation activity is created in the DM Portal:
🚨 ESCALATED: Approval expired — Q2 Newsletter — April 2026
Priority: urgent
Assigned to: All DM reviewers (Globex Corp)
The email campaign approval expired 2 hours ago without a decision.
3 activities are currently blocked.
Resolve immediately — the campaign go-live date was yesterday.- A
highpriority notification is sent via all active channels (including SMS for high-risk types)
Expiry does not auto-approve or auto-reject. A human must always make the call.
Approval State Machine
create
│
▼
┌─────────┐
│ pending │
└────┬────┘
┌─────────────┼──────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌─────────┐
│ approved │ │ rejected │ │ expired │
└──────────┘ └──────────┘ └─────────┘
pending → approved human approves (or edit+approves)
pending → rejected human rejects with feedback
pending → expired expires_at passes with no decisionThere is no path back from approved, rejected, or expired to pending. If a reviewer changes their mind after approving, a new approval must be created. This ensures a complete and immutable audit trail.
Audit Trail
Every approval event is written to audit_logs:
| Event | Logged fields |
|---|---|
| Approval created | type, riskLevel, createdByType, createdByAgentRole, linkedActivityIds |
| Approval approved | reviewedByUserId, reviewerNotes, chosenOption, resolvedAt |
| Approval rejected | reviewedByUserId, reviewerNotes, resolvedAt |
| Approval expired | resolvedAt, escalationActivityId |
| Edit + approve | reviewedByUserId, editedContentDiff, resolvedAt |
All audit records are immutable and retained for a minimum of 2 years. See Governance — Audit Trail Security.