Aegis Orchestrator
Guides

Human Approvals

Pause workflow execution for human review, approval, or rejection using the built-in approval gate.

Human Approvals

Workflows can pause at any point to wait for a human decision before proceeding. The orchestrator holds the workflow execution open, notifies via the approval API, and resumes or terminates based on the response — all with configurable timeouts.


Defining an Approval Gate

Add a kind: Human state to your workflow manifest:

states:
  - name: GENERATE_DRAFT
    kind: Agent
    agent_id: draft-agent-uuid
    transitions:
      - target: AWAIT_APPROVAL
        condition: "always"

  - name: AWAIT_APPROVAL
    kind: Human
    prompt: "Review the draft in {{GENERATE_DRAFT.output}}. Approve to publish, reject to discard."
    timeout_seconds: 86400      # 24 hours — reject on timeout if omitted means 3600s default
    reject_on_timeout: false    # if true, workflow transitions to on_reject on timeout
    transitions:
      - target: PUBLISH
        condition: "approved"
      - target: DISCARD
        condition: "rejected"

  - name: PUBLISH
    kind: Agent
    agent_id: publish-agent-uuid

  - name: DISCARD
    kind: System
    command: "echo 'Draft discarded'"

kind: Human Fields

FieldTypeDefaultDescription
promptstringText displayed to the reviewer. Supports Blackboard template syntax.
timeout_secondsinteger3600How long to wait before the request expires.
reject_on_timeoutbooleanfalseIf true, a timeout transitions to the rejected condition. If false, the workflow fails.

The approved and rejected transition conditions are reserved keywords for kind: Human states.


Listing Pending Approval Requests

Poll the approval queue from any HTTP client:

curl https://your-aegis-node/v1/human-approvals \
  -H "Authorization: Bearer $KEYCLOAK_JWT"

Response:

{
  "pending_requests": [
    {
      "id": "req-uuid",
      "execution_id": "exec-uuid",
      "prompt": "Review the draft in <draft content>. Approve to publish, reject to discard.",
      "created_at": "2026-03-05T14:00:00Z",
      "timeout_seconds": 86400
    }
  ]
}

Fetch a specific request:

curl https://your-aegis-node/v1/human-approvals/req-uuid \
  -H "Authorization: Bearer $KEYCLOAK_JWT"

Approving a Request

curl -X POST https://your-aegis-node/v1/human-approvals/req-uuid/approve \
  -H "Authorization: Bearer $KEYCLOAK_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "feedback": "Looks good, approved for production.",
    "approved_by": "[email protected]"
  }'

Both fields are optional. Response:

{ "status": "approved" }

The workflow immediately resumes from the AWAIT_APPROVAL state and transitions to the approved target state (PUBLISH in the example above).


Rejecting a Request

curl -X POST https://your-aegis-node/v1/human-approvals/req-uuid/reject \
  -H "Authorization: Bearer $KEYCLOAK_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Draft needs more citations before publishing.",
    "rejected_by": "[email protected]"
  }'

reason is required. Response:

{ "status": "rejected" }

The workflow transitions to the rejected target state (DISCARD in the example).


Timeout Behaviour

When timeout_seconds expires without a response:

  • reject_on_timeout: false (default): The workflow execution is marked failed with reason human_input_timeout. No transition occurs.
  • reject_on_timeout: true: The orchestrator treats the timeout as a rejection and transitions to the rejected target state. Useful when a missing response should be treated as a "no".

Building an Approval UI

A minimal polling loop for a custom approval interface:

const ORCHESTRATOR = 'https://your-aegis-node';
const TOKEN = process.env.KEYCLOAK_JWT;

async function pollApprovals() {
  const res = await fetch(`${ORCHESTRATOR}/v1/human-approvals`, {
    headers: { Authorization: `Bearer ${TOKEN}` },
  });
  const { pending_requests } = await res.json();

  for (const req of pending_requests) {
    console.log(`[${req.id}] ${req.prompt}`);
    console.log(`  Created: ${req.created_at}  Timeout: ${req.timeout_seconds}s`);
  }
}

async function approve(requestId: string, feedback?: string) {
  await fetch(`${ORCHESTRATOR}/v1/human-approvals/${requestId}/approve`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ feedback, approved_by: '[email protected]' }),
  });
}

async function reject(requestId: string, reason: string) {
  await fetch(`${ORCHESTRATOR}/v1/human-approvals/${requestId}/reject`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ reason, rejected_by: '[email protected]' }),
  });
}

Template Variables in Prompts

The prompt field in a kind: Human state supports Blackboard template syntax, giving reviewers access to outputs from earlier workflow states:

- name: AWAIT_APPROVAL
  kind: Human
  prompt: |
    Agent produced the following output:
    
    {{GENERATE_DRAFT.output}}
    
    Score from quality check: {{QUALITY_CHECK.output.score}}
    
    Please approve or reject.

See the Template Syntax Reference for the full variable namespace.


See Also

On this page