This guide covers how to integrate your frontend application with Swarmd agents. It applies to both channel-based access (for apps, bots, and services) and user-based access (for logged-in humans). Both access types use the same JSON-RPC protocol and follow the same request/response patterns — the only difference is the endpoint and how you authenticate.
Channels authenticate using the OAuth2 client credentials flow. You received a clientId and clientSecret when the channel was created (see Your First Agent).
The client_id is always the channel’s clientId field — formatted as channel-{channelId}. The token is short-lived (typically 5 minutes). Your app should refresh it before expiry by repeating the same request.
This endpoint requires a user Bearer token (from login), not a channel token. Channel management operations (creating, subscribing, listing) are admin actions performed by logged-in users.
There are two distinct scenarios depending on whether you call the agent directly or through a chain. These apply regardless of whether you use channel or user access.
You call Agent A, which delegates to Agent B. Agent B’s response triggers HITL review. Agent A waits for Agent B to resolve before responding to you.
Your App → Relay → Agent A → Relay → Agent B ↓ HITL triggered ↓ Agent A waits (polling) ↓ Relay times out, returns "working" to you ↓ You poll until resolved
In both flows, your app follows the same polling pattern. The difference is what the final response contains.
The relay has taken ownership of this task and returned immediately so your request doesn’t hang. The metadata.relay_reason field tells you why.How to detect:result.status.state === "working" and result.metadata.relay_reason is present.Action (all cases):
Save result.id — this is the task ID you’ll poll with
A detection policy (regex, Presidio, Comprehend) matched the message content and escalated it for human review. The metadata includes the triggering policy details so your UI can display context.
HITL_HELD_AGENT_INPUT_REQUIRED — Agent Requested Human Input
The downstream agent explicitly returned INPUT_REQUIRED, requesting human confirmation before proceeding. No policy was involved — the agent made this decision itself.
Action: Show “This request was not approved”. No agent response is available.
The HITL_REJECTED reason can appear both in the initial message/send response (if the rejection happened before the relay returned) and in tasks/get poll responses. The policy_name, policy_version, and policy_level fields are nullable — they are null when the original hold was agent-initiated rather than policy-triggered.
An admin approved a downstream agent’s HITL request. The parent agent received the downstream result, combined it with its own data, and returned a single complete answer.
{ "jsonrpc": "2.0", "id": 2, "result": { "id": "bfe9c650-...", "contextId": "64018875-...", "status": { "state": "completed", "message": { "role": "agent", "parts": [ { "kind": "text", "text": "The time in London is 18:00, and the weather is rainy." } ] }, "timestamp": "2026-03-28T20:28:58.595091+00:00" }, "artifacts": [ { "artifactId": "97c87236-...", "parts": [ { "kind": "text", "text": "The time in London is 18:00, and the weather is rainy." } ] } ] }}
Action: Stop polling. Render the response. This is the complete, combined answer from the entire chain. No partial answers to stitch together.
An admin rejected a downstream agent’s HITL request. The parent agent continues without the downstream agent’s data and responds with whatever information it had on its own.
{ "jsonrpc": "2.0", "id": 2, "result": { "id": "d978cc99-...", "contextId": "80d34dcc-...", "status": { "state": "completed", "message": { "role": "agent", "parts": [ { "kind": "text", "text": "The time in London is 18:00." } ] } }, "artifacts": [ { "artifactId": "ec988402-...", "parts": [ { "kind": "text", "text": "The time in London is 18:00." } ] } ] }}
Action: Stop polling. Render the response. Notice the weather data is missing because the downstream weather agent was rejected. The parent agent returned only what it could provide on its own.
In a chain rejection, the response state is completed (not canceled) because the parent agent did complete — just without the blocked sub-agent’s input. Your app should render this as a normal response. The user may not know data is missing unless the agent mentions it.
No data is leaked when a downstream agent is rejected. The rejected agent’s response is never forwarded — the parent agent receives null for the delegation and has no access to the blocked agent’s data. The parent can only respond with information it already had independently (in this example, the time). The HITL rejection acts as a hard gate: if it’s rejected, that agent’s data does not flow anywhere in the chain.
Receive message/send response | vHas metadata.relay_reason? YES -> Check relay_reason: | TIMEOUT -> Show "Processing..." (no human action needed) HITL_HELD -> Show "Pending review - policy: {policy_name}" HITL_HELD_AGENT_INPUT_REQUIRED -> Show "Agent requires approval" HITL_REJECTED -> Show "Request not approved" | If state is "working": Save result.id, poll tasks/get every 5s Stop on: completed, canceled, or failed If state is "canceled": Stop. Show rejection message. NO | vIs state "completed"? YES -> Render response. Done.
After a request resolves (whether HITL-blocked or not), use contextId from the response for follow-up messages. The agent retains the full conversation history.
While the request/response format is identical, there are differences in how HITL policies are applied:
Aspect
Channel Access
User Access
Policy scoping
Tenant-level policies only
Tenant + user-level policies
Skill filtering
All agent skills visible (unfiltered)
Skills filtered per user’s permissions
HITL trigger
Agent-initiated INPUT_REQUIRED always applies; policy-based HITL applies if bound at tenant level
Both agent-initiated and user-scoped policy-based HITL apply
Audit tracking
Records channelId and channelSource
Records sourceUserId
Both channel and user access support the full HITL workflow. The only difference is which policies are evaluated. If you’ve bound a HITL detection policy at the tenant level, it will apply to channel requests too. User-scoped policies (bound to specific users) only apply to user access.