> ## Documentation Index
> Fetch the complete documentation index at: https://docs.swarmd.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Human-in-the-Loop

> Set up manual approval workflows so humans can review, approve, or reject agent actions before they proceed.

# Human-in-the-Loop (HITL)

Human-in-the-loop lets you pause agent communication and require manual approval before a message is forwarded. This is critical for high-stakes operations where you need a human to verify what an agent is about to do.

***

## How HITL Gets Triggered

There are two distinct ways a message can be held for human review:

| Source                | How it happens                                                             | Typical use case                                                                |
| --------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| **Policy escalation** | A detection policy with action `HUMAN_REVIEW_REQUIRED` matches the message | Compliance rules, PII detection, restricted operations                          |
| **Agent-initiated**   | The downstream agent returns task status `INPUT_REQUIRED`                  | Agent is uncertain, needs clarification, or is designed to ask for confirmation |

<Info>
  These are fundamentally different flows. **Policy escalation** is triggered by a detection policy (regex, Presidio, Comprehend) at the relay level — before or after the message reaches the agent, depending on which leg matched. **Agent-initiated** `INPUT_REQUIRED` happens when the agent has processed the message and decided it needs human input. Both are automatically intercepted by the relay's built-in HITL middleware.
</Info>

***

## Setting Up Policy-Based HITL

To escalate messages for human review, set the `action` to `HUMAN_REVIEW_REQUIRED` on any detection policy (regex, Presidio, or Comprehend). There is no separate "HITL policy type" — human review is an action you configure on your existing detection policies.

<Tabs>
  <Tab title="API">
    ```bash theme={null}
    curl -X POST https://api.swarmd.ai/relay/v1/policies \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer $SWARMD_TOKEN" \
      -d '{
        "name": "Review Messages with SSNs",
        "description": "Escalate to human review when SSNs are detected",
        "config": {
          "type": "PRESIDIO_DETECTION",
          "enabledLegs": {
            "requestFromSource": true,
            "requestToSink": true,
            "responseFromSink": true,
            "responseToSource": true
          },
          "entity": {
            "type": "US_SSN",
            "scoreThreshold": 0.7,
            "action": "HUMAN_REVIEW_REQUIRED"
          }
        }
      }'
    ```
  </Tab>

  <Tab title="UI">
    Coming soon.
  </Tab>
</Tabs>

<Tip>
  After creating your policy, remember to add it to a **policy group** and **bind** that group to a tenant, agent, or subscription. See the [Policy Configuration tutorial](/tutorials/policy-configuration) for the full flow.
</Tip>

***

## Reviewing Approvals

When a message is held for review, it appears as a pending approval.

### List Pending Approvals

<Tabs>
  <Tab title="API">
    ```bash theme={null}
    curl "https://api.swarmd.ai/relay/v1/approvals?status=PENDING" \
      -H "Authorization: Bearer $SWARMD_TOKEN"
    ```

    Response:

    ```json theme={null}
    [
      {
        "id": "a1b2c3d4-...",
        "taskId": "task-5678",
        "agentMessageRole": "user",
        "agentMessageText": "Please process this SSN: 123-45-6789",
        "detectionSource": "POLICY_ESCALATION",
        "sourceAgentId": "...",
        "sinkAgentId": "...",
        "policyName": "Review Messages with SSNs",
        "matchedContent": "123-45-6789",
        "createdAt": "2025-03-10T14:30:00",
        "resolution": null
      }
    ]
    ```

    To see all approvals (including resolved ones):

    ```bash theme={null}
    curl "https://api.swarmd.ai/relay/v1/approvals?status=ALL" \
      -H "Authorization: Bearer $SWARMD_TOKEN"
    ```
  </Tab>

  <Tab title="UI">
    Coming soon.
  </Tab>
</Tabs>

### Get a Specific Approval

<Tabs>
  <Tab title="API">
    ```bash theme={null}
    curl https://api.swarmd.ai/relay/v1/approvals/APPROVAL_ID \
      -H "Authorization: Bearer $SWARMD_TOKEN"
    ```
  </Tab>

  <Tab title="UI">
    Coming soon.
  </Tab>
