You have two agents that need to work together. One handles compliance checks, built by a partner team in Python using Google's ADK. The other is your .NET service that orchestrates customer onboarding. Today, you would write custom HTTP endpoints, invent a message format, and maintain bespoke serialisation logic on both sides. When a third agent joins — perhaps a billing validator from yet another team — you repeat the exercise. The integration surface grows linearly with each new agent, and none of it is reusable.

The Agent-to-Agent (A2A) protocol exists to eliminate that glue code. Originally developed by Google and now governed by the Linux Foundation with backing from AWS, Cisco, IBM Research, Microsoft, Salesforce, SAP, and ServiceNow, A2A defines a standard way for agents to discover each other, exchange messages, and coordinate on tasks — over HTTP, across any boundary, in any language or framework.

On 28 April 2026, Microsoft shipped A2A v1 support in the Agent Framework for .NET, making it possible to both expose and consume A2A agents using the same AIAgent abstraction you already use for local agents. A remote agent built in Python on the other side of the network looks identical to a local one in your code.

What A2A actually is

A2A is a wire protocol for agent interoperability. It sits at the same layer as REST or gRPC — it defines how agents communicate, not what they do internally. The protocol has four core concepts:

The transport is HTTP with JSON-RPC for the request/response binding and Server-Sent Events for streaming. If you have built a webhook handler or a minimal API endpoint, the mechanics are familiar.

How A2A differs from MCP

If you are already using MCP (Model Context Protocol) with your agents, you might wonder where A2A fits. The distinction is straightforward:

They are complementary, not competing. An agent might use MCP to access a database and A2A to delegate a compliance check to a partner team's agent. The Agent Framework supports both simultaneously.

Exposing an agent via A2A

The server-side story uses familiar ASP.NET Core patterns. You register your agent in DI, add A2A server services, and map the A2A endpoints. The hosting library acts as a protocol adapter — it translates incoming A2A requests into RunAsync calls on your agent and formats the responses back into A2A messages.

Program.cs
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.Hosting.A2A;

var builder = WebApplication.CreateBuilder(args);

builder.AddAIAgent(
    "onboarding-validator",
    instructions: """
        You validate customer onboarding submissions.
        Check that all required fields are present and correctly formatted.
        Flag any compliance issues.
        """,
    description: "Validates customer onboarding data for completeness and compliance.");

builder.Services.AddA2AServer();

var app = builder.Build();

app.MapA2AServer();
app.Run();

That is the complete server. The framework handles agent card generation, task lifecycle management, request routing, and response serialisation. Your agent's description and name flow directly into the published agent card, which clients use for discovery.

// NOTE

The AddAIAgent extension registers the agent with a model provider configured via standard .NET configuration. The hosting layer is provider-agnostic — it does not care whether the underlying agent uses Azure OpenAI, Anthropic, or Ollama.

Customising the agent card

The auto-generated agent card works for most cases, but you can customise it to declare specific skills, input/output modes, and authentication requirements:

Program.cs
builder.Services.AddA2AServer(options =>
{
    options.AgentCard = new AgentCard
    {
        Name = "Onboarding Validator",
        Description = "Validates customer onboarding data for completeness and compliance.",
        Version = "1.0.0",
        Skills =
        [
            new AgentSkill
            {
                Id = "validate-kyc",
                Name = "KYC Validation",
                Description = "Validates Know Your Customer documentation",
                InputModes = ["text/plain", "application/json"],
                OutputModes = ["application/json"]
            }
        ],
        Capabilities = new AgentCapabilities
        {
            Streaming = true,
            PushNotifications = false
        }
    };
});

Skills are how clients understand what your agent can do. A well-described skill with clear input/output modes lets orchestrating agents route work intelligently without human configuration.

Consuming a remote A2A agent

The client side is even simpler. The A2ACardResolver fetches the agent card from a remote endpoint and returns a standard AIAgent that you use exactly like a local one:

Services/ComplianceService.cs
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.A2A;

public class ComplianceService(HttpClient httpClient)
{
    public async Task<string> ValidateCustomerAsync(string customerData)
    {
        var resolver = new A2ACardResolver(
            new Uri("https://compliance.partner-team.internal"),
            httpClient);

        AIAgent complianceAgent = await resolver.GetAIAgentAsync();

        var result = await complianceAgent.RunAsync(
            $"Validate this customer submission: {customerData}");

        return result;
    }
}

The key insight is that complianceAgent implements the same AIAgent interface as your local agents. You can pass it into workflows, use it as a tool for another agent, or stream its responses — all with the same API surface. The A2A transport is invisible to your application logic.

Streaming responses

For long-running tasks where you want incremental updates, use the streaming variant:

