Skip to navigation

How A2A works

An A2A Surface lets external AI agents discover and call your Product’s Capabilities through the Agent-to-Agent (A2A) protocol. A caller reads the Surface’s Agent Card, then sends JSON-RPC requests that run a Capability as an A2A task. Frameworks such as LangChain, CrewAI, and Vercel AI SDK can call the Surface, and so can agents in a partner organization, without a custom integration.

Runtype A2A Surfaces use the A2A Protocol v1.0 wire format.

How discovery works

The Agent Card describes the Surface’s name, skills, JSON-RPC endpoint, and authentication schemes. The following command fetches it:

curl https://api.runtype.com/v1/products/YOUR_PRODUCT_ID/surfaces/YOUR_SURFACE_ID/a2a/.well-known/agent-card.json

Replace the following:

  • YOUR_PRODUCT_ID: the ID of the Product
  • YOUR_SURFACE_ID: the ID of the A2A Surface

The Agent Card is public, so the request needs no key. Runtype serves the card only while the Surface is active. For an inactive Surface, the request returns 404. Runtype marks the card as cacheable for up to one hour, so a client that caches it can take up to an hour to see a changed name or skill list.

Each entry in the card’s skills array is one skill that a caller can run. The skill id is the exposed name of the Surface item, or the Capability name when you set no exposed name. An external agent Capability adds one skill for each skill that the external agent exposes. Pass that id as params.metadata.skill to run the skill.

For a lightweight connectivity check, send GET to /v1/products/YOUR_PRODUCT_ID/surfaces/YOUR_SURFACE_ID/a2a/info. The public response includes the protocol version, the JSON-RPC endpoint, and the Agent Card URL.

How invocation works

A caller sends every request as a JSON-RPC 2.0 POST to the A2A endpoint, with an A2A Surface key. The following command runs the answer_questions skill:

curl -X POST https://api.runtype.com/v1/products/YOUR_PRODUCT_ID/surfaces/YOUR_SURFACE_ID/a2a \
-H "Authorization: Bearer a2a_YOUR_SURFACE_KEY" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": "req-1",
"method": "SendMessage",
"params": {
"message": {
"role": "ROLE_USER",
"parts": [{ "text": "What is your return policy?" }],
"messageId": "msg_123"
},
"metadata": { "skill": "answer_questions" }
}
}'

Replace a2a_YOUR_SURFACE_KEY with an A2A Surface key. You can also send the A2A Surface key in an X-API-Key header instead of Authorization.

The endpoint accepts these methods:

  • SendMessage: runs a task and returns it after it finishes.
  • SendStreamingMessage: runs a task and streams updates over Server-Sent Events (SSE).
  • GetTask: gets a task by ID.
  • ListTasks: lists tasks with cursor pagination.
  • CancelTask: cancels a task that hasn’t finished.
  • SubscribeToTask: resumes the SSE stream for a task.

When SendMessage or SendStreamingMessage starts a Runtype run, the HTTP response carries the run’s ID in the X-Runtype-Execution-Id header. A request refused before a run starts carries no header, and neither does a skill backed by an external agent, which runs outside Runtype. A SendStreamingMessage call to a Claude-managed skill also carries no header: its stream starts before the run does, and the run’s ID is the task ID that the stream reports. Use the ID to correlate logs, quote it in a support request, or open the run with GET /v1/runs/{executionId} and an API key from your Runtype account.

The endpoint also accepts the slash-style method names that A2A 0.3 clients send, such as message/send. Use the v1.0 names in new integrations.

How requests route to a skill

The Surface’s orchestration mode decides which skill runs when a request names none:

  • Delegate mode lists each Capability as a separate skill. The caller selects a skill with params.metadata.skill.
  • Managed mode adds an orchestrator skill to the Agent Card, named assistant unless you set another Skill name. If a Capability already uses that name, Runtype adds _orchestrator to the orchestrator skill’s name. The orchestrator skill routes a request to a matching Capability. Callers can still select an individual skill by name.

In Managed mode, omit params.metadata.skill to send the request to the orchestrator skill. In Delegate mode, always set params.metadata.skill. If you omit it, Runtype runs the first Capability that the key can run instead of returning an error. A scoped key can’t use the orchestrator skill, so the same fallback applies to a scoped key in Managed mode.

For more information about the two modes, see Surface orchestration modes.

What the Capability receives

For a Flow or Agent Capability, Runtype joins the text parts of params.message with newlines and passes the result as the user message. Runtype accepts data, url, and raw parts but doesn’t pass them to a Flow or Agent Capability.

A contextId groups related tasks, and you can filter ListTasks by it. It doesn’t replay earlier turns to a Flow or Agent Capability, so each task sees only the current message. To continue a conversation, include the context that the Capability needs in the message text.

