Google's A2A Protocol and the Elixir Answer Nobody Asked For
Agent-to-agent communication is the new distributed systems. Elixir solved this in 1986.
Google announced the Agent-to-Agent (A2A) protocol in April 2025 and the AI world treated it like a solved problem with a press release attached. The spec describes a standard HTTP/JSON protocol for AI agents to discover each other, negotiate capabilities, and delegate tasks — a kind of REST API, but the callers and responders are themselves AI systems. The goal is interoperability: a Claude agent and a Gemini agent should be able to coordinate without either one knowing what’s running on the other side.
This is genuinely useful. It’s also a problem Elixir developers will recognize immediately, because it’s distributed message passing with an HTTP wrapper bolted on.
What A2A Actually Describes
Strip away the “agentic” framing and A2A is a specification for three things: discovery (how does agent A find agent B?), capability negotiation (what can agent B do, and in what format?), and task delegation (how does A send B work and receive results, including streaming?).
The transport is HTTP. The format is JSON. Agents expose an agent-card at a well-known URL describing their capabilities. Task requests go over POST, long-running tasks return a task ID for polling or streaming via SSE.
This is fine engineering. It’s also, if you’re an Elixir developer, a description of things you already have. Registry gives you discovery. Process capabilities are just functions on a GenServer. Delegation is GenServer.call/3. Streaming is a Task that sends messages to a listening process. The OTP primitives map almost perfectly onto what A2A is trying to standardize across HTTP.
The interesting question isn’t whether A2A is a good spec. It’s whether the framing — cross-service HTTP coordination — is actually the right primitive for multi-agent systems, or whether it’s a concession to the Python ecosystem’s architectural constraints.
The Elixir Angle: You Already Have This
Imagine you’re building a multi-agent pipeline in Elixir. You have a ResearchAgent, a SummaryAgent, and a CritiquAgent. Each is a GenServer. They coordinate via message passing. The supervisor tree ensures each one is alive; if one crashes, it restarts independently without taking the others down. Distribution across nodes is handled by :rpc or Horde for cluster-wide process registration.
defmodule PipelineOrchestrator do
use GenServer
def run_pipeline(topic) do
GenServer.call(__MODULE__, {:run, topic})
end
def handle_call({:run, topic}, _from, state) do
research = GenServer.call(ResearchAgent, {:research, topic})
summary = GenServer.call(SummaryAgent, {:summarize, research})
critique = GenServer.call(CritiqueAgent, {:critique, summary})
{:reply, %{research: research, summary: summary, critique: critique}, state}
end
end
What A2A adds to this picture is cross-service, cross-runtime coordination. If your ResearchAgent is a Python service running a specialized model and your SummaryAgent is a Go service with a fine-tuned summarizer, you need a protocol. That’s real. A2A solves it.
But if your whole pipeline runs in Elixir? You’re already there. You don’t need discovery — Registry or Horde handles it. You don’t need capability negotiation — that’s just your function signatures. You don’t need task polling — that’s GenServer’s synchronous call model. The overhead A2A introduces (HTTP round trips, JSON serialization, service discovery lookups) is real cost with no benefit in a monolith.
Where A2A Actually Wins
There are cases where A2A’s model is genuinely the right one. If you’re composing agents across organizational boundaries — your sales agent delegates to a partner’s scheduling agent, which delegates to a vendor’s availability service — you need a neutral protocol. No one is running the same runtime. No one should have to.
There’s also a developer experience argument. A2A’s agent-card schema is a standard format for expressing what an agent can do. That’s a useful thing to have, even inside a monolith — a self-describing agent is easier to compose, test, and reason about than one whose capabilities are implicit in its function signatures.
The honest position: A2A is primarily valuable at the boundaries between systems, not within them. The mistake to avoid is treating it as the default primitive for all agent coordination, which is what you’ll do if your mental model of “services” starts and ends with HTTP.
Calling A2A Agents from Elixir
For Elixir teams that do need to interoperate with A2A agents — calling an external Python or JS agent from a Phoenix application — the implementation is straightforward. Req is the community’s clear choice here:
defmodule A2AClient do
@spec call_agent(String.t(), String.t(), map()) :: {:ok, map()} | {:error, term()}
def call_agent(base_url, task, input) do
Req.post("#{base_url}/tasks",
json: %{task: task, input: input},
auth: {:bearer, Application.fetch_env!(:myapp, :a2a_token)}
)
|> case do
{:ok, %{status: 200, body: body}} -> {:ok, body}
{:ok, %{status: status}} -> {:error, {:unexpected_status, status}}
{:error, _} = err -> err
end
end
end
Wrap that in a supervised GenServer, add retry logic via exponential_backoff/1, and you have a resilient A2A client that degrades gracefully when the external agent is unavailable. OTP handles the failure modes. A2A handles the protocol. They compose cleanly.
The Take
A2A is the right protocol for the right problem: heterogeneous, cross-runtime agent coordination where the participants can’t share a process model. Google’s bet is that multi-agent systems will look like microservices — each agent a service, each interaction an HTTP call. That’s probably right for enterprise systems that need to compose across vendors.
The Elixir bet is different: if you control the runtime, you don’t need HTTP between your agents. Message passing is cheaper, supervision is better, and the debuggability story is far simpler when your whole pipeline is in Observer. The architectural clarity you get from running agents as processes in a supervision tree is worth something — and it’s something that HTTP-first frameworks will spend years trying to recreate.
Use A2A at the edges. Keep the inside clean.
Enjoy this issue?
Get ElixirLens every Monday — sharp takes on Elixir and the trends shaping it.