Skip to main content
BlogAI

A2A Protocol: How AI Agents Discover and Delegate to Peers

MCP connects an agent to tools. A2A connects it to other agents it cannot see inside, and the task states that admit a delegation has stalled are the interesting part.

Matthews Wong

Software Engineer

September 202611 min read
A2A Protocol: How AI Agents Discover and Delegate to Peers

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.

MCP hands you a tool, A2A hands you a peer

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.

QuestionMCPA2A
What is on the other endA tool surface: named functions with declared schemasAn agent that plans, and may delegate onward itself
How you find itClient config, a registry entry, or a command that spawns itAn Agent Card fetched from a well-known URL on the peer's domain
What a call returnsA tool result, nowA task with a lifecycle that may stall for hours
Who owns a failureYou do, because it is your serverThe peer does, and you only see the state it chooses to publish
Where the trust boundary sitsUsually inside one organisationUsually 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.

The Agent Card is the whole discovery surface

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.

Five stacked bands listing what an Agent Card declares: identity and provider, skills, supported interfaces, security schemes, and the signature
Skills are prose, not a schema, and that is the design decision the rest of the protocol follows from. A calling agent reads the description and decides; it never gets a function signature to validate against.

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.

What one delegation looks like on the wire

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.

The two states that admit a delegation has stalled

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:

Four bands of the A2A task lifecycle: submitted, working, input required, and terminal, each with the caller action it implies
The third band is the one that changes how you write a client. An interrupted task has not failed, so the correct response is to resume it with the same taskId rather than open a replacement.
  • An agent requesting authorisation must track the operation as a task, transition it to TASK_STATE_AUTH_REQUIRED, and include a status message explaining what is required.
  • Credentials should arrive out of band. In-band credentials pass through every agent in the chain, which is exactly why the specification recommends binding a credential to the agent that requested it.
  • A client that is itself an agent may transition its own task to TASK_STATE_AUTH_REQUIRED, forming a chain of stalled tasks that ends at a human. That is delegation admitting what it is.
  • The transition is not itself an approval. The specification says an agent must not treat the state change as authorisation for any operation, and a credential obtained during one stall does not authorise later messages on the same task.

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.

Terminal means terminal, and that is a storage problem

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.
}
  • The taskId and contextId, stored against the ERP document rather than in memory, because an interrupted task can outlive the process that started it.
  • The last observed state and its timestamp, so a restarted worker knows whether to resubscribe or to leave the task alone.
  • Whichever update channel you chose: an open SubscribeToTask stream, a registered push-notification config, or a GetTask polling schedule.
  • A rule for what a repeat means, because a second SendMessage with no taskId opens a different task and an opaque peer cannot warn you that it duplicates the first.

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.

Where A2A actually is, one year in

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.

What I would ship this quarter, and what I would not

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.

  1. Write the Agent Card for the single skill you would genuinely let an outsider invoke, and serve it at the well-known path behind your existing SSO.
  2. Implement SendMessage and GetTask over the JSON-RPC binding for that one skill. Two methods is already a working agent.
  3. Return TASK_STATE_INPUT_REQUIRED and TASK_STATE_AUTH_REQUIRED honestly instead of blocking or failing, and persist the taskId and contextId against the ERP record they belong to.
  4. Stop there until a named partner asks to call you. Building the outbound half against no counterparty leaves you maintaining a protocol client with one imaginary user.

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.

FAQ

Frequently Asked Questions

Short answers to what readers ask most about this topic.

What is the A2A protocol?

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.

How is A2A different from MCP?

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.

What is an Agent Card and where is it published?

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.

What do the input-required and auth-required task states mean?

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.

Should a small team adopt A2A now?

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.