An external agent Capability is different: Runtype forwards the full message, including every part and the contextId, to the external agent.

Limitations

A2A Surfaces have these limits:

  • A2A can’t run a Capability that pauses for approval or input. If a Capability’s Flow has a pause step, or its Agent requires tool approval, the request fails with -32603. For SendMessage, the error message starts with Task execution failed:. For SendStreamingMessage, the stream sends a task/error event. Remove pause steps and required tool approvals from Capabilities that you expose on an A2A Surface.
  • Runtype doesn’t deliver push notifications. It accepts params.configuration.pushNotificationConfig but never calls the webhook. To detect completion, use SendStreamingMessage, SubscribeToTask, or poll with GetTask.
  • SendMessage returns after the task finishes and ignores params.configuration.blocking. For a long-running task, use SendStreamingMessage.

Message format

Requests and responses follow these v1.0 conventions:

  • Each message part has exactly one of text, data, url, or raw: { "text": "Hello" }, { "data": { "key": "value" } }, { "url": "https://example.com/file.pdf" }, or { "raw": "YWJj" }. Don’t add a kind field.
  • A message’s role is ROLE_USER or ROLE_AGENT.
  • Task states use TASK_STATE_* values.
  • A SendMessage result holds the task in its task field.
  • Each SSE event’s result contains either statusUpdate or artifactUpdate. To detect completion, watch for a terminal task state and the end of the stream.

Method examples

The following examples show the params and results for each method. Send each one in the same JSON-RPC envelope and with the same headers as the invocation example.

SendMessage

A successful SendMessage response wraps the finished task in result.task:

{
"jsonrpc": "2.0",
"id": "req-1",
"result": {
"task": {
"id": "atask_01jq8z7m1nd4e5f6g7h8j9k0p1",
"contextId": "ctx_1767225600000_a1b2c3d",
"status": { "state": "TASK_STATE_COMPLETED" },
"artifacts": [
{
"artifactId": "3f2b8c1e-7d4a-4b9e-a5c6-2e8f1d0b9a47",
"name": "response",
"parts": [{ "text": "Our return policy allows returns within 30 days." }]
}
],
"metadata": {}
}
}
}

To group a follow-up task with this one, set contextId in params.message.

SendStreamingMessage

SendStreamingMessage takes the same params as SendMessage. Each data: line in the stream holds a JSON-RPC response envelope. A stream is similar to the following:

event: task/status
data: {"jsonrpc":"2.0","id":"req-1","result":{"statusUpdate":{"taskId":"atask_01jq8z7m1nd4e5f6g7h8j9k0p1","contextId":"ctx_1767225600000_a1b2c3d","status":{"state":"TASK_STATE_WORKING"}}}}
event: task/artifact
data: {"jsonrpc":"2.0","id":"req-1","result":{"artifactUpdate":{"taskId":"atask_01jq8z7m1nd4e5f6g7h8j9k0p1","contextId":"ctx_1767225600000_a1b2c3d","artifact":{"artifactId":"3f2b8c1e-7d4a-4b9e-a5c6-2e8f1d0b9a47","name":"response","parts":[{"text":"Our return policy "}]},"append":false,"lastChunk":false}}}
event: task/artifact
data: {"jsonrpc":"2.0","id":"req-1","result":{"artifactUpdate":{"taskId":"atask_01jq8z7m1nd4e5f6g7h8j9k0p1","contextId":"ctx_1767225600000_a1b2c3d","artifact":{"artifactId":"3f2b8c1e-7d4a-4b9e-a5c6-2e8f1d0b9a47","parts":[{"text":"allows returns within 30 days."}]},"append":true,"lastChunk":true}}}
event: task/status
data: {"jsonrpc":"2.0","id":"req-1","result":{"statusUpdate":{"taskId":"atask_01jq8z7m1nd4e5f6g7h8j9k0p1","contextId":"ctx_1767225600000_a1b2c3d","status":{"state":"TASK_STATE_COMPLETED"}}}}

A task/error event reports a failure. Parse the JSON-RPC payload in data, and use the event name only for debugging. The stream closes after the terminal status.

GetTask

GetTask takes the task id and an optional historyLength, which sets how many history messages to include:

{
"jsonrpc": "2.0",
"id": "req-2",
"method": "GetTask",
"params": {
"id": "atask_01jq8z7m1nd4e5f6g7h8j9k0p1",
"historyLength": 10
}
}

ListTasks

ListTasks lists the Surface’s tasks, most recently updated first, with pageSize and pageToken for cursor pagination:

