← All articles
Tracify Team22 min read

Agent-to-Agent (A2A) Protocols Explained

Understand how independent AI agents discover, authenticate, and delegate work to each other, and what to instrument before you trust a message from an agent you don't control.

engineering
Two agent nodes on opposite sides of a dashed trust boundary exchanging a signed acid-yellow message envelope labeled with a correlation id

At a glance

This guide is for engineers building or integrating systems where one AI agent calls another AI agent, not a human calling a tool through a fixed API. It helps you reason about message formats, discovery, authentication, delivery semantics, and error handling between agents you may not control, so you leave with a mental model for what "agent-to-agent" actually requires, and what remains unsettled.

Before you startWhat to inspectWhat good looks like
A workflow where one agent must hand work to another, possibly across teams or vendorsThe message envelope, the trust boundary, and the failure path when the other agent is slow or wrongA bounded, observable exchange with an explicit timeout and a fallback, not a bare HTTP call wrapped in hope

Agent-to-agent (A2A) communication is the pattern where an autonomous agent — not a human, not a fixed backend service — is the caller and the callee. One agent decides what to ask for and how urgently; another agent decides whether it can help and how to do the work. That shift changes what the message needs to carry, because neither side can assume the other was compiled from the same codebase, deployed by the same team, or even fully trusted.

A2A protocols are still an early, evolving area — some specifications are publicly documented and versioned, others are ad hoc conventions teams build internally. This guide separates what is reasonably settled from what each team currently has to decide for itself, and it will say so explicitly whenever a detail is not standardized.

The operating model

Use Discover → Negotiate → Delegate → Observe as the path through every agent-to-agent exchange:

  1. Discover: the calling agent learns what the other agent can do, and how to reach it.
  2. Negotiate: both sides agree on a capability, an authentication scheme, and a message shape.
  3. Delegate: the calling agent hands over a task with a correlation id and waits, synchronously or not.
  4. Observe: every hop is traceable, every timeout has a defined outcome, and every result is verified before it's trusted.

This is the backbone the rest of the article follows. It does not claim there is one universal wire format all agents already speak — there isn't — but the four stages are consistent regardless of which concrete protocol or convention you adopt.

Why A2A is a different problem than calling a tool

An agent calling a tool through MCP is calling something with a fixed, small contract: a name, a schema, a result. The tool has no goals of its own. Agent-to-agent communication is different because the thing on the other end of the call is also making decisions — it might reformulate your request, decline part of it, ask a clarifying question, or delegate to a third agent you never knew existed.

That difference shows up in four concrete ways:

  • The contract is behavioral, not just structural. A tool schema tells you the shape of the input and output. An agent's capability card tells you what it claims to be willing and able to do, which is a promise, not a type signature.
  • The conversation can have more than one round. A tool call is typically one request, one response. An agent-to-agent task can involve a status update, a clarifying question, a partial result, and a final result — all tied to the same task.
  • Trust has to be established, not assumed. A tool usually lives inside your own infrastructure or a vetted MCP server. A remote agent might belong to a different company, run on infrastructure you've never audited, and be updated on a schedule you don't control.
  • Failure is ambiguous by default. When a tool call times out, you know the tool didn't respond. When an agent-to-agent task stalls, you don't yet know if the remote agent is still working, gave up silently, or was never able to start.

Field note: Treat "the other side is also an agent" as the central design constraint, not a footnote. Every section below — message shape, auth, timeouts, tracing — exists because a message can no longer assume a deterministic function on the receiving end.

Message formats and envelopes

Whatever transport you use — HTTP, gRPC, a message queue — an agent-to-agent message needs an envelope: metadata that travels with the payload regardless of what the payload contains. A minimal, defensible envelope looks like this:

json
{
  "protocol_version": "1.0",
  "message_id": "msg_9f3a1c",
  "correlation_id": "7f2c9a4e-19a4-4e2c-9b0a-1c9b2b6a0e51",
  "conversation_id": "conv_5b71",
  "sender": {
    "agent_id": "urn:agent:acme:orchestrator",
    "org": "acme.example"
  },
  "recipient": {
    "agent_id": "urn:agent:partnerco:pricing-agent"
  },
  "type": "task.create",
  "issued_at": "2026-08-26T12:00:00Z",
  "expires_at": "2026-08-26T12:05:00Z",
  "auth": {
    "scheme": "bearer_jwt",
    "token": "eyJhbGciOi..."
  },
  "payload": {
    "skill": "quote.generate",
    "input": { "sku": "AX-100", "quantity": 250 }
  }
}

