A2A Protocol: How AI Agents Discover and Delegate to Peers

A2A, short for Agent2Agent, is an open standard that lets one AI agent delegate work to another agent built by a different team or vendor. It was announced by Google in April 2025, donated to the Linux Foundation, and reached version 1.0.0 as its first stable specification. Its defining assumption is opaque execution: agents cooperate through declared capabilities without exposing their internal plans or tools.
MCP connects an agent to tools whose schemas it is allowed to read, usually inside one organisation. A2A connects an agent to another agent it cannot see inside, typically across a trust boundary, so authentication and a task lifecycle are part of the protocol itself. They are complementary layers rather than competitors, and most real systems will speak both.
An Agent Card is a JSON document describing an agent's identity, provider, version, skills, supported transports and required security schemes. The specification's primary discovery mechanism is a fixed path on the agent's own domain, /.well-known/agent-card.json, though registries and direct configuration are also allowed. Cards may be signed with JSON Web Signature after canonicalisation, so a client can verify the skills list has not been altered.
They are the two interrupted states in A2A's task lifecycle. TASK_STATE_INPUT_REQUIRED means the remote agent needs more information from a human before it can continue, and TASK_STATE_AUTH_REQUIRED means it needs a credential. Both are distinct from the four terminal states, and both come with a task id you use to resume the same task rather than starting a new one.
Publishing an Agent Card is cheap and worth doing: it is a static JSON file plus an endpoint that answers SendMessage for one narrow skill. Building the calling side is where the cost sits, because a caller must persist task ids, handle interrupted states, keep an update channel alive and manage credentials. A reasonable rule is to publish first and only build the consumer half once a named partner asks to be called.

Key Takeaway
The A2A protocol, now a Linux Foundation project at version 1.0.0, lets one AI agent delegate work to another it cannot see inside. Agents publish a JSON Agent Card at a well-known URL, then exchange tasks whose lifecycle includes interrupted states for missing input and missing credentials rather than pretending delegation always completes.
Every MCP server I have shipped ends at the same boundary. It exposes functions I own, running on infrastructure I control, described by a schema I wrote. That is fine while both ends of the connection are mine. It stops being fine the moment the work belongs to someone else's system, because you cannot publish a tool schema for a process you are not allowed to see inside.
So I read the A2A specification the way an ERP developer reads any integration standard: looking for what it says about the parts that go wrong. This post is that read. Everything technical here comes from the A2A v1.0.0 specification and the Linux Foundation's one-year release, and the conclusion at the end is mine.
The two protocols look adjacent and are not. An MCP server is a surface you are allowed to read: every tool has a name, a JSON schema and a result type, and the client's job is to pick one and call it. A2A starts from the opposite premise. Its guiding principles list Opaque Execution, which the specification defines as agents collaborating on declared capabilities and exchanged information, without needing to share their internal thoughts, plans or tool implementations.
| Question | MCP | A2A |
|---|---|---|
| What is on the other end | A tool surface: named functions with declared schemas | An agent that plans, and may delegate onward itself |
| How you find it | Client config, a registry entry, or a command that spawns it | An Agent Card fetched from a well-known URL on the peer's domain |
| What a call returns | A tool result, now | A task with a lifecycle that may stall for hours |
| Who owns a failure | You do, because it is your server | The peer does, and you only see the state it chooses to publish |
| Where the trust boundary sits | Usually inside one organisation | Usually across two, which is why auth is in the protocol itself |
That one principle changes every design decision downstream. You cannot validate a peer's arguments, retry its individual steps, or reason about how long it should take, because none of that is exposed to you. What you get is a declared skill, a task id and a state. The protocol's whole job is to make that thin interface survive a partner's outage, a partner's approval queue and a partner's identity provider.
An A2A server must publish an Agent Card, and the specification's first discovery mechanism is a fixed path on the server's own domain: /.well-known/agent-card.json. The card carries identity, provider, version, the skills on offer, the transports the agent speaks in preference order, and the security schemes a caller has to satisfy. Discovery and authentication live in the same document, which is the part I did not expect.
# The entire discovery surface: one cacheable document at a fixed path.
GET /.well-known/agent-card.json HTTP/1.1
Host: erp.contoh.co.id
{
"name": "Delivery Scheduling Agent",
"description": "Reserves dock time against warehouse capacity for a confirmed purchase order.",
"provider": { "organization": "Contoh Logistik", "url": "https://erp.contoh.co.id" },
"version": "1.4.0",
// Ordered and normative: a client MUST take the first binding it speaks,
// not the one it would rather use.
"supportedInterfaces": [
{ "url": "https://erp.contoh.co.id/a2a/v1", "protocolBinding": "JSONRPC", "protocolVersion": "1.0" },
{ "url": "https://erp.contoh.co.id/a2a/json", "protocolBinding": "HTTP+JSON", "protocolVersion": "1.0" }
],
"capabilities": { "streaming": true, "pushNotifications": true, "extendedAgentCard": false },
// Discovery and auth are the same document. A caller learns what you can do
// and which token you want in one GET.
"securitySchemes": {
"partner-oidc": {
"openIdConnectSecurityScheme": {
"openIdConnectUrl": "https://sso.contoh.co.id/.well-known/openid-configuration"
}
}
},
"securityRequirements": [
{ "schemes": { "partner-oidc": { "list": ["openid", "delivery.write"] } } }
],
"defaultInputModes": ["application/json", "text/plain"],
"defaultOutputModes": ["application/json"],
"skills": [
{
"id": "book-delivery-slot",
"name": "Book a delivery slot",
"description": "Reserves a dock and a time window for a confirmed purchase order.",
"tags": ["logistics", "scheduling", "erp"],
"examples": ["Book the earliest slot for PO-2026-0184 at the Semarang DC."],
"inputModes": ["application/json"],
"outputModes": ["application/json"]
}
]
}The comments in that card are mine; the real file is plain JSON, and the field names come straight from the specification's own sample. Two details are worth stealing even if you never speak A2A. The supportedInterfaces array is ordered and normative, so a client must select the first binding it supports rather than the one it prefers. And the card is cacheable by design: servers should send Cache-Control and an ETag derived from the version field, and clients should revalidate with If-None-Match instead of re-downloading a document that changes a few times a year.
Cards may also be signed with JSON Web Signature, and the specification is specific about the step that is easy to get wrong. Before signing, the content must be canonicalised with the JSON Canonicalization Scheme, so the same card serialised by two different libraries produces the same bytes and therefore the same signature. A signed card is how a partner proves the skills list came from them and not from whoever last edited your configuration.