</Tabs>

### Key Fields in an Approval

| Field                            | Description                                                                    |
| -------------------------------- | ------------------------------------------------------------------------------ |
| `detectionSource`                | `POLICY_ESCALATION` (from a policy) or `AGENT_INPUT_REQUIRED` (from the agent) |
| `agentMessageText`               | The message content that triggered the review                                  |
| `matchedContent`                 | The specific content that matched the policy (if policy-triggered)             |
| `policyName`                     | The name of the policy that triggered escalation (if policy-triggered)         |
| `sourceAgentId` / `sourceUserId` | Who sent the message                                                           |
| `sinkAgentId`                    | The target agent                                                               |
| `resolution`                     | `null` if pending, otherwise contains the resolution details                   |

***

## Resolving Approvals

### Approve

Approving a request allows the message to continue through the relay to the target agent.

<Tabs>
  <Tab title="API">
    ```bash theme={null}
    curl -X POST https://api.swarmd.ai/relay/v1/approvals/APPROVAL_ID/resolve \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer $SWARMD_TOKEN" \
      -d '{
        "action": "APPROVED",
        "message": "Verified — this is a test SSN for the demo environment"
      }'
    ```
  </Tab>

  <Tab title="UI">
    Coming soon.
  </Tab>
</Tabs>

**What happens next:**

* The held message is forwarded to the target agent
* The task resumes from where it was paused — callers polling `tasks/get` will see `completed` state with the full agent response
* The `relay_reason` metadata is no longer present in the resolved response
* The approval, resolution, and reviewer are recorded in the audit log

### Reject

Rejecting a request stops the message from being forwarded. The task is cancelled.

<Tabs>
  <Tab title="API">
    ```bash theme={null}
    curl -X POST https://api.swarmd.ai/relay/v1/approvals/APPROVAL_ID/resolve \
      -H "Content-Type: application/json" \
      -H "Authorization: Bearer $SWARMD_TOKEN" \
      -d '{
        "action": "REJECTED",
        "message": "Contains real PII — must not be sent to external agent"
      }'
    ```
  </Tab>

  <Tab title="UI">
    Coming soon.
  </Tab>
</Tabs>

**What happens next:**

* The message is **not** forwarded
* The associated task is cancelled — callers polling `tasks/get` will see `canceled` state with `relay_reason: "HITL_REJECTED"` in the metadata (including `policy_name`, `policy_version`, and `policy_level` if the hold was policy-triggered)
* The rejection reason is recorded in the audit log

***

## Policy Escalation vs Agent INPUT\_REQUIRED

It's important to understand the difference between these two flows:

|                      | Policy Escalation                                                                 | Agent INPUT\_REQUIRED                                               |
| -------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| **Triggered by**     | A detection policy with `HUMAN_REVIEW_REQUIRED` action                            | Downstream agent returning `INPUT_REQUIRED` task status             |
| **When**             | During relay policy evaluation (on the matching leg)                              | After the agent has processed the message                           |
| **Detection source** | `POLICY_ESCALATION`                                                               | `AGENT_INPUT_REQUIRED`                                              |
| **Approval fields**  | `policyName` and `matchedContent` populated                                       | `policyName` and `matchedContent` are `null`                        |
| **Use case**         | Compliance, safety guardrails                                                     | Agent needs clarification or confirmation                           |
| **On approve**       | Calls `resumeTask()` — the original held message is forwarded to the target agent | Calls `resumeTask()` — a confirmation response is sent to the agent |

<Warning>
  When an agent returns `INPUT_REQUIRED`, the agent has already seen the message and is asking for more input. The relay's built-in HITL middleware automatically intercepts this and creates an approval request — you don't need to configure a policy for this path. It happens by default.
</Warning>

***

## Response Metadata — `relay_reason`

When the relay masks a response due to HITL (or a timeout), it sets a `relay_reason` field in the response metadata. This tells API consumers **why** the relay has taken ownership of the task. The response metadata is a strongly-typed discriminated union — the shape of the metadata depends on the `relay_reason` value.

