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.
Discovery
Section titled “Discovery”An agent is served over A2A when the key’s creator can use it — their own agent, or one shared with them at Can use or higher — when it is not paused, 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. A paused agent (its Automated runs switch is off) drops out of the index and serves no card until it is resumed.
The index lists exactly the eligible agents:
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" } ]}An MCP client can also find cards through
list_agents, which returns a card_url for every agent the caller can see.
That list is broader than the index: a paused or personal agent appears in it
too, and its card URL answers 404.
The agent card
Section titled “The agent card”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:
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.
Starting a task
Section titled “Starting a task”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.
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.
Task lifecycle
Section titled “Task lifecycle”| 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.
What comes back
Section titled “What comes back”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.
Streaming
Section titled “Streaming”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.
Answering a question
Section titled “Answering a question”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.
Limits
Section titled “Limits”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, counted on each server separately | 20 | 429. |
An admin can raise the two workspace caps up to the plan’s ceiling under Guardrails → Limits & budgets in organization or workspace settings, and Pro holds fresh turns to 25 an hour; see Limits.
Only a message that starts a turn is counted. Answering a task whose agent is still running — replying to its question, say — counts against neither workspace cap; a follow-up to a task whose agent has gone idle starts a new turn and counts like a fresh one.
Money is bounded elsewhere. There is no separate A2A dollar cap: an A2A turn counts toward a monthly spend limit or a session budget exactly as any other turn does, wherever one is configured.
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 — among them
TaskNotFoundError (-32001) for a task id the key cannot see, and
TaskNotCancelableError (-32002) for canceling a task that has already
finished.
Not supported
Section titled “Not supported”- Push notifications. The configuration methods answer
PushNotificationNotSupportedError(-32003); the card declarespushNotifications: false. PollGetTaskor stream. ListTasksanswersUnsupportedOperationError(-32004). Track the task ids you were given.GetExtendedAgentCardanswers with the extended-card-not-configured error (-32007), matching the card’sextendedAgentCard: false.urlcontent parts are declined (-32005); sendtext,data, orrawbytes.- 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.
Related
Section titled “Related”A2A is for frameworks that hand an agent a whole task. If the caller is an MCP client — Claude Code, Claude Desktop, a script, or another Workprentice agent — and you want to decide which tools it sees and what each one asks the agent to do, give the agent an API instead: Give an Agent an API.
For the complete site index, see llms.txt.