Services/AnalysisService.cs
public async IAsyncEnumerable<string> AnalyseDocumentAsync(
    string documentContent,
    [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
    var resolver = new A2ACardResolver(
        new Uri("https://analysis.internal"),
        httpClient);

    AIAgent analyst = await resolver.GetAIAgentAsync();

    await foreach (var chunk in analyst.RunStreamingAsync(
        $"Analyse this document: {documentContent}",
        cancellationToken: cancellationToken))
    {
        yield return chunk;
    }
}

Streaming uses Server-Sent Events under the hood. The agent card declares whether streaming is supported via the Capabilities.Streaming flag, so clients can check before attempting it.

Using A2A agents in multi-agent workflows

Where A2A becomes genuinely powerful is in orchestration. Because remote agents look identical to local ones, you can mix them freely in the Agent Framework's workflow patterns:

Workflows/OnboardingWorkflow.cs
using Microsoft.Agents.AI;
using Microsoft.Agents.AI.A2A;
using Microsoft.Agents.AI.Workflows;

public static class OnboardingWorkflow
{
    public static async Task<string> RunAsync(
        AIAgent localFormParser,
        A2ACardResolver complianceResolver,
        A2ACardResolver billingResolver,
        string submission)
    {
        AIAgent complianceAgent = await complianceResolver.GetAIAgentAsync();
        AIAgent billingAgent = await billingResolver.GetAIAgentAsync();

        var parsed = await localFormParser.RunAsync(
            $"Extract structured data from: {submission}");

        var complianceResult = await complianceAgent.RunAsync(
            $"Check compliance for: {parsed}");

        var billingResult = await billingAgent.RunAsync(
            $"Validate billing information: {parsed}");

        return $"Compliance: {complianceResult}\nBilling: {billingResult}";
    }
}

The compliance agent might be a Python service running Google ADK. The billing agent might be a Java service using LangChain4j. Your .NET orchestrator does not know or care — it communicates through the standard A2A protocol.

Authentication and security

A2A treats security as a discoverable contract. Each agent publishes its required authentication in the agent card under securitySchemes, following the same conventions as OpenAPI:

.well-known/agent-card.json
{
  "name": "Compliance Validator",
  "url": "https://compliance.partner-team.internal/a2a",
  "securitySchemes": {
    "bearer": {
      "type": "http",
      "scheme": "bearer",
      "bearerFormat": "JWT"
    }
  },
  "security": [
    { "bearer": ["compliance:read", "compliance:validate"] }
  ]
}

Clients discover the required scheme by reading the agent card and configure their HTTP client accordingly. The Agent Framework's A2ACardResolver accepts a pre-configured HttpClient, so you wire up authentication using standard DelegatingHandler patterns:

Program.cs
builder.Services.AddHttpClient("compliance-a2a", client =>
{
    client.BaseAddress = new Uri("https://compliance.partner-team.internal");
})
.AddHttpMessageHandler<BearerTokenHandler>();

// WARNING

Never hardcode tokens or skip authentication validation in production A2A endpoints. The protocol's security model relies on both sides correctly implementing the schemes declared in their agent cards.

Task lifecycle and state management

A2A tasks progress through well-defined states: submitted, working, input-required, completed, failed, and cancelled. The input-required state is particularly interesting — it enables human-in-the-loop patterns where a remote agent can pause execution and request additional information from the client.

On the server side, the Agent Framework manages task state automatically when you use the standard hosting model. For more complex scenarios where you need explicit control over task transitions, you can implement a custom ITaskHandler:

Handlers/ReviewTaskHandler.cs
public class ReviewTaskHandler : ITaskHandler
{
    public async Task<TaskResponse> HandleAsync(
        TaskRequest request,
        CancellationToken cancellationToken)
    {
        var review = await PerformReviewAsync(request.Message);

        if (review.NeedsHumanApproval)
        {
            return TaskResponse.InputRequired(
                "This submission requires manual review. Please confirm approval.");
        }

        return TaskResponse.Completed(review.Result);
    }
}

The client sees the input-required status and can respond with additional context, creating a conversational loop between agents without either side needing to know the other's internal implementation.

Common pitfalls

Treating A2A like a synchronous RPC. A2A tasks are inherently asynchronous. Even though RunAsync awaits the result, the remote agent might take seconds or minutes to complete. Design your consuming code to handle timeouts and cancellation gracefully, and prefer streaming for anything that might take longer than a few seconds.

Publishing overly broad agent cards. An agent card that says "I can do anything with customer data" gives clients no useful information for routing. Be specific about skills, input modes, and output modes. The more precise your card, the better orchestrating agents can select the right agent for a task.

Skipping the discovery step in production. It is tempting to hardcode the remote agent's URL and skip A2ACardResolver. This works until the remote team changes their authentication scheme or adds a required scope. Always resolve the agent card — it is cheap (cacheable) and keeps your integration resilient to changes.

Confusing A2A with MCP. If you need your agent to call a function or read a database, that is MCP. If you need your agent to delegate a task to another autonomous agent that reasons independently, that is A2A. Using A2A where MCP suffices adds unnecessary complexity and latency.

Ignoring error states. A2A tasks can fail. The remote agent might return a failed status with an error message. Your consuming code should handle this explicitly rather than assuming every task completes successfully.

Summary