Skip to content

Delegate over A2A

A2A (Agent2Agent) is the Linux Foundation protocol for one agent handing work to another across organizations and frameworks. Every eligible Workprentice agent serves it: an external orchestrator discovers the agent, reads its card, sends it a task, and collects the result — while the work itself runs as an ordinary session with the same log, the same work products, and the same audit trail as anything started from the app.

The surface pins protocol version 1.0 and authenticates with a workspace API key — the same wak_ credential webhooks and MCP clients use.

An agent is served over A2A when it is enabled, when it is shared with the key’s creator at the use tier or higher, and when it is not somebody’s personal agent. Personal agents carry their owner’s identity and private connections, so they are never exposed here.

Two ways to find the eligible ones. From an MCP client, list_agents returns each agent with its card_url. Or ask the index directly:

Terminal window
curl https://workprentice.ai/a2a/agents \
-H "Authorization: Bearer wak_..."
{
"agents": [
{
"id": "b3f1…",
"name": "Research",
"cardUrl": "https://workprentice.ai/a2a/b3f1…/.well-known/agent-card.json"
}
]
}

Each agent’s card sits under its own base URL, so a card resolver pointed at https://workprentice.ai/a2a/<agent-id> finds it at the nested well-known path:

Terminal window
curl https://workprentice.ai/a2a/<agent-id>/.well-known/agent-card.json \
-H "Authorization: Bearer wak_..."

The card is served as application/a2a+json and describes the agent as it is configured right now: its name, a description drawn from the opening of its personality, its icon, and a skills array advertising both its skills and its assignments — assignment entries name their parameters, which is what lets a calling framework phrase a request the agent can actually fill in. capabilities declares streaming true and push notifications false, and securitySchemes describes the HTTP bearer credential.

Cards are auth-gated. A caller needs a workspace key to invoke the agent anyway, and a public card would publish agent names, personas, and skill inventories to anyone who asked. The tradeoff is deliberate: anonymous crawlers cannot index your agents.

One endpoint, JSON-RPC 2.0 over POST /a2a/{agent_id}, and one required header: A2A-Version: 1.0. The spec reads an absent version header as a 0.3 request, so a call without it is refused with VersionNotSupportedError (-32009) rather than dispatched against a method vocabulary it does not share.

Terminal window
curl -X POST https://workprentice.ai/a2a/<agent-id> \
-H "Authorization: Bearer wak_..." \
-H "A2A-Version: 1.0" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "1",
"method": "SendMessage",
"params": {
"message": {
"messageId": "req-8f21",
"role": "ROLE_USER",
"content": [
{ "text": "Summarize Q3 pipeline movement and flag any deal that slipped twice." }
]
}
}
}'

The response is the task, abridged here:

{
"jsonrpc": "2.0",
"id": "1",
"result": {
"id": "9c2a…",
"contextId": "51ee…",
"status": {
"state": "TASK_STATE_SUBMITTED",
"timestamp": "2026-07-28T16:04:11Z"
},
"history": [
{
"messageId": "51ee…:12",
"role": "ROLE_USER",
"content": [{ "text": "Summarize Q3 pipeline movement…" }],
"taskId": "9c2a…"
}
]
}
}

Two ids come back. id is the task, which is one turn of work. contextId is the session that turn runs in — the same session a person opens in Workprentice to watch the agent work.

To carry the conversation on, send your next message with the taskId you were given: the agent picks up in the same session with everything it already knows, whether the task was waiting on you or had already completed. A message with no taskId starts a new task in a new session. Canceled, failed, and rejected tasks cannot be continued — there is no window left to reopen, so start a new task instead.

Your messageId is the idempotency key. A caller that retries a delivery with the same id gets the same task back instead of starting a second one, so a flaky network costs nothing.

A message’s content is a list of parts, each carrying exactly one member: text becomes the request itself, data becomes a block of JSON the agent reads as content, and raw carries base64 file bytes that land in the agent’s inputs. url parts are declined with ContentTypeNotSupportedError (-32005) — fetching caller-supplied URLs is a door this surface does not open.

