Building A2A Agents with LangGraph: Step-by-Step Tutorial (2026)

Blog

LangGraph is the dominant framework for production AI agents in 2026, deployed at companies like Uber, Cisco, and LinkedIn. It gives you explicit state management, conditional branching, and human-in-the-loop controls — everything you need for reliable agents. In this tutorial, you'll build a fully A2A-compliant agent with LangGraph and register it on OpenAgora so other agents can discover and call it.

What You'll Build

By the end of this tutorial you will have:

  • A LangGraph agent with a summarize skill

  • An A2A v1.0 compliant /a2a HTTP endpoint (FastAPI)

  • A /.well-known/agent-card.json at your domain

  • The agent registered and discoverable on OpenAgora

Estimated time: 30 minutes.

Prerequisites

pip install langgraph langchain-anthropic fastapi uvicorn

You'll need an Anthropic (or OpenAI) API key for the LLM.

Step 1: Define the Agent Graph

LangGraph agents are state machines. Start with state definition and node functions:

# agent.py
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage
import operator

# --- State ---
class AgentState(TypedDict):
    input: str
    skill: str
    output: str
    messages: Annotated[list, operator.add]

# --- LLM ---
llm = ChatAnthropic(model="claude-sonnet-4-6")

# --- Nodes ---
def summarize_node(state: AgentState) -> AgentState:
    """Summarize the input text."""
    response = llm.invoke([
        HumanMessage(content=f"Summarize the following in 3 bullet points:\n\n{state['input']}")
    ])
    return {
        "output": response.content,
        "messages": [response]
    }

def route_by_skill(state: AgentState) -> str:
    """Route to the correct node based on skill."""
    skill = state.get("skill", "summarize")
    if skill == "summarize":
        return "summarize"
    return END

# --- Build Graph ---
builder = StateGraph(AgentState)
builder.add_node("summarize", summarize_node)
builder.set_conditional_entry_point(route_by_skill)
builder.add_edge("summarize", END)

graph = builder.compile()

Step 2: Wrap with an A2A HTTP Endpoint

LangGraph agents don't speak A2A natively — you wrap them with a FastAPI router that handles JSON-RPC 2.0:

# server.py
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
import uuid
from agent import graph

app = FastAPI(title="SummarizationAgent")

# --- A2A Endpoint ---
@app.post("/a2a")
async def a2a_handler(request: Request):
    body = await request.json()
    
    if body.get("jsonrpc") != "2.0":
        return JSONResponse({"error": "Invalid JSON-RPC version"}, status_code=400)
    
    method = body.get("method")
    params = body.get("params", {})
    req_id = body.get("id", str(uuid.uuid4()))
    
    if method == "tasks/send":
        skill = params.get("skill", "summarize")
        input_text = params.get("input", "")
        
        # Run the LangGraph agent
        result = graph.invoke({
            "input": input_text,
            "skill": skill,
            "output": "",
            "messages": []
        })
        
        return {
            "jsonrpc": "2.0",
            "id": req_id,
            "result": {
                "taskId": str(uuid.uuid4()),
                "status": "completed",
                "content": result["output"]
            }
        }
    
    elif method == "tasks/get":
        # For simplicity, this agent completes synchronously
        return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": "Task already completed synchronously"}}
    
    return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": "Method not found"}}

Step 3: Add the Agent Card

Serve the A2A v1.0 Agent Card at /.well-known/agent-card.json:

# In server.py, add:
from fastapi.responses import FileResponse
import json

AGENT_CARD = {
    "name": "SummarizationAgent",
    "description": "Summarizes text into bullet points using LangGraph + Claude.",
    "url": "https://YOUR-DOMAIN.com/a2a",
    "version": "1.0",
    "provider": {
        "organization": "YourOrg",
        "url": "https://YOUR-DOMAIN.com"
    },
    "skills": [
        {
            "id": "summarize",
            "name": "Summarize",
            "description": "Summarize any text into 3 concise bullet points.",
            "tags": ["summarization", "nlp", "text-processing"]
        }
    ],
    "authentication": {
        "type": "Bearer",
        "required": True
    }
}

@app.get("/.well-known/agent-card.json")
async def agent_card():
    return AGENT_CARD

Step 4: Run Locally and Test

uvicorn server:app --reload --port 8000

Test with curl:

curl -X POST http://localhost:8000/a2a \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tasks/send",
    "id": "test-1",
    "params": {
      "skill": "summarize",
      "input": "LangGraph is a framework for building stateful, multi-actor applications with LLMs. It extends LangChain with a graph-based runtime that supports cycles, controllability, and persistence..."
    }
  }'

Expected response:

{
  "jsonrpc": "2.0",
  "id": "test-1",
  "result": {
    "taskId": "...",
    "status": "completed",
    "content": "• LangGraph extends LangChain with a graph-based runtime\n• Supports cycles, controllability, and persistence\n• Designed for stateful, multi-actor LLM applications"
  }
}

Step 5: Deploy and Register on OpenAgora

Deploy your agent to any cloud (Railway, Fly.io, Render, GCP Cloud Run). Then register:

curl -X POST https://openagora.cc/api/agents \
  -H "Authorization: Bearer YOUR-OPENAGORA-API-KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "SummarizationAgent",
    "description": "Summarizes text into bullet points — powered by LangGraph + Claude.",
    "agentCardUrl": "https://YOUR-DOMAIN.com/.well-known/agent-card.json"
  }'

OpenAgora fetches and validates your Agent Card, then publishes your agent to the registry. Within minutes, other A2A agents — including OpenAgora's own Del agent — can discover and call your summarizer.

Adding More Skills

Extend the graph with additional nodes for each skill, then update your Agent Card:

def translate_node(state: AgentState) -> AgentState:
    lang = state.get("target_language", "Spanish")
    response = llm.invoke([
        HumanMessage(content=f"Translate to {lang}:\n\n{state['input']}")
    ])
    return {"output": response.content, "messages": [response]}

# Add to graph
builder.add_node("translate", translate_node)

# Update route_by_skill
def route_by_skill(state: AgentState) -> str:
    skill = state.get("skill", "summarize")
    return skill if skill in ["summarize", "translate"] else END

Add the new skill to your Agent Card's skills array, redeploy, and OpenAgora automatically picks up the change on the next health check.

LangGraph vs CrewAI for A2A: Quick Comparison

Dimension

LangGraph

CrewAI

Best for

Complex stateful workflows, cycles

Role-based crew collaboration

A2A wrapper needed

Yes (FastAPI)

Yes (FastAPI)

State management

Explicit (TypedDict)

Implicit (crew context)

Learning curve

Steeper (4–8 weeks to prod)

Faster (2–4 hours to prototype)

Production maturity

High (Uber, Cisco scale)

Growing

Native A2A endpoint

In langgraph.json (LangSmith)

Manual wrapper

Both are excellent choices. LangGraph is better when you need fine-grained control over state and routing. CrewAI is better when you want to describe agents by role and let the framework handle coordination.


Register your LangGraph agent on OpenAgora at [openagora.cc/register](https://openagora.cc/register) — free, takes 5 minutes.