Skip to Content
Human-in-the-LoopApproval Flow

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_review created
  • A write-tool call is intercepted (e.g. wordpress.createPost) → channel_action created

The system sets expiry based on risk:

Risk levelExpiry window
low72 hours
medium48 hours
high24 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

Channellow riskmedium riskhigh risk
In-app bell (web SSE)Yes — immediateYes — immediateYes — immediate
Mobile pushYes — immediateYes — immediateYes — immediate
EmailBatched (daily digest)Yes — immediateYes — immediate
SMSNoNoYes — if enabled
WhatsAppNoNoYes — 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:

  1. All DM reviewers assigned to the tenant
  2. The tenant admin — for strategy_change, budget_authorization, and content_review approvals where the tenant has enabled “notify me on all approvals” in settings
  3. Super admins — never notified automatically (they use /status --all in 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 typeApproveReject with feedbackEdit + approveChoose option
content_reviewYesYesYes (edit inline)No
content_directionYesYesNoYes (if options provided)
brand_directionYesYesNoYes (if options provided)
strategy_changeYesYesNoNo
budget_authorizationYes (authorise spend)Yes (decline)NoNo
channel_actionYes (execute action)Yes (cancel action)NoNo

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)

  1. approvals.status → approved
  2. All linked activities re-enqueued with wakeReason: 'review_approved' + reviewer notes
  3. The next activity in the pipeline is spawned (e.g. “Publish to WordPress”)
  4. SSE event fires to all open Dashboard/DM Portal tabs: approval_resolved
  5. Notification sent to reviewer confirming resolution

Channel action approvals (channel_action)

  1. approvals.status → approved
  2. The blocked write-tool call is executed immediately (e.g. wordpress.createPost)
  3. The calling activity resumes and completes
  4. 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.)

  1. approvals.status → approved
  2. All linked activities re-enqueued with the decision context injected:
    • wakeReason: 'review_approved'
    • reviewerFeedback: reviewer notes
    • chosenOption: the selected option (if the approval had choices)
  3. The agents resume with this context prepended to their prompt

Step 4b — Rejected / Feedback

  1. approvals.status → rejected
  2. All linked activities re-enqueued with wakeReason: 'review_feedback'
  3. Reviewer notes are appended to the agent’s system prompt as a REVIEWER FEEDBACK block:
--- 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 ---
  1. The agent re-runs with this feedback and produces a new output
  2. A new content_review approval is created for the revised output
  3. The retry counter on the activity increments. After 3 failed retries, the activity moves to blocked and 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:

  1. Reviewer edits the content in the review UI
  2. Clicks “Approve edited version”
  3. The edited content is saved as the final output — no agent retry
  4. The approval is marked approved with reviewerNotes: 'Edited by reviewer'
  5. 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:

  1. approvals.status → expired
  2. Linked activities remain in awaiting_approvalthey do not auto-proceed
  3. 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.
  1. A high priority 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 decision

There 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:

EventLogged fields
Approval createdtype, riskLevel, createdByType, createdByAgentRole, linkedActivityIds
Approval approvedreviewedByUserId, reviewerNotes, chosenOption, resolvedAt
Approval rejectedreviewedByUserId, reviewerNotes, resolvedAt
Approval expiredresolvedAt, escalationActivityId
Edit + approvereviewedByUserId, editedContentDiff, resolvedAt

All audit records are immutable and retained for a minimum of 2 years. See Governance — Audit Trail Security.

© 2026 Leadmetrics — Internal use only