Three fields matter more than the rest. correlation_id is the thread that ties every message in a task — including status updates and the final result — back to the same unit of work; it is the single most important field for later debugging. expires_at states a deadline the sender is willing to wait, rather than leaving the recipient to guess. auth is separated from payload, so a logging pipeline can redact or drop credentials without touching business data.

Envelope fieldPurposeCommon mistake
correlation_idTies every message in one task together across hopsRegenerating it per hop instead of propagating the original
expires_at / deadlineTells the recipient how long the result is still usefulOmitting it and relying on a generic infrastructure timeout
sender.agent_idA stable identity, not a display nameUsing a mutable string that changes when the agent is renamed
typeDistinguishes discovery, negotiation, task, status, and result messagesOverloading one message type for everything, which breaks routing and tracing

Two competing conventions exist for how much of this metadata belongs in a shared header versus inside a payload the two agents privately agree on. Some designs favor a thin, protocol-level envelope (identity, correlation, auth, deadline) with an opaque payload the agents interpret themselves. Others favor a richer, protocol-defined payload schema per message type. Neither is standardized across the industry today — pick the thinner envelope when you're integrating with agents you don't control, since it requires the least shared assumption about payload structure.

Capability discovery and negotiation

Before an agent can delegate work, it has to find out what the other agent can actually do. This is usually modeled as a capability card or agent card: a small, fetchable document describing the agent's identity, the skills it exposes, the input/output shape for each skill, and how to authenticate to it.

json
{
  "agent_id": "urn:agent:partnerco:pricing-agent",
  "name": "PartnerCo Pricing Agent",
  "version": "2.3.0",
  "skills": [
    {
      "id": "quote.generate",
      "description": "Generate a bulk-order price quote",
      "input_schema": { "sku": "string", "quantity": "integer" },
      "output_schema": { "unit_price": "number", "currency": "string", "valid_until": "string" }
    }
  ],
  "auth_schemes": ["bearer_jwt", "mtls"],
  "endpoints": {
    "task": "https://agents.partnerco.example/a2a/tasks"
  }
}

Discovery is typically served from a well-known, predictable location (a pattern borrowed from /.well-known/ conventions used elsewhere on the web) so a client agent doesn't need out-of-band configuration for every partner. Negotiation is the step right after: the calling agent picks a skill from the card, picks an auth scheme both sides support, and — if the card advertises more than one wire format or version — agrees on which one to use for this conversation.

Capability discovery is a trust decision disguised as a technical one: fetching a card and calling a skill it advertises means you are choosing to rely on a claim the remote agent made about itself, with no independent verification that it will behave as described. Treat a newly discovered agent the way you'd treat a newly integrated third-party API — verify behavior with a small, low-stakes call before routing anything consequential to it.

What discovery does not solve

Discovery tells you what an agent claims it can do. It does not tell you:

  • Whether the agent is reliable under load, or only in a demo.
  • Whether the agent's behavior is stable across its own version upgrades.
  • Whether a skill's description matches its actual side effects — a quote.generate skill that also silently reserves inventory is a discovery-time surprise you will only catch by reading documentation or testing.

Keep a local allowlist of agents and skills your system is permitted to call automatically, and require a human or a stricter policy check before adding a newly discovered agent to that list.

Authentication and trust boundaries

Once discovery and negotiation are settled, the harder question is: should this remote agent be trusted with this task, and should this task's caller be trusted by the remote agent? That question gets sharper the moment the two agents cross an organizational or vendor boundary.

Trust boundaryWhat changesWhat to require
Same team, same deploymentShared secrets, shared logging, shared on-callBasic auth is often sufficient; focus on internal audit trails
Same organization, different teamDifferent release cadence, different data-handling rulesScoped service credentials, per-team rate limits
Different organization or vendorNo shared infrastructure, no shared incident channel by defaultSigned, short-lived tokens; mutual TLS or equivalent; explicit data-handling agreement