Treat the version field as your cache key. Bump it whenever a skill, a transport or a security scheme changes, derive the ETag from it, and every partner's If-None-Match request costs you a 304 instead of a full card.
A2A v1.0.0 defines three protocol bindings over one data model, and permits custom ones: JSON-RPC 2.0, gRPC, and HTTP+JSON/REST. The method names are the same across all three, which is the point of separating the bindings from the model: SendMessage and SendStreamingMessage, GetTask and ListTasks, CancelTask, SubscribeToTask, the push-notification config methods, and GetExtendedAgentCard. Choosing a binding is a plumbing decision. Choosing the interaction pattern is not.
# 1. Delegate. SendMessage is BLOCKING by default, and "blocking" means it
# returns when the task reaches a terminal state OR an interrupted one --
# not when the work is finished.
POST /a2a/v1 HTTP/1.1
Host: erp.contoh.co.id
Content-Type: application/json
Authorization: Bearer eyJhbGciOi...
{
"jsonrpc": "2.0",
"id": 1,
"method": "SendMessage",
"params": {
"message": {
"role": "ROLE_USER",
"messageId": "8f31c0d2-4c1e-4f9a-9a3e-1b70b0c2e5aa",
"parts": [{ "text": "Book the earliest slot for PO-2026-0184 at the Semarang DC." }]
},
"configuration": {
"returnImmediately": false,
"acceptedOutputModes": ["application/json"]
}
}
}
# 2. It did not complete. It stalled -- and said so, with a question attached.
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"task": {
"id": "9f2c1e40-6d55-4d29-9d2f-3b6c9a7f1e04",
"contextId": "3a77b512-1f0c-49a4-bd6e-2f2a0e5d77c1",
"status": {
"state": "TASK_STATE_INPUT_REQUIRED",
"message": {
"role": "ROLE_AGENT",
"parts": [{ "text": "Two docks are free on 11 Sep. Which one, and is a tail lift needed?" }]
}
}
}
}
}
# 3. Resume the SAME task by echoing its id. Sending a fresh message with no
# taskId opens a DIFFERENT task -- and an opaque peer will not tell you it
# just booked a second slot.
{
"jsonrpc": "2.0",
"id": 2,
"method": "SendMessage",
"params": {
"message": {
"taskId": "9f2c1e40-6d55-4d29-9d2f-3b6c9a7f1e04",
"contextId": "3a77b512-1f0c-49a4-bd6e-2f2a0e5d77c1",
"role": "ROLE_USER",
"messageId": "b41d7f66-90a2-4c7d-8f1b-5c2d9e3a4f09",
"parts": [{ "text": "Dock 2, tail lift required." }]
}
}
}The line that is easiest to skim is the one that matters. SendMessage is blocking by default, and blocking means it returns when the task reaches a terminal state or an interrupted one. A stalled task is a successful response, not an error. Set returnImmediately to true and you get the task id straight away, at the cost of having to poll GetTask, hold a SubscribeToTask stream, or register a webhook yourself.
Eight task states are defined, and the two most useful ones are the two that are not endings. TASK_STATE_INPUT_REQUIRED means the agent needs more from a human before it can proceed. TASK_STATE_AUTH_REQUIRED means it needs a credential. The specification calls both of these interrupted states and keeps them separate from the four terminal ones, which are completed, failed, canceled and rejected. That distinction is the most honest thing in the document.
Anyone who has built an ERP approval chain recognises the shape instantly. A purchase requisition does not fail because the manager is on leave; it waits. Most integration protocols have no vocabulary for waiting, so they express it as a timeout, and a timeout is a lie about whose turn it is. A2A gives the stall a state, a status message explaining what is needed, and a task id to resume against. The in-task authorization rules are worth reading in full, but four of them decide your design:

The stall can end without telling you. Credentials often arrive out of band, and the agent may resume processing immediately with no follow-up message from the client, so a caller holding no stream, webhook or poll misses the completion entirely and reports a hung task that actually finished hours ago.
Once a task reaches completed, canceled, rejected or failed it cannot be restarted. Any refinement is a new task carrying the same contextId, optionally naming the earlier one through referenceTaskIds. The reasoning in the specification is audit clarity and I agree with it, but it moves real work onto the caller: you now own a small durable state machine per delegation instead of a request and a response.
// Terminal is terminal: a completed, failed, canceled or rejected task cannot
// be restarted. Follow-up work is a NEW task carrying the same contextId.
const TERMINAL = new Set([
"TASK_STATE_COMPLETED",
"TASK_STATE_FAILED",
"TASK_STATE_CANCELED",
"TASK_STATE_REJECTED",
]);
const INTERRUPTED = new Set([
"TASK_STATE_INPUT_REQUIRED", // a human still has to answer something
"TASK_STATE_AUTH_REQUIRED", // a credential still has to be fetched
]);
async function onTaskUpdate(task: A2ATask): Promise<void> {
const state = task.status.state;
if (TERMINAL.has(state)) {
// Never send to task.id again. A revision is a new task, same contextId,
// with the old id listed in referenceTaskIds so the audit trail survives.
await closeDelegation(task.id, state);
return;
}
if (INTERRUPTED.has(state)) {
// The stall is the normal case, not the error path. Park it against the
// ERP document and notify a human -- but keep the subscription open:
// an out-of-band credential can un-stall the task with no message from us.
await parkForHuman(task.id, task.contextId, task.status.message);
return;
}
// TASK_STATE_SUBMITTED / TASK_STATE_WORKING: keep exactly one of the three
// update channels alive -- SubscribeToTask, a push-notification webhook, or
// a GetTask poll. Drop all three and the completion arrives nowhere.
}That is the honest cost of delegating to something you cannot see inside. With an MCP tool a crash loses a call you can simply make again. With an A2A task a crash loses your only handle on work a partner is still doing on your behalf.
The governance question is settled in a way it was not in 2025. Google announced A2A on 9 April 2025 with more than 50 launch partners and donated it to the Linux Foundation. The Linux Foundation's one-year release, dated 9 April 2026, reports more than 150 supporting organisations, over 22,000 GitHub stars, and version 1.0 as the first stable specification. The same release notes that the related Agent Payments Protocol has more than 60 supporting organisations.
Platform support is easy to verify: Microsoft integrated A2A into Azure AI Foundry and Copilot Studio, AWS added support through Amazon Bedrock AgentCore Runtime, and Google Cloud ships it as well. Five SDKs are described as production-ready, covering Python, JavaScript, Java, Go and .NET. Production use is named by vertical rather than by customer, in supply chain, financial services, insurance and IT operations. Notice what that phrasing does not say. No company is named, and I would not build a roadmap on an unnamed reference.
The specification, the five SDKs and the sample agents all live under the a2aproject organisation on GitHub, so the document and the code that implements it move together. If you read one section before deciding anything, read the task lifecycle; it is short, and it is where the design choices actually are.
For a small shop the answer is deliberately asymmetric: publish an Agent Card, do not build a peer mesh. Publishing is a static JSON file plus an endpoint that can answer SendMessage for one narrow skill. Consuming is where the cost lives, in the durable task store, the interrupted-state handling, the credential plumbing, and a partner willing to sign something about what their agent may do to your data.
The state machine is the evidence for that ordering. Everything expensive about A2A sits on the calling side: the interrupted states, the resumption rules, and the fact that terminal is final. The serving side is a document and two methods.
MCP made an agent's tools portable. A2A is trying to make an agent's colleagues portable, which is harder, because a colleague can answer not yet. Judge the protocol by its task lifecycle rather than by its partner count. A standard that ships input-required and auth-required as first-class states is one that expects real work to stall, and that expectation is why it deserves a small ERP shop's attention now, as a card to publish rather than a mesh to build.
Sources and further reading