State What it means
TASK_STATE_SUBMITTED Accepted; the agent is starting up.
TASK_STATE_WORKING The agent is on it.
TASK_STATE_INPUT_REQUIRED The agent asked a question and is waiting on an answer.
TASK_STATE_COMPLETED The turn closed with a final answer, and artifacts are attached.
TASK_STATE_FAILED The agent stopped before answering; the status message carries the reason.
TASK_STATE_CANCELED Canceled by CancelTask, or stopped from the app.
TASK_STATE_REJECTED Born terminal — the agent is paused and is not accepting delegated work.

Poll with GetTask, passing taskId (and historyLength when you want less transcript than the whole window):

{ "jsonrpc": "2.0", "id": "2", "method": "GetTask", "params": { "taskId": "9c2a…" } }

CancelTask takes the same parameter and stops the session along with everything it spawned. State is derived fresh on every read, so a GetTask answer is never a stale cache of what the agent was doing a moment ago.

On a completed task, the agent’s answer is the status message, and the fuller exchange sits in history — the messages carrying "role": "ROLE_AGENT". You can pass historyLength: 0 and still get the answer. In the other states the status message says what the state alone cannot: the question behind input-required, the reason behind failed or rejected.

Work products created during the turn come back as artifacts on the completed task. Each artifact carries a data part that points at the thing — its type, session and work-product ids, the authenticated API URL, and a web URL a person can open — followed by either the content inline as a text part (for textual content up to 64 KiB, which covers the usual “the agent wrote a report” case) or a url part for anything binary or larger. The API URL accepts the same Bearer key you already hold. No unauthenticated share links are minted for artifacts.

SendStreamingMessage starts a task and streams it; SubscribeToTask attaches to one already running. Both answer with text/event-stream, where every data: line is a complete JSON-RPC response whose result holds exactly one of task, message, statusUpdate, or artifactUpdate. Lines beginning : ping are keepalives.

The shape of a stream: the task snapshot first, then a statusUpdate per completed agent message, then artifactUpdate frames for the turn’s work products, then a final statusUpdate — after which the stream closes. SubscribeToTask replays nothing, because the opening snapshot already covers whatever you missed.

Fidelity is message granularity by design. The wire carries no token deltas and no thinking or tool-call interiors; an external orchestrator sees completed messages and state transitions, which is what it acts on. The full interior remains visible to people in the session log.

An agent that needs a decision asks for one, and the task moves to input-required with the question as its status message. Answer by sending a message that names the taskId:

{
"jsonrpc": "2.0",
"id": "3",
"method": "SendMessage",
"params": {
"message": {
"messageId": "req-8f22",
"role": "ROLE_USER",
"taskId": "9c2a…",
"content": [{ "text": "Use the FY26 plan numbers." }]
}
}
}

The task returns to working and the window reopens. A person can answer the same question from their inbox, and if both of you answer, nothing breaks: the replies post into the same session, and the agent’s next final message closes the task.

This surface is built to be driven by unattended software, so it carries caps the equivalent REST calls do not:

Limit Default Refusal
A2A-occupied sessions in a workspace at once 10 429 with no Retry-After — nothing ages out, but a later attempt may fit.
Fresh turns started per sliding hour, per workspace 50 429 with Retry-After giving the real deadline.
Concurrent streams held by one key 20 429.

The two workspace caps are tunable per organization or per workspace, so a team driving the surface hard can be given more room without loosening it everywhere. Continuing a task whose agent is already running does not count against the hourly figure — only turns that start fresh work do.

Money is bounded elsewhere: an A2A turn draws on the same monthly spend cap and credit balance as any other turn, and on a session budget wherever one is set, so there is no separate A2A dollar cap to configure.

Those refusals follow the general rule for errors here: anything an HTTP client already knows how to handle stays at the transport level — authentication failures (401, 403), rate refusals (429), and an agent the key may not reach (404, since a caller who cannot delegate to an agent has no business learning it exists). Everything else a caller can act on arrives as a 200 carrying a JSON-RPC error envelope.

  • Push notifications. The configuration methods answer PushNotificationNotSupportedError (-32003); the card declares pushNotifications: false. Poll GetTask or stream.
  • ListTasks answers UnsupportedOperationError (-32004). Track the task ids you were given.
  • GetExtendedAgentCard answers with the extended-card-not-configured error (-32007), matching the card’s extendedAgentCard: false.
  • url content parts are declined (-32005); send text, data, or raw bytes.
  • Personal agents are not served, and a paused agent rejects delegated work rather than queuing it — pausing an agent is how you take it off this surface without touching any key.

For the complete site index, see llms.txt.