For cross-organization calls, two mechanisms do most of the practical work. Bearer tokens with a narrow audience claim (a JWT whose aud names the specific recipient agent, not a broad API) let the recipient reject a token that was minted for someone else, even if it leaked. Mutual TLS authenticates the transport connection itself, which matters when you want to be certain which organization's infrastructure is on the other end, independent of anything in the application payload.

ts
// Illustrative: verify a cross-org A2A token before accepting a task.
type A2AToken = { iss: string; aud: string; sub: string; exp: number };

function verifyIncomingTask(token: A2AToken, expectedRecipient: string, trustedIssuers: string[]) {
  if (!trustedIssuers.includes(token.iss)) {
    return { accepted: false, reason: "untrusted_issuer" as const };
  }
  if (token.aud !== expectedRecipient) {
    return { accepted: false, reason: "wrong_audience" as const };
  }
  if (token.exp * 1000 < Date.now()) {
    return { accepted: false, reason: "expired" as const };
  }
  return { accepted: true as const, callerAgentId: token.sub };
}

Decision rule: Reject a task before you look at its payload if the issuer isn't on your trusted list, the audience doesn't name your agent specifically, or the token has expired. Payload-level validation should never be the first line of defense against an untrusted caller.

The subtler trust problem is what happens after authentication succeeds. A correctly authenticated remote agent can still return a result that is wrong, incomplete, or — in the more concerning case — contains instructions embedded in its output that your own agent might follow. The same discipline covered in prompt injection defense applies here: a result from another agent is data your system received, not an instruction your system must obey, no matter how well-formed the envelope looks.

Synchronous vs asynchronous messaging patterns

Not every agent-to-agent task fits a single request/response cycle. A quote-generation skill might answer in 200 milliseconds; a research or multi-step planning skill might take minutes. Both are legitimate, and most real systems need to support both, sometimes for the same remote agent.

PatternHow it worksGood fitFailure mode to design for
Synchronous request/responseCaller sends a task, holds the connection, gets a result on the same callFast, bounded skills (lookups, simple transforms)Caller's own timeout fires before the remote agent finishes; result is dropped even though work continued
PollingCaller sends a task, gets a task id back, polls a status endpointMedium-duration tasks where push infrastructure isn't availablePolling interval too aggressive (load) or too relaxed (perceived latency)
Webhook / callbackCaller sends a task with a callback URL; remote agent pushes the result when doneLong-running tasks, especially across organizationsCallback delivery fails silently if the caller's endpoint is briefly unreachable
Streaming (SSE / WebSocket)Remote agent pushes incremental status and partial results over an open channelTasks with useful partial output, or where the user is watching liveConnection drop loses in-flight state unless the client can resume from the last acknowledged event

Asynchronous patterns all share one requirement that's easy to skip under deadline pressure: the caller has to be able to reconcile a result that arrives after it stopped actively waiting. If your orchestrator gave up on a task and told the user something failed, and the remote agent's result shows up ninety seconds later, your system needs a defined behavior — discard it, apply it if still relevant, or flag it for review — rather than an unhandled message that either crashes a handler or silently vanishes.

Choosing synchronous delivery for a task that might occasionally take much longer than expected is one of the more common design mistakes in early multi-agent systems. It works in a demo, where every call is fast, and then fails intermittently in production the first time a downstream dependency is slow. Default to an asynchronous pattern — polling or callback — for any skill whose duration is not tightly bounded, and reserve synchronous calls for skills you can guarantee complete quickly.

A2A compared with adjacent protocols

