Skip to main content
Approval gates let you insert checkpoints into the task lifecycle that either automatically evaluate a condition or block execution until a human approves or rejects. Gates are registered as lifecycle hooks. When a hook fires (e.g. before:task:assign), the gate evaluates:
  • Auto gates — check a condition expression. If the condition matches, the operation is blocked.
  • Human gates — pause the operation, transition the task to awaiting_approval, and wait for external resolution.
Approval gate evaluation flow: auto gate blocks or passes, human gate transitions to awaiting_approval

The awaiting_approval State

When a human gate triggers, the task transitions to awaiting_approval. This is a dedicated state in the task state machine — the task will not be picked up by agents or advance until resolved. On resolution:
  • Approved — task transitions back to assigned and continues execution
  • Revised — task receives feedback in its description, transitions back to assigned, and the agent re-spawns with the updated instructions
  • Rejected — task transitions to failed
  • Timeout — applies the configured timeoutAction (default: reject)

Configuration

Gates are configured under settings.approvalGates as an array:
{
  "settings": {
    "approvalGates": [
      {
        "id": "prod-deploy-gate",
        "name": "Production Deployment Review",
        "handler": "human",
        "hook": "task:assign",
        "condition": {
          "expression": "task.group === 'production'"
        },
        "notifyChannels": ["slack-alerts"],
        "timeoutMs": 3600000,
        "timeoutAction": "reject",
        "priority": 50,
        "maxRevisions": 3
      },
      {
        "id": "high-retry-guard",
        "name": "Block tasks with excessive retries",
        "handler": "auto",
        "hook": "task:assign",
        "condition": {
          "expression": "task.retries >= 5"
        }
      }
    ]
  }
}

Gate Properties

PropertyTypeRequiredDescription
idstringYesUnique gate identifier
namestringYesHuman-readable name
handler"auto" | "human"YesGate type
hookstringYesLifecycle hook to attach to (e.g. task:assign, task:fail)
condition{ expression: string }NoWhen to activate the gate
notifyChannelsstring[]NoChannels to alert when gate activates
timeoutMsnumberNoTimeout in ms for human gates (0 = no timeout)
timeoutAction"approve" | "reject"NoAction on timeout (default: "reject")
prioritynumberNoHook priority — lower runs first (default: 50)
maxRevisionsnumberNoMax revision rounds before only approve/reject is allowed (default: 3)

Auto Gates

Auto gates evaluate a condition expression against the hook payload. The expression has access to data, task, and mission variables.
{
  "id": "block-failed-missions",
  "name": "Block tasks in failed missions",
  "handler": "auto",
  "hook": "task:assign",
  "condition": {
    "expression": "data.allPassed === false"
  }
}
When the condition matches, the hook is cancelled and the operation is blocked. If there is no condition, the auto gate is a no-op (always passes).

Human Gates

Human gates block execution and create an ApprovalRequest that must be resolved externally — via the API, TUI, or timeout.
{
  "id": "manual-review",
  "name": "Manual Code Review",
  "handler": "human",
  "hook": "task:complete",
  "condition": {
    "expression": "task.assignTo === 'junior-agent'"
  },
  "timeoutMs": 1800000,
  "timeoutAction": "reject",
  "maxRevisions": 3
}

Revising (Request Changes)

Instead of a binary approve/reject, reviewers can revise — send the task back for rework with specific feedback. This is the most common action in practice: the work is almost right but needs a correction.
POST /api/v1/projects/:projectId/approvals/:id/revise

{ "feedback": "Add error handling for the 404 case", "resolvedBy": "alice" }
When you revise:
  1. The approval request is marked "revised" with the feedback as its note
  2. The feedback is appended to the task description (the agent sees it as context)
  3. The task’s revisionCount is incremented
  4. The task goes back to assigned and the agent re-spawns
  5. The agent works with the original task + all accumulated feedback
  6. When it finishes, if the same gate matches again, a new ApprovalRequest is created
Each gate has a maxRevisions limit (default: 3). Once the task has been revised that many times, the only options left are approve or reject. This prevents infinite revision loops.
{
  "id": "code-review-gate",
  "name": "Code Review",
  "handler": "human",
  "hook": "task:complete",
  "maxRevisions": 5
}

Timeout Handling

timeoutActionBehavior
"approve"Task proceeds automatically after timeout
"reject"Task fails after timeout (default)
If timeoutMs is 0 or omitted, the gate waits indefinitely.
Pending approvals survive orchestrator restarts. On startup, the ApprovalManager resumes timeout timers for any pending requests, adjusting for elapsed time.

Persistence

Approval requests are persisted via the ApprovalStore interface. The default implementation (FileApprovalStore) writes to .polpo/approvals.json. Each request tracks:
  • Gate ID and name
  • Related task and mission IDs
  • Status (pending, approved, rejected, revised, timeout)
  • Timestamps for creation and resolution
  • Who resolved it and optional notes

Events

EventPayloadDescription
approval:requested{ requestId, gateId, gateName, taskId, missionId }A new approval request was created
approval:resolved{ requestId, status, resolvedBy }Request was approved or rejected
approval:revised{ requestId, taskId, feedback, revisionCount, resolvedBy }Request was revised — task sent back for rework
approval:timeout{ requestId, action }Request timed out; action is the applied timeout action

Integration with Escalation

When escalation reaches Level 3 (human-in-the-loop), the escalation manager transitions the task to awaiting_approval and emits approval:requested. This creates the same approval flow as a configured gate — the task waits for human resolution via the API or TUI.