Skip to main content
Approvals are the primary (HITL) control in Gloo Forge. They insert a decision point into an otherwise deterministic workflow. When the interpreter encounters an approval gate, the workflow suspends: a notification is posted, an escalation clock starts, and the interpreter sleeps on a durable workflow event. No LLM tokens are consumed while waiting.

🔄 Approval State Machine

APPROVAL STATE MACHINE

PENDING

Waiting for human decision. Escalation clock running. Workflow is paused.

Auto-rejects after 7 days if unresolved.

APPROVED

Human granted approval. Workflow resumes, step dispatches.

REJECTED

Human denied approval. Step follows its on_failure policy.

PENDING → approve → APPROVED|PENDING → reject → REJECTED|PENDING → 7d timeout → REJECTED (auto)

RESOLUTION CHANNELS

BurgundyBurgundy

Dashboard UI

SlackSlack

Action buttons

Platform API

External systems

All channels call the same approvals.resolve mutation

⚡ How Approvals Are Triggered

Three paths create approval records:
SourceTriggerDescription
Explicit gate.lobsterX step declares approval: "required"The interpreter creates an approval before dispatching the step.
EscalationStep with on_failure: retry_once_then_escalate fails its retryThe interpreter creates an escalation approval so a human can choose to retry, skip, or abort.
Policy-sourcedGovernance pipeline returns hold dispositionA governance policy (via gate 9 or 10) requires approval before the step can execute.

✏️ Creation

When an approval is needed, approvals.create runs as an internal mutation:
  1. Inserts a row in the approvals table with status: "pending" and requestedAt: Date.now()
  2. Records an approval_created audit event
  3. Increments the pendingApprovals counter on the parent workflowRuns record
  4. Updates the run’s operatorStatus to reflect the pending approval
The approval record captures context for the reviewer:
FieldDescription
runIdThe workflow run that is blocked
stepIdThe step waiting for approval
summaryHuman-readable description of what is being requested
source"workflow" (from .lobsterX) or "policy" (from governance)
workflowNameWorkflow identifier for context
slackChannelTarget Slack channel (if configured)

🔔 Notification

After creating the approval, the interpreter fires a notification event through the notification bus:
event.type = "approval.requested"
The bus routes to all configured channels. When Slack is configured, the handler:
  1. Builds a Block Kit message with the approval summary, workflow name, and context
  2. Posts to the configured Slack channel
  3. Includes Approve and Reject action buttons with encoded approvalId|runId|stepId
  4. Stores the Slack message_ts back on the approval record for future updates
Notification is fire-and-forget. If the Slack call fails, the error is logged but does not block the workflow or the approval lifecycle. The approval remains pending and can be resolved through other channels.

⏰ Escalation Schedule

A cron job runs every 15 minutes and checks all pending approvals against a three-tier escalation schedule:

ESCALATION TIMELINE

Tier 1: Normal

0h → 1h

Gentle first reminder at 1 hour.

Tier 2: Elevated

1h → 24h

Stronger nudge at 24 hours.

Tier 3: Critical

24h → 72h

Final warning at 72 hours.

Auto-Reject

7d

System rejects.

Each tier fires an approval.reminder notification event. Debounced: no re-send within 1 hour.

Each reminder:
  • Fires an approval.reminder notification event with the current urgency tier
  • Updates reminderSentAt on the approval to prevent duplicate sends within the same tier
  • Records an approval_reminder_sent audit event
The cron debounces: it will not re-send a reminder if the last one was sent less than 1 hour ago.

📡 Resolution Channels

Approvals can be resolved through three channels:
ChannelHowIdentifier
BurgundyOperator clicks Approve/Reject in the dashboardresolvedVia: "burgundy"
SlackSlackOperator clicks the action button in the Slack messageresolvedVia: "slack"
APIExternal system calls the resolve mutationresolvedVia: "api"
All resolution paths call the same approvals.resolve mutation, which:
  1. Updates the approval status to "approved" or "rejected"
  2. Records resolvedBy, resolvedAt, reason, and resolvedVia
  3. Decrements pendingApprovals on the workflow run
  4. Updates the run’s operatorStatus
  5. Fires the interpreter resume event via the outbox pattern
The resolve mutation requires the governance:approve permission. Standard users cannot resolve approvals — only operators with the appropriate role.

▶️ Workflow Resume

When an approval resolves, the interpreter must resume. Forge uses the reliable event delivery pattern (outbox + retry) to guarantee this:

RELIABLE RESUME FLOW

1
Insert Outbox Entry

Resolve mutation writes to eventOutbox: eventType “approval”, workflow ID, step ID, decision.

2
Schedule Delivery

Immediately schedules reliable event delivery via ctx.scheduler.runAfter(0, …).

3
Fire Workflow Event

Delivery function fires completion event to durable workflow via awaitEvent. Interpreter wakes.

4
Safety Net Sweep

Cron sweeps stale outbox entries every 30s. If initial delivery failed, retries up to 3 attempts.

This pattern replaced a previous fire-and-forget approach where the approval was resolved in the database but the workflow could remain stuck if the scheduled function failed. The outbox guarantees eventual delivery.

⏳ Auto-Reject

A daily cron (approval auto-reject) runs at 02:00 UTC and rejects any approval pending longer than 7 days:
  1. Sets status to "rejected" with resolvedBy: "system:auto_reject"
  2. Fires the interpreter resume event via the outbox pattern
  3. Records an approval_auto_rejected audit event with the approval age
  4. For lifecycle approvals, records the corresponding transition rejection
Auto-reject prevents workflows from being blocked indefinitely by unattended approvals. The 72-hour critical escalation reminder (Tier 3) gives operators a 4-day warning before auto-reject fires.

📋 Approval Sources

SourceCreated bystepId patternExample
Workflow stepInterpreter{step_id}review_plan
EscalationInterpreter (after retry failure){step_id}_escalationgenerate_code_escalation
Deployment policyDeployment governancedeployment:{deploymentId}deployment:abc123
Lifecycle transitionAgent lifecycle managementlifecycle:{agentId}:{stage}lifecycle:agent-1:active
Position creationOrg structure governanceposition_creation:{details}position_creation:senior-dev

💬 SlackSlack Integration

When Slack is configured, approvals get interactive Block Kit messages:
  • Summary block with workflow name, step ID, and approval reason
  • Approve and Reject buttons with encoded metadata
  • Context section with prior step outputs (if available)
  • Thread updates when the approval is resolved (updates the original message)
Resolution via Slack calls back to Convex through Slack’s interactivity endpoint, which calls approvals.resolve with resolvedVia: "slack".

📋 Audit Trail

Every stage of the approval lifecycle is recorded:
EventWhen
approval_createdApproval record inserted
approval_reminder_sentEscalation reminder dispatched (with urgency tier)
approval_resolvedHuman resolves the approval (approve or reject)
approval_auto_rejectedSystem auto-rejects after 7 days
governance_decision (hold)Governance pipeline returns hold disposition
See also: Governance Pipeline | Workflow Execution Flow