What Problem A2A Solves
Most enterprise agent estates in 2026 are polyglot: a Joule assistant on SAP BTP, a Foundry Hosted Agent on Azure, a LangGraph service on a team’s Kubernetes, a vendor copilot nobody controls. Each can reason and act; none can talk to the others without bespoke code.
The usual workaround — wrapping an agent as a tool — works for one stateless call and fails elsewhere: it hides the agent’s planning loop, cannot express “I need more input,” and does not survive an organisation boundary. The Agent2Agent (A2A) protocol is the open standard for this layer. It gives agents a common language whatever the framework or vendor, built on HTTP(S), JSON-RPC 2.0, and Server-Sent Events (SSE), and treats an agent as a peer, not a function.
Governance matters for a standard you bet an integration landscape on. Google contributed A2A to the Linux Foundation in 2025; it is now a project of the Agentic AI Foundation, the Linux Foundation umbrella that also hosts MCP. Both SAP and Microsoft ship first-party support.
Five properties make A2A fit for SAP estates:
- Opaque execution. Callers see declared skills and results, never your prompts, memory, or tools — which lets you expose an S/4HANA agent to a partner without leaking IP.
- Asynchronous by design. Long-running work is normal in ERP — a credit-block review takes minutes, not milliseconds — and A2A has a first-class task lifecycle for it.
- Enterprise-ready. Auth, tracing, and monitoring follow standard web practice, not a bespoke scheme.
- Modality independent. Parts carry typed content — structured JSON, images, files — not just text.
- Framework neutral. LangGraph, CrewAI, ADK, or plain Python — A2A sits above them all.
Core Concepts: Agent Cards, Tasks, and Streaming
Three actors define the protocol: the user, an A2A client (the caller), and an A2A server (the remote agent). The objects below, from the specification and core concepts, carry everything between them:
- Agent Card — JSON published by the server describing identity, endpoint, capabilities, skills, and security schemes. This is the contract; nothing else is visible before the first call.
- Message — one turn of conversation, with a
roleofuseroragentand a list of parts. - Part — a single content unit: text, structured
data, aurl, or raw bytes, each with a media type. - Artifact — the actual deliverable produced by the remote agent. An agent can emit several artifacts for one task.
- Task — a stateful unit of work with a unique id and a lifecycle. A task is what distinguishes A2A from a tool call.
A contextId groups related tasks and messages into one conversation, so a follow-up lands in the same session even when it starts a new task.
Discovery follows RFC 8615: a client fetches https://{host}/.well-known/agent-card.json, reads the card, authenticates against the declared scheme, then calls the endpoint. Three strategies exist: the well-known URI, curated registries, and direct configuration. Because cards are public and cached, serve them with Cache-Control and an ETag derived from the card’s version. Here is a minimal valid v1.0 card for a sales-order agent on SAP BTP Cloud Foundry (ProtoJSON names are camelCase; each interface now carries its own protocolVersion, moved out of the card root):
{
"name": "SAP Sales Order Agent",
"description": "Explains open sales orders, statuses and net values for a customer.",
"version": "1.0.0",
"supportedInterfaces": [
{
"protocolBinding": "JSONRPC",
"url": "https://sales-order-agent.cfapps.eu10.hana.ondemand.com/",
"protocolVersion": "1.0"
}
],
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["application/json"],
"capabilities": { "streaming": true, "pushNotifications": false, "extendedAgentCard": true },
"skills": [
{
"id": "sales-order-lookup",
"name": "Sales order lookup",
"description": "Return open sales orders, delivery status and net values for a customer.",
"tags": ["sap", "sales"],
"examples": ["Show open orders for customer ACME"]
}
],
"provider": { "organization": "Contoso" },
"securitySchemes": {
"oidc": { "openIdConnect": { "openIdConnectUrl": "https://<tenant-id>.cf-eur10.ondemand.com/sap/bc/sec/oauth2/token" } }
},
"security": [{ "oidc": [] }]
}
The life of a task is the heart of the protocol. A server replies either with a stateless message or with a task in submitted; tasks move to working and can pause in input-required or auth-required — the ERP-friendly case where a human must approve something. Terminal states are completed, canceled, rejected, and failed, and they are immutable: refining a finished task means a new task in the same contextId referencing the old one.
Three mechanisms cover throughput: request/response with polling via GetTask, SSE streaming via SendStreamingMessage, and push notifications to a client webhook when no connection can be held. A stream opens with the task in submitted, then TaskStatusUpdateEvent and TaskArtifactUpdateEvent frames follow until the terminal state ends it.
Version note before you reuse an older snippet: A2A v1.0 renamed the JSON-RPC operations (message/send → SendMessage, message/stream → SendStreamingMessage), dropped the kind discriminator and the final flag, switched enums to SCREAMING_SNAKE_CASE, folded preferredTransport and additionalInterfaces into supportedInterfaces, and removed the implicit and password OAuth2 grant types. v0.3.x clients remain widely deployed, so negotiation is per-interface via the A2A-Version and A2A-Extensions headers.
A2A and MCP: Complementary, Not Competing
The official comparison is blunt: MCP gives each agent depth, A2A gives your system reach. Use both: SAP Integration Suite ships an MCP Gateway, and Joule Studio supports each.
| Dimension | A2A | MCP |
|---|---|---|
| Primary purpose | Peer agents delegating work to each other across a boundary | Plugging tools, resources, and prompts into one model or agent |
| Topology | Horizontal, federated, any agent to any agent | Vertical, hub-and-spoke around a single application |
| Unit of work | Stateful Task with lifecycle, contextId, artifacts |
Mostly stateless tool invocation with structured input/output |
| Long-running interaction | Native: streaming, polling, push notifications, input-required |
Limited; server-initiated tools, often request-bound |
| SAP example | Joule delegating to a bring-your-own-agent sales service | A Foundry agent querying SAP HANA Cloud through an MCP tool |
Bridge: an A2A skill can surface as an MCP resource when genuinely stateless. Keep it honest — if the remote side negotiates, pauses, or needs approval, it is a task.
The SAP Side: Joule Agents and BTP Extensibility
At TechEd Berlin 2025 SAP put A2A at the centre of its agentic capabilities for SAP BTP: custom agents built in Joule Studio went generally available in December 2025, and the platform supports A2A plus MCP, with CrewAI and LangGraph running under BTP’s end-to-end identity and governance model.
Two directions exist: extend SAP-delivered Joule agents, or bring your own. The second is documented in the Joule development guide and implemented by SAP’s joule-a2a-agent-toolkit: a declarative Digital Assistant (DTA) project plus an A2A endpoint.
- The namespace must be
joule.ext, and the capability file usesschema_version: 3.28.0— the minimum that permits theagent-requestaction type. - A function calls the remote agent with
type: agent-request,agent_type: remote, and asystem_aliasthat resolves to a BTP HTTP destination. The first turn sends nullcontextIdandtaskId; pass them back on follow-ups so Joule continues the same context. - The destination points at the agent’s base URL without the
/.well-known/suffix — Joule appends the well-known path itself. Set Proxy Type toInternetandHTML5.DynamicDestination=true, per SAP’s HTTP destination guide. - Deploy with the Joule Studio CLI (
joule deploy) into an existing assistant; rolescapabilityadminandextensibility_developerplus an IAS app-to-app trust are prerequisites.
Budget latency: Joule waits synchronously for roughly 60 seconds. Anything longer belongs behind a task surfaced by a status update or a follow-up turn.
The Azure Side: AI Foundry Agent Service Interop
Microsoft Foundry’s Agent Service exposes an existing prompt agent as an A2A endpoint, so a Joule agent, an ADK agent, or any A2A client can call it without bespoke glue. The endpoint how-to is precise: one endpoint serves A2A v1.0 (generally available) and v0.3 (preview). A card at .../endpoint/protocols/a2a/agentCard/v1.0 or .../agentCard/v0.3 selects the version; the A2A-Version: 1.0 header or ?a2a-version=1.0 overrides it, and a conflict returns HTTP 400 version-ambiguous. Omitting the version today falls back to v0.3, so pin it explicitly.
Enablement is not in the portal: use REST or the SDK against a prompt agent whose output format is the responses protocol. For the outbound direction, use the agent-to-agent tool:
curl -sS -X PATCH \
"https://{account}.services.ai.azure.com/api/projects/{project}/agents/{agent}/endpoint/protocols/a2a?api-version=2025-04-01-preview" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{ "agent_card": { "supported_interfaces": [{ "url": "https://{account}.services.ai.azure.com/api/projects/{project}/agents/{agent}/endpoint/protocols/a2a" }] } }'
Know the limits before designing across this boundary: incoming A2A is text modality only, does not support SSE streaming, retains a task or context for 60 days after its last write, and binds v1.0 to JSON-RPC only — gRPC is not available. Plan for GetTask polling on Azure even if your SAP-side agent streams.
Build and Expose a Demo Agent, Step by Step
The reference implementation follows the official Python quickstart: Python 3.12+, a2a-sdk 1.3.0 at time of writing.
- Install
pip install "a2a-sdk==1.3.0" uvicornin a clean virtual environment. - Implement the executor. This is your agent’s brain; the SDK handles wire format, tasks, and streaming.
from a2a.helpers import (
get_message_text, new_task_from_user_message, new_text_message, new_text_part,
)
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.server.tasks import TaskUpdater
from a2a.types import TaskState
class SalesOrderAgentExecutor(AgentExecutor):
def __init__(self, agent) -> None:
self.agent = agent
async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
task = context.current_task or new_task_from_user_message(context.message)
if not context.current_task:
await event_queue.enqueue_event(task)
updater = TaskUpdater(event_queue=event_queue, task_id=task.id, context_id=task.context_id)
await updater.update_status(
state=TaskState.TASK_STATE_WORKING, message=new_text_message('Querying sales data...')
)
result = await self.agent.invoke(user_request=get_message_text(context.message))
await updater.add_artifact(parts=[new_text_part(text=result, media_type='text/plain')])
await updater.update_status(
state=TaskState.TASK_STATE_COMPLETED, message=new_text_message('Request is completed!')
)
async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
raise NotImplementedError('Cancellation is not supported by this agent.')
- Serve it. Declare the card, wire the handler, mount both routes. Keep the task store durable in production;
InMemoryTaskStoreis a development convenience.
import uvicorn
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentInterface, AgentSkill
from starlette.applications import Starlette
skill = AgentSkill(
id='sales-order-lookup', name='Sales order lookup',
description='Return open sales orders for a customer.',
input_modes=['text/plain'], output_modes=['text/plain'],
tags=['a2a', 'sap'], examples=['Show open orders for customer ACME'],
)
card = AgentCard(
name='SAP Sales Order Agent', description='Reads sales orders for a customer.',
version='1.0.0', default_input_modes=['text/plain'], default_output_modes=['text/plain'],
capabilities=AgentCapabilities(streaming=True, extended_agent_card=True),
supported_interfaces=[AgentInterface(
protocol_binding='JSONRPC', url='http://127.0.0.1:9999', protocol_version='1.0')],
skills=[skill],
)
handler = DefaultRequestHandler(
agent_executor=SalesOrderAgentExecutor(agent), task_store=InMemoryTaskStore(), agent_card=card
)
routes = [*create_agent_card_routes(card), *create_jsonrpc_routes(handler, '/')]
uvicorn.run(Starlette(routes=routes), host='127.0.0.1', port=9999)
create_agent_card_routes publishes /.well-known/agent-card.json, and DefaultRequestHandler refuses streaming or push requests your card does not advertise.
- Smoke-test before integration.
curl -s http://127.0.0.1:9999/.well-known/agent-card.json | jq '.skills[].id'
- Expose to Joule: deploy to Cloud Foundry, create the HTTP destination, author the
agent-requestcapability withsystem_alias, thenjoule deploy ./da.sapdas.yaml --compile -n "Line-of-Business Assistant". - Expose to Foundry: PATCH the A2A endpoint as above, request an Entra ID token for
https://ai.azure.com/.default, and call with an A2A v1.0 client usingstreaming=False.
Security and Identity Notes
A2A declares security in the card and enforces it on the wire; nothing is implicit. Follow the enterprise features guidance: treat an agent endpoint like any internet-facing API — TLS always, audience-scoped tokens, least privilege on exposed skills.
Between agents, OAuth2 client credentials against your SAP IAS tenant (or Entra ID on the Azure side) is the clean default; v1.0 added the device code flow, removed implicit and password grants, and lets an authorization server require PKCE through pkce_required. In a Joule topology, let BTP destination service attach the credential so agent code never holds a secret; on Azure, an A2A caller needs the Foundry Agent Consumer role at project or agent scope, with authentication in on-behalf-of and service-identity variants; key-based and anonymous card access are not supported.
Two subtleties. First, the card itself is sensitive: an unauthenticated public card advertises your skills to anyone probing the well-known URI, so gate it with mTLS, IP allow-lists, or an authenticated extended card that reveals more to verified callers. Second, delegation must not silently widen authority: if Joule calls Foundry and Foundry calls an SAP HANA Cloud MCP tool, carry the user’s identity to the data layer, or give the intermediate agent a scoped service identity with writes gated by approval.
FAQ
Does A2A replace MCP in an SAP landscape?
No. MCP connects one agent to its tools and data; A2A connects agents to each other. SAP BTP ships both — Integration Suite’s MCP Gateway and Joule’s A2A support are complementary. The useful split: MCP inside your agent, A2A across the organisational boundary.
My agent card uses url and kind — will a v1.0 client still work?
Those are v0.3 fields. v1.0 moved transport details into supportedInterfaces, each with its own protocolVersion, and removed the kind discriminators. Serve both cards from one endpoint, or advertise v0.3 explicitly, since version negotiation is per-interface via the A2A-Version header.
Can a Joule agent stream A2A responses to the user?
Not on that path. Joule issues a synchronous A2A message with a roughly 60-second timeout, and the Azure incoming endpoint disables SSE. Design for one completed task, or return input-required and continue in a later turn.
How do I test an A2A server before registering it?
Fetch /.well-known/agent-card.json with curl, then send a SendMessage JSON-RPC request with a text part and check the reply is a task carrying an artifact. The official tutorials cover the client side, and the A2A CLI works for CI smoke tests.
References
- A2A Protocol Specification (v1.0) — https://a2a-protocol.org/latest/specification/
- Core Concepts — https://a2a-protocol.org/latest/topics/key-concepts/
- A2A and MCP comparison — https://a2a-protocol.org/latest/topics/a2a-and-mcp/
- Life of a Task — https://a2a-protocol.org/latest/topics/life-of-a-task/
- Agent Discovery — https://a2a-protocol.org/latest/topics/agent-discovery/
- Enterprise Features — https://a2a-protocol.org/latest/topics/enterprise-ready/
- What’s New in v1.0 — https://a2a-protocol.org/latest/whats-new-v1/
- Python quickstart: Agent Executor — https://a2a-protocol.org/latest/tutorials/python/4-agent-executor/
- Python quickstart: Start Server — https://a2a-protocol.org/latest/tutorials/python/5-start-server/
a2a-sdkon PyPI — https://pypi.org/project/a2a-sdk/- Microsoft Foundry: Enable an A2A endpoint — https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/enable-agent-to-agent-endpoint
- Microsoft Foundry: Connect to an A2A agent endpoint — https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/tools/agent-to-agent
- Microsoft Foundry: A2A authentication — https://learn.microsoft.com/en-us/azure/foundry/agents/concepts/agent-to-agent-authentication
- SAP News: New agentic capabilities in SAP BTP — https://news.sap.com/2025-11-new-agentic-capabilities-sap-btp-supercharge-developers/
- SAP Help: Bring your own agent (Joule) — https://help.sap.com/docs/joule/joule-development-guide-ba88d1ec6a1b442098863d577c19b0c0/code-based-agents-bring-your-own-agent
- SAP Help: Create HTTP destinations — https://help.sap.com/docs/connectivity/sap-btp-connectivity-cf/create-http-destinations
- SAP sample: joule-a2a-agent-toolkit — https://github.com/SAP-samples/joule-a2a-agent-toolkit
- RFC 8615: Well-Known URIs — https://www.rfc-editor.org/rfc/rfc8615
- Linux Foundation: Agentic AI Foundation — https://www.linuxfoundation.org/press/agentic-ai-foundation-announces-four-new-projects-agent-skills-open-responses-openenv-and-opensandbox
Next Steps
- Continue with Integrating SAP BTP with Azure OpenAI to wire the model layer behind your agents.
- Browse more agent-to-agent work in the A2A integration category.