Skip to main content

HITL Frontend Integration

When an agent chain includes a HITL step (human review required), the relay holds the blocked agent’s response until an admin approves or rejects it. Your frontend needs to detect this and poll for the final result. The relay is designed so that the entire chain resolves before the caller gets a completed answer. You never need to stitch together partial responses from multiple agents. You send one request, poll if needed, and get one complete answer.

The Three Response Scenarios

Every message/send response falls into one of three categories. Check them in this order:

1. Completed — no review needed

The agent (or agent chain) finished normally. Render the response immediately.
{
  "jsonrpc": "2.0",
  "id": "msg-001",
  "result": {
    "id": "e6a262db-...",
    "contextId": "17aa30cf-...",
    "status": {
      "state": "completed",
      "message": {
        "role": "agent",
        "parts": [{"kind": "text", "text": "Emma Alvarez qualifies for Horizon Visa Platinum and Horizon Savings Plus."}]
      }
    },
    "kind": "task"
  }
}
What to do: Render result.status.message as the assistant’s response. Done. How to detect: result.status.state === "completed" and no result.metadata.hitl_pending and no result.metadata.relay_task.

2. Working — chain is blocked, poll for result

A downstream agent in the chain requires human review. The relay returned early because the chain is still processing in the background. This is the most common HITL scenario when agents use the Swarmd SDK.
{
  "jsonrpc": "2.0",
  "id": "msg-001",
  "result": {
    "id": "f7a3b2c1-relay-task-uuid",
    "status": {
      "state": "working"
    },
    "kind": "task",
    "metadata": {
      "relay_task": true
    }
  }
}
What to do:
  1. Show a “Pending review” state in the UI
  2. Save result.id (the relay task ID)
  3. Poll tasks/get using that ID until the state changes
How to detect: result.status.state === "working" and result.metadata.relay_task === true. Polling request (same endpoint, same agent ID as the original request):
{
  "jsonrpc": "2.0",
  "method": "tasks/get",
  "id": "poll-001",
  "params": {
    "id": "f7a3b2c1-relay-task-uuid"
  }
}
While pending — poll returns working:
{
  "jsonrpc": "2.0",
  "id": "poll-001",
  "result": {
    "id": "f7a3b2c1-relay-task-uuid",
    "status": { "state": "working" }
  }
}
After admin approves — poll returns completed with the full answer:
{
  "jsonrpc": "2.0",
  "id": "poll-002",
  "result": {
    "id": "e6a262db-...",
    "contextId": "17aa30cf-...",
    "status": {
      "state": "completed",
      "message": {
        "role": "agent",
        "parts": [{
          "kind": "text",
          "text": "Horizon Visa Platinum has been assigned to Emma Alvarez (CU-1340). The product is now active in her portfolio."
        }]
      }
    },
    "kind": "task"
  }
}
This response is the complete, combined answer from the entire agent chain. The parent agent waited for the sub-agent’s HITL to resolve, got the sub-agent’s result, and formulated a single response. Render it as a normal assistant message. If admin rejects — poll returns failed:
{
  "jsonrpc": "2.0",
  "id": "poll-003",
  "result": {
    "id": "f7a3b2c1-relay-task-uuid",
    "status": { "state": "failed" }
  }
}

3. Completed with hitl_pending — fallback for legacy agents

This scenario only occurs when agents are not using PollingRemoteA2aAgent from the Swarmd SDK. Once all agents are updated to use the SDK, this scenario does not happen — you will always get scenario 1 or 2.
The parent agent returned its own response (completed), but a downstream agent is still blocked by HITL. The parent did not wait.
{
  "jsonrpc": "2.0",
  "id": "msg-001",
  "result": {
    "id": "e6a262db-...",
    "contextId": "17aa30cf-...",
    "status": {
      "state": "completed",
      "message": {
        "role": "agent",
        "parts": [{"kind": "text", "text": "I've submitted the product assignment. It requires approval before it can be processed."}]
      }
    },
    "kind": "task",
    "metadata": {
      "hitl_pending": {
        "hitl_request_id": "486ebe5c-...",
        "task_id": "96047697-...",
        "context_id": "7b290f0f-...",
        "sink_agent_id": "d8e9bef0-...",
        "detection_source": "AGENT_INPUT_REQUIRED"
      }
    }
  }
}
What to do:
  1. Render the parent agent’s partial response
  2. Show a “Pending review” indicator
  3. Poll tasks/get on the blocked sub-agent using hitl_pending.sink_agent_id and hitl_pending.task_id
  4. When resolved, render the sub-agent’s result alongside the parent’s response
How to detect: result.status.state === "completed" and result.metadata.hitl_pending is present. Polling request (note: different agent ID — the blocked sub-agent):
// POST /v1/human/agents/{hitl_pending.sink_agent_id}/a2a/0.3.0
{
  "jsonrpc": "2.0",
  "method": "tasks/get",
  "id": "poll-001",
  "params": {
    "id": "96047697-..."
  }
}

Decision Flowchart

Receive message/send response
         |
         v
Is state "working" + relay_task?
    YES → Show "Pending review", poll tasks/get with result.id
     NO ↓
Is state "completed" + hitl_pending?
    YES → Render partial answer, poll sub-agent for remaining result
     NO ↓
Is state "completed" (no hitl_pending)?
    YES → Render full answer. Done.

Polling Best Practices

SettingRecommendation
Poll interval5 seconds
Max poll durationMatch your use case (HITL approvals can take minutes to hours)
Terminal statesStop polling on completed, failed, or canceled
EndpointSame POST /v1/human/agents/{agentId}/a2a/0.3.0 endpoint used for message/send

How the Chain Resolves

When agents use PollingRemoteA2aAgent from the Swarmd Python SDK, the full chain blocks until every sub-agent completes:
Human sends message/send to Agent A

Relay starts processing (30s timeout)

Agent A calls Agent B via relay

Agent B is HITL-blocked → relay masks as "working"

Agent A's PollingRemoteA2aAgent polls Agent B every 5s

Relay returns "working" to human (30s timeout exceeded)

Human polls tasks/get → "working"

Admin approves HITL → Agent B completes

Agent A receives Agent B's result → formulates combined answer

Relay stores completed result in task store

Human polls tasks/get → "completed" with full answer
The human gets one complete response that includes data from all agents in the chain. No partial answers, no manual stitching.

Conversation Continuity with HITL

HITL responses work with the standard conversation flow. After a HITL-blocked request resolves:
  1. Use contextId from the completed response for follow-up messages (maintains session history)
  2. Each follow-up is a new message/send with the contextId — the agent has full conversation context
{
  "jsonrpc": "2.0",
  "method": "message/send",
  "id": "msg-002",
  "params": {
    "message": {
      "contextId": "17aa30cf-...",
      "role": "user",
      "parts": [{"kind": "text", "text": "What other products does she qualify for?"}]
    }
  }
}

Next Steps

Human-in-the-Loop Setup

Configure HITL policies and manage approvals.

Policy Configuration

Set up detection policies that trigger HITL escalation.