Skip to main content
New TypeScript integrations should use @swarmd/channel-client. It wraps the Conversation REST API, OAuth token refresh, polling, reply extraction, and typed lifecycle events. The JSON-RPC path documented here still works and remains supported; this guide is kept for existing integrations and protocol-level reference.

Frontend Integration (JSON-RPC — legacy)

This guide covers how to integrate your frontend application with Swarmd agents using the JSON-RPC a2a/0.3.0 protocol. 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.

Access Types at a Glance


Authentication

Channel Authentication

Channels authenticate using the OAuth2 client credentials flow. You received a clientId and clientSecret when the channel was created (see Your First Agent).
Response:
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.

User Authentication

Users authenticate via the Swarmd login endpoint:
Response:
Refresh before expiry:

Listing Available Agents

Before invoking an agent, your frontend may need to display a list of available agents.

Channel: List Subscribed Agents

Response:
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.

User: List Subscriptions


Sending a Request

Every interaction starts with a message/send call. The request body is identical for both access types — only the endpoint differs.

The Two Flows

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.

Flow 1: Direct Agent Call

You call an agent directly and that agent’s response triggers HITL review.

Flow 2: Multi-Agent Chain

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.
In both flows, your app follows the same polling pattern. The difference is what the final response contains.

Every Response Your App Can Receive

1. completed — No Review Needed

The agent responded immediately with no HITL involved. Render the response and you’re done.
Action: Render result.status.message. No polling needed. How to detect: result.status.state === "completed".

2. working — Relay-Managed Task, You Must Poll

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):
  1. Save result.id — this is the task ID you’ll poll with
  2. Start polling tasks/get (see How to Poll below)
  3. Show context-appropriate UI based on relay_reason (see below)

TIMEOUT — Agent is Slow

The agent didn’t respond within the relay’s early-return timeout. The relay is still waiting for the agent in the background.
UI suggestion: Show a “Processing…” or spinner. No human action is required — the agent is still working.

HITL_HELD — Policy Escalation

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.
UI suggestion: Show “Pending review” with the policy name, e.g. “Held by policy: Large Transaction Policy”.

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.
UI suggestion: Show “Agent requires approval” — this is a confirmation step initiated by the agent, not a compliance check.

3. canceled — HITL Rejected

A human reviewer rejected the HITL request. If a policy triggered the original hold, the metadata includes the policy details.
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.

relay_reason Reference

policy_level Values


How to Poll

Send a tasks/get request to the same endpoint you used for the original message/send:
Poll every 5 seconds until you receive a terminal state.

Poll Responses

Still Pending

The review hasn’t been resolved yet. Keep polling.
Action: Continue polling. Show “Pending review” in your UI.

Approved — Direct Agent

An admin approved the request. The agent processed it and returned a result.
Action: Stop polling. Render result.artifacts[0].parts or result.status.message as the agent’s response.

Approved — Multi-Agent Chain

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.
Action: Stop polling. Render the response. This is the complete, combined answer from the entire chain. No partial answers to stitch together.

Rejected — Direct Agent

An admin rejected the request. The task is canceled and the agent did not process it.
Action: Stop polling. Show a message like “This request was not approved” in your UI. There is no agent response to render.

Rejected — Multi-Agent Chain

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.
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.

Terminal States Reference

Stop polling when you see any of these states:

Decision Flowchart


Polling Best Practices

The polling endpoint is the same as the endpoint you used for message/send:

Key Fields Reference


Conversation Continuity

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.

Channel vs User: Differences for HITL

While the request/response format is identical, there are differences in how HITL policies are applied:
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.

Summary


Next Steps

Human-in-the-Loop Setup

Configure HITL policies and manage approvals.

Policy Configuration

Set up detection policies that trigger HITL escalation.