🔄 Approval State Machine
APPROVAL STATE MACHINE
Waiting for human decision. Escalation clock running. Workflow is paused.
Auto-rejects after 7 days if unresolved.
Human granted approval. Workflow resumes, step dispatches.
Human denied approval. Step follows its on_failure policy.
RESOLUTION CHANNELS
Dashboard UI
Action buttons
External systems
All channels call the same approvals.resolve mutation
⚡ How Approvals Are Triggered
Three paths create approval records:| Source | Trigger | Description |
|---|---|---|
| Explicit gate | .lobsterX step declares approval: "required" | The interpreter creates an approval before dispatching the step. |
| Escalation | Step with on_failure: retry_once_then_escalate fails its retry | The interpreter creates an escalation approval so a human can choose to retry, skip, or abort. |
| Policy-sourced | Governance pipeline returns hold disposition | A 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:
- Inserts a row in the
approvalstable withstatus: "pending"andrequestedAt: Date.now() - Records an
approval_createdaudit event - Increments the
pendingApprovalscounter on the parentworkflowRunsrecord - Updates the run’s
operatorStatusto reflect the pending approval
| Field | Description |
|---|---|
runId | The workflow run that is blocked |
stepId | The step waiting for approval |
summary | Human-readable description of what is being requested |
source | "workflow" (from .lobsterX) or "policy" (from governance) |
workflowName | Workflow identifier for context |
slackChannel | Target Slack channel (if configured) |
🔔 Notification
After creating the approval, the interpreter fires a notification event through the notification bus:- Builds a Block Kit message with the approval summary, workflow name, and context
- Posts to the configured Slack channel
- Includes Approve and Reject action buttons with encoded
approvalId|runId|stepId - Stores the Slack
message_tsback on the approval record for future updates
⏰ Escalation Schedule
A cron job runs every 15 minutes and checks all pending approvals against a three-tier escalation schedule:ESCALATION TIMELINE
0h → 1h
Gentle first reminder at 1 hour.
1h → 24h
Stronger nudge at 24 hours.
24h → 72h
Final warning at 72 hours.
7d
System rejects.
Each tier fires an approval.reminder notification event. Debounced: no re-send within 1 hour.
- Fires an
approval.remindernotification event with the current urgency tier - Updates
reminderSentAton the approval to prevent duplicate sends within the same tier - Records an
approval_reminder_sentaudit event
📡 Resolution Channels
Approvals can be resolved through three channels:| Channel | How | Identifier |
|---|---|---|
| Burgundy | Operator clicks Approve/Reject in the dashboard | resolvedVia: "burgundy" |
| Operator clicks the action button in the Slack message | resolvedVia: "slack" | |
| API | External system calls the resolve mutation | resolvedVia: "api" |
approvals.resolve mutation, which:
- Updates the approval status to
"approved"or"rejected" - Records
resolvedBy,resolvedAt,reason, andresolvedVia - Decrements
pendingApprovalson the workflow run - Updates the run’s
operatorStatus - Fires the interpreter resume event via the outbox pattern
▶️ 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
Resolve mutation writes to eventOutbox: eventType “approval”, workflow ID, step ID, decision.
Immediately schedules reliable event delivery via ctx.scheduler.runAfter(0, …).
Delivery function fires completion event to durable workflow via awaitEvent. Interpreter wakes.
Cron sweeps stale outbox entries every 30s. If initial delivery failed, retries up to 3 attempts.
⏳ Auto-Reject
A daily cron (approval auto-reject) runs at 02:00 UTC and rejects any approval pending longer than 7 days:
- Sets status to
"rejected"withresolvedBy: "system:auto_reject" - Fires the interpreter resume event via the outbox pattern
- Records an
approval_auto_rejectedaudit event with the approval age - For lifecycle approvals, records the corresponding transition rejection
📋 Approval Sources
| Source | Created by | stepId pattern | Example |
|---|---|---|---|
| Workflow step | Interpreter | {step_id} | review_plan |
| Escalation | Interpreter (after retry failure) | {step_id}_escalation | generate_code_escalation |
| Deployment policy | Deployment governance | deployment:{deploymentId} | deployment:abc123 |
| Lifecycle transition | Agent lifecycle management | lifecycle:{agentId}:{stage} | lifecycle:agent-1:active |
| Position creation | Org structure governance | position_creation:{details} | position_creation:senior-dev |
💬
Slack 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)
approvals.resolve with resolvedVia: "slack".
📋 Audit Trail
Every stage of the approval lifecycle is recorded:| Event | When |
|---|---|
approval_created | Approval record inserted |
approval_reminder_sent | Escalation reminder dispatched (with urgency tier) |
approval_resolved | Human resolves the approval (approve or reject) |
approval_auto_rejected | System auto-rejects after 7 days |
governance_decision (hold) | Governance pipeline returns hold disposition |