{
"jsonrpc": "2.0",
"id": "req-3",
"method": "ListTasks",
"params": {
"pageSize": 25,
"status": "TASK_STATE_COMPLETED",
"includeArtifacts": false
}
}

pageSize defaults to 50 and accepts up to 100. You can filter by contextId or status. Set includeArtifacts to true to include artifacts. The default is false. The result includes tasks, nextPageToken, pageSize, and totalSize.

CancelTask

CancelTask takes the id of a task that hasn’t finished:

{
"jsonrpc": "2.0",
"id": "req-4",
"method": "CancelTask",
"params": {
"id": "atask_01jq8z7m1nd4e5f6g7h8j9k0p1"
}
}

A successful cancel returns the task with the TASK_STATE_CANCELED state. Canceling a task that already completed, failed, or was canceled returns -32005.

SubscribeToTask

SubscribeToTask takes the id of an existing task and resumes its SSE stream:

{
"jsonrpc": "2.0",
"id": "req-5",
"method": "SubscribeToTask",
"params": {
"id": "atask_01jq8z7m1nd4e5f6g7h8j9k0p1"
}
}

Task states

A task reports one of these states:

StateDescription
TASK_STATE_SUBMITTEDTask received and queued.
TASK_STATE_WORKINGTask execution is in progress.
TASK_STATE_COMPLETEDTask execution finished successfully.
TASK_STATE_FAILEDTask execution failed.
TASK_STATE_CANCELEDTask execution was canceled.
TASK_STATE_INPUT_REQUIREDTask requires additional input.
TASK_STATE_AUTH_REQUIREDTask requires authentication or authorization.
TASK_STATE_REJECTEDTask was rejected.
TASK_STATE_UNSPECIFIEDRuntype can’t determine the task state.

No A2A method resumes a task that is waiting for input or authorization.

Errors

Runtype returns errors as JSON-RPC error objects with a numeric code, a message, and a data array. The data array holds a google.rpc.ErrorInfo entry with a stable reason. When Runtype has extra detail, such as the list of valid skills, it adds the detail to metadata.detail as a JSON string:

{
"jsonrpc": "2.0",
"id": "req-1",
"error": {
"code": -32002,
"message": "Skill not found: answer_question",
"data": [
{
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
"reason": "SKILL_NOT_FOUND",
"domain": "a2a-protocol.org",
"metadata": { "detail": "{\"availableSkills\":[\"answer_questions\"]}" }
}
]
}
}

The Surface returns these standard JSON-RPC error codes:

CodeReasonMeaning
-32700PARSE_ERRORThe request isn’t valid JSON.
-32600INVALID_REQUESTThe request is invalid, or a daily execution limit was reached.
-32601METHOD_NOT_FOUNDThe method doesn’t exist.
-32602INVALID_PARAMSThe method parameters are invalid.
-32603INTERNALThe task couldn’t run, for example because the Capability pauses for approval.

The following Runtype A2A Surface error codes are Runtype’s own. Other A2A servers can use these numbers for different errors, so branch on reason rather than on the numeric code:

CodeReasonMeaning
-32001TASK_NOT_FOUNDThe task doesn’t exist, or the A2A Surface key can’t see it.
-32002SKILL_NOT_FOUNDThe skill doesn’t exist, or the A2A Surface key can’t run it.
-32003UNAUTHORIZEDThe A2A Surface key is missing or invalid, or the skill can’t run.
-32004RATE_LIMITEDThe A2A Surface key exceeded its rate limit.
-32005TASK_CANCELEDThe task already finished, so it can’t be canceled.
-32006CONTEXT_INVALIDReserved for an invalid context. Runtype doesn’t currently send it.

When a key exceeds its rate limit, the response includes a Retry-After header with the number of seconds to wait. The streaming methods return the same error as a task/error event.

A Test key runs up to 50 tasks per day, and the count resets at midnight UTC. When a Test key reaches the limit, Runtype returns -32600 with a message that names the limit. Use a Production key for production traffic. Your account’s daily execution quota also applies, and reaching it also returns -32600.

Access control

An A2A Surface controls access in these ways:

  • Public Agent Card: anyone can read the Agent Card. Every JSON-RPC request requires a valid A2A Surface key, which starts with a2a_.
  • Rate limits: each A2A Surface key has its own per-minute and per-day limits.
  • Capability scoping: a scoped A2A Surface key runs only the Capabilities in its scope.
  • Execution logs: each A2A task appears in your execution records.

A scoped key never receives 403 on an A2A Surface. A skill outside the key’s scope returns -32002, and the error lists only the skills that the key can run. The orchestrator skill isn’t available to a scoped key. GetTask and CancelTask return -32001 for tasks outside the key’s scope, and ListTasks leaves those tasks out.

Next steps