It's worth being precise about what A2A-style protocols are for, because they're frequently discussed alongside — and sometimes confused with — protocols that solve a related but distinct problem.

  • MCP (Model Context Protocol) standardizes how a single agent discovers and calls tools and data sources: functions, APIs, and structured resources that don't have their own goals or reasoning. MCP observability covers tracing that boundary in depth. A2A-style protocols standardize how one autonomous agent talks to another autonomous agent, where both sides can reason, decline, or delegate further. A useful shorthand: MCP is agent-to-tool, A2A is agent-to-agent.
  • Plain REST or RPC APIs between services are the closest ancestor of A2A messaging, and in practice many "A2A" integrations today are exactly that: an HTTP API with a task-shaped payload. What A2A-specific conventions add on top — capability cards, correlation ids designed for multi-turn tasks, and negotiation of skills rather than fixed endpoints — matters most once you're integrating with agents you didn't build and can't hardcode assumptions about.
  • Agent orchestration frameworks (the kind used to coordinate multiple agents within one team's system) often have their own internal message-passing conventions. Those are usually simpler than cross-organization A2A because every agent shares deployment, trust, and versioning — the negotiation and trust-boundary concerns in this guide matter far less inside a single system than across one.

No agent-to-agent protocol in this space should be treated as a mature, universally adopted standard yet, the way HTTP or OAuth are. Public specifications exist and are actively developed, but implementations, versioning practices, and even basic vocabulary still vary between vendors. Build your integration layer so the wire format is isolated behind an internal envelope and adapter, so a future protocol revision — or a switch to a different partner's convention — doesn't require rewriting how your own agents reason about tasks.

Error handling and timeout semantics

A multi-agent conversation has more ways to fail than a single API call, because failure can happen at discovery, at negotiation, at delegation, or partway through execution — and each stage needs its own defined outcome.

ts
// Illustrative: a bounded state machine for one delegated A2A task.
type TaskState =
  | "pending"
  | "acknowledged"
  | "working"
  | "completed"
  | "failed"
  | "timed_out";

function nextState(current: TaskState, event: "ack" | "progress" | "result" | "error" | "deadline_passed"): TaskState {
  if (event === "deadline_passed" && current !== "completed" && current !== "failed") return "timed_out";
  if (current === "pending" && event === "ack") return "acknowledged";
  if ((current === "acknowledged" || current === "working") && event === "progress") return "working";
  if ((current === "acknowledged" || current === "working") && event === "result") return "completed";
  if (event === "error") return "failed";
  return current;
}

Every terminal-looking state above — completed, failed, timed_out — needs an explicit consumer, not just a value stored somewhere. A task that reaches timed_out should trigger one of a small number of defined responses: retry the same remote agent, fail over to an alternative agent that offers the same skill, or escalate to a human. Leaving timed_out unhandled is functionally the same as having no timeout at all, since nothing in the system reacts to it.

Failure pointWhat it looks likeReasonable response
Discovery failsCapability card endpoint unreachable or malformedFall back to a cached card if recent enough, otherwise fail the request before any task is created
Negotiation failsNo shared auth scheme or skill versionFail fast with a specific reason; do not silently downgrade to an unauthenticated call
Task never acknowledgedNo response before an ack deadlineTreat as failed delivery; retry with backoff or route to a fallback agent
Acknowledged but never completesRemote agent accepted the task but the deadline passesMark timed_out; do not assume completion, and do not silently discard evidence the task existed
Result arrives after timeoutLate result for a task the caller already gave up onLog and evaluate against current relevance before applying; never apply blindly to live user state

Decision rule: Every timeout you set needs a named consumer of the resulting state before you ship it. If you can't say what the system does when the deadline passes, the timeout isn't finished — it's a number that will eventually surprise someone on call.

Choosing a fallback strategy before you need one

A timeout tells you the exchange failed to complete in time. It does not tell you what to do next, and that decision is worth writing down before the first incident forces it. Three fallback strategies cover most cases, and they are not mutually exclusive — a mature integration usually escalates through them in order.

Retry the same agent when the failure looks transient: a single slow response, a connection reset, or a 503 from the remote agent's own infrastructure. Bound the retry count and use backoff, and only retry idempotent skills — a quote.generate call is safe to retry, a payment.charge call almost certainly is not unless the envelope carries an idempotency key the remote agent honors.

Fail over to an alternative agent when one exists and offers an equivalent skill. This only works if your integration layer already normalized the capability so switching providers doesn't require different application logic — another reason to keep the wire-format details behind an internal adapter rather than scattering vendor-specific assumptions through your own agent's reasoning.

Degrade to a bounded, honest response when neither retry nor failover is available. Tell the user a quote couldn't be generated right now rather than guessing at a price, or fall back to a cached, clearly-labeled stale value if one exists and the use case tolerates it. A visible, explained limitation is a better product outcome than a silent wrong answer.

FallbackWhen it's safeWhen it's not
Retry same agentSkill is idempotent and the failure looks transientThe skill has side effects and no idempotency key was sent
Fail over to alternative agentAn equivalent skill is normalized behind your own adapterOnly one provider exists for this skill, or outputs aren't truly interchangeable
Degrade to a bounded responseThe user can act on a clear "unavailable" messageThe workflow requires the result to proceed at all, with no safe default

Walk through one full exchange

The sequence below is the same handshake shown in the diagram earlier in this article, expanded step by step with the actual envelope contents at each stage. Reading it top to bottom is the fastest way to see how discovery, negotiation, delegation, and result delivery share one identity and one correlation id.

Sequence diagram of the full agent-to-agent handshake: discovery request and capability card, skill and auth negotiation, task delegation with a correlation id, a streamed status update, and the final result, with a boxed timeout fallback at the bottom

Step 1 — Discovery

The client agent fetches the remote agent's capability card before it knows anything else about it.

http
GET /.well-known/agent-card HTTP/1.1
Host: agents.partnerco.example

The response is the capability card shown earlier in this guide: skills, schemas, supported auth schemes, and the task endpoint. Nothing here is authenticated yet — discovery is typically public, since it only describes capabilities, not access.

Step 2 — Negotiation

The client agent picks quote.generate from the advertised skills and bearer_jwt from the advertised auth schemes, then mints a short-lived token scoped to this specific remote agent's audience.

Negotiated fieldClient's choiceWhy
Skillquote.generateMatches the user's request; input schema is satisfiable from data the client already has
Auth schemebearer_jwt over mtlsSimpler to rotate across many partner agents than managing per-partner certificates
Protocol version1.0Highest version both the client and the card agree on

Step 3 — Delegation

The client sends the task.create envelope shown earlier, with correlation_id: 7f2c9a4e... and a five-minute expires_at. The remote agent's first response is an acknowledgment, not the result:

json
{ "type": "task.ack", "correlation_id": "7f2c9a4e-19a4-4e2c-9b0a-1c9b2b6a0e51", "state": "working" }

Step 4 — Status (asynchronous case)

If the skill takes longer than a trivial lookup, the remote agent pushes or the client polls for a status update carrying the same correlation id:

json
{ "type": "task.status", "correlation_id": "7f2c9a4e-19a4-4e2c-9b0a-1c9b2b6a0e51", "state": "working", "progress": "checking inventory constraints" }

Step 5 — Result

json
{
  "type": "task.result",
  "correlation_id": "7f2c9a4e-19a4-4e2c-9b0a-1c9b2b6a0e51",
  "state": "completed",
  "output": { "unit_price": 18.40, "currency": "USD", "valid_until": "2026-09-02T00:00:00Z" }
}

The client agent now has everything it needs to close the loop with the user: the same correlation_id that started the task, ready to be joined against the client's own trace for a single, reconstructable record of the exchange.

Step 6 — What if it times out instead

If no task.result (and no task.status) arrives before expires_at, the client transitions the task to timed_out using the state machine shown earlier, and applies whichever fallback policy it defined in advance — retry, route to a different pricing agent, or return a bounded "quote unavailable" response to the user rather than leaving them waiting indefinitely.

Observability and tracing across agent hops

Everything above is only debuggable in production if the correlation id survives every hop, the way it does in the walkthrough above. This is the same discipline covered in LLM tracing explained, extended across a boundary the tracing tooling inside one process can't see by itself.

Three stacked agent spans — orchestrator, research agent, pricing agent — all sharing one correlation id and linked by parent span references, with the pricing agent's span marked as the hop that timed out

Four practices make a multi-agent conversation reconstructable after the fact:

  1. Propagate, never regenerate, the correlation id. Every agent in the chain should read the id from the incoming envelope and pass it forward unchanged. A new id minted at each hop breaks the ability to join records into one conversation.
  2. Record a parent-child span relationship, not just a flat log line. When Agent A delegates to Agent B, Agent B's span should reference Agent A's span, the same way a child span references a parent inside one process's trace. Cross-organization tracing usually can't share a full trace store, but the id references still let each side's own tooling line up independently.
  3. Log the negotiated contract, not just the payload. Capture which skill, auth scheme, and protocol version were negotiated for this task. A behavior regression after a remote agent's version upgrade is far easier to diagnose when the negotiated version is part of the record.
  4. Make timeouts a first-class trace event, not a missing log line. A task that reaches timed_out should emit an event saying so, with the deadline and the last known state. The absence of a result should never be the only signal that something went wrong.
Trace fieldWhy it matters here specifically
correlation_idJoins every message across every hop into one reconstructable conversation
hop / depthDistinguishes "the first agent was slow" from "the third agent in the chain was slow"
negotiated_versionExplains behavior changes tied to a remote agent's release, not your own
deadline and outcomeTurns "nothing came back" into "here is exactly when and why we gave up"

What this article doesn't settle

Be direct with your team about the limits here. There is no single wire format every agent speaks today, and vendors differ on details as basic as how a capability card is versioned or how a task is canceled mid-flight. Multi-agent identity — proving which agent, not just which service account, sent a message — is an active area without a widely agreed answer. And cross-organization revocation (telling every partner "stop trusting this agent identity immediately") is closer to an operational runbook problem today than a protocol feature you can rely on out of the box.

None of that is a reason to avoid building agent-to-agent integrations. It is a reason to isolate the wire-format details behind your own envelope and adapter layer, and to treat every remote agent's result as data to verify rather than an instruction to trust — the same posture prompt injection defense recommends for any untrusted input.

Make the operating decision

Carrying the pricing-agent example through to a release decision: ship the integration once discovery is cached and fails closed, negotiation rejects unsupported auth schemes rather than downgrading silently, every task has a deadline with a named fallback, and the correlation id shows up in your own tracing the same way a normal request would. Hold the release if any timeout path is still unhandled, or if a remote agent's result can reach a user-visible surface without passing through the same validation you'd apply to any other untrusted input.

Leave the reader with the next move

  • [ ] Every outgoing task has an expires_at and a defined response to a timeout.
  • [ ] The correlation id is propagated, not regenerated, at every hop.
  • [ ] Incoming tasks are rejected on issuer, audience, and expiry before the payload is read.
  • [ ] A remote agent's result is treated as untrusted data until validated, never as an instruction.
  • [ ] The rendered article, diagrams, and message walkthrough have been checked on both desktop and mobile widths.

If you're integrating with your first external agent this quarter, start by writing the envelope and the timeout state machine before you write any negotiation logic — most early multi-agent incidents trace back to an unhandled timeout, not a malformed message.

Frequently asked questions

Is there one official A2A protocol every agent uses?

No single specification is universally adopted yet. Public agent-to-agent specifications exist and are actively developed, but many production integrations today are closer to a task-shaped REST API between two teams. Isolate the wire format behind your own envelope so you can adapt as conventions mature.

How is A2A different from MCP?

MCP standardizes how one agent calls tools and data sources that have no goals of their own. A2A-style protocols standardize how one autonomous agent talks to another autonomous agent, where both sides can reason, decline, or delegate further. Most real systems need both.

What's the minimum authentication I need for a cross-organization agent call?

A short-lived bearer token with an audience claim naming the specific recipient agent, validated for issuer, audience, and expiry before the payload is read. Add mutual TLS when you need to authenticate the transport itself, not just the application-level claim.

Should agent-to-agent calls be synchronous or asynchronous?

Use synchronous request/response only for skills with a tightly bounded duration. Default to polling or a callback for anything else, and make sure your system can reconcile a result that arrives after it already gave up waiting.

What should I trace across an agent-to-agent conversation?

Propagate one correlation id across every hop, record parent-child span relationships between the delegating and receiving agents, log the negotiated skill and auth scheme, and emit an explicit event when a task times out rather than leaving its absence as the only signal.

Written byTracify Team

Practical field notes from the team building Tracify and operating AI agents in production.

#AI agents#multi-agent systems#observability

Next in the loop

AI Agent Security: A Practical Production Checklist

Keep reading →