| `relay_reason`                   | `status.state` | Meaning                                                     | Additional fields                                          |
| -------------------------------- | -------------- | ----------------------------------------------------------- | ---------------------------------------------------------- |
| `TIMEOUT`                        | `working`      | Agent didn't respond within the early-return timeout window | *(none)*                                                   |
| `HITL_HELD`                      | `working`      | Held for human review by a detection policy                 | `policy_name`, `policy_version`, `policy_level`            |
| `HITL_HELD_AGENT_INPUT_REQUIRED` | `working`      | Agent explicitly requested human input (no policy involved) | *(none)*                                                   |
| `HITL_REJECTED`                  | `canceled`     | Human reviewer rejected the HITL request                    | `policy_name`, `policy_version`, `policy_level` (nullable) |

**Policy fields** (`policy_name`, `policy_version`, `policy_level`) are included for `HITL_HELD` and `HITL_REJECTED` to give consumers context about which policy triggered the hold. These fields are nullable on `HITL_REJECTED` — they will be `null` if the original hold was agent-initiated rather than policy-triggered.

**`policy_level`** indicates the scope at which the policy is bound: `TENANT`, `AGENT`, or `SUBSCRIPTION`.

Example metadata for a policy-held response:

```json theme={null}
{
  "relay_reason": "HITL_HELD",
  "policy_name": "Review Messages with SSNs",
  "policy_version": "1.0.0",
  "policy_level": "TENANT"
}
```

Example metadata for an agent-initiated hold:

```json theme={null}
{
  "relay_reason": "HITL_HELD_AGENT_INPUT_REQUIRED"
}
```

<Info>
  For a TypeScript integration, use the [`@swarmd/channel-client` lifecycle events](/sdks/typescript/channel-client#handle-lifecycle-events) to react to `working`, `hitl-held`, `completed`, `rejected`, `failed`, and `canceled` without implementing polling yourself. The raw [Conversation API frontend tutorial](/tutorials/frontend-integration-conversations) documents the underlying `aggregateState` / `relay_reason` protocol, and the older JSON-RPC flow is documented separately at [Frontend Integration (JSON-RPC — legacy)](/tutorials/hitl-frontend-integration).
</Info>

***

## Auditing HITL Decisions

Every HITL event is recorded in the audit log with specific audit types:

| Audit Type        | Description                                                         |
| ----------------- | ------------------------------------------------------------------- |
| `HITL`            | HITL detection occurred (policy escalation or agent input-required) |
| `HITL_GUARD`      | The message was held and the task is guarded pending review         |
| `HITL_RESOLUTION` | The approval was resolved (approved or rejected)                    |

Query HITL-related events:

<Tabs>
  <Tab title="API">
    ```bash theme={null}
    curl "https://api.swarmd.ai/audit/v1/events?auditType=HITL_RESOLUTION&page=0&size=10" \
      -H "Authorization: Bearer $SWARMD_TOKEN"
    ```

    To see the complete flow — trigger, hold, and resolution — use the trace endpoint with the `correlationId` from the approval:

    ```bash theme={null}
    curl https://api.swarmd.ai/audit/v1/traces/CORRELATION_ID \
      -H "Authorization: Bearer $SWARMD_TOKEN"
    ```
  </Tab>

  <Tab title="UI">
    Coming soon.
  </Tab>
</Tabs>

***

## Required Permissions

To manage HITL approvals, users need the following permissions (granted through group membership):

| Action                | Required Permission         |
| --------------------- | --------------------------- |
| List / view approvals | `AGENT_CONVERSATIONS:READ`  |
| Approve or reject     | `AGENT_CONVERSATIONS:WRITE` |

The default **Editor** group includes both of these permissions. See the [User Management tutorial](/tutorials/user-management) for how to manage groups and permissions.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="TypeScript Channel Client" icon="code" href="/sdks/typescript/channel-client#handle-lifecycle-events">
    Handle HITL transitions through typed channel lifecycle events.
  </Card>

  <Card title="Monitoring & Audit" icon="chart-line" href="/tutorials/monitoring-and-audit">
    Track all agent activity and HITL decisions in the audit log.
  </Card>
</CardGroup>
