skills/copilotkit-integrations/references/integrations/langgraph.md
CopilotKit supports LangGraph in three configurations: Python with self-hosted FastAPI, Python with LangGraph Platform, and JavaScript/TypeScript. All use the AG-UI protocol.
This is the langgraph-fastapi example pattern. You run the LangGraph agent as a standalone FastAPI server and connect via LangGraphHttpAgent.
poetry or uv for Python dependency management# pyproject.toml
[project]
dependencies = [
"copilotkit==0.1.74",
"langchain==1.0.1",
"langchain-openai==1.0.1",
"langgraph==1.0.1",
"fastapi==0.115.12",
"uvicorn>=0.38.0",
"python-dotenv>=1.0.0",
"ag-ui-langgraph==0.0.22",
"pydantic>=2.0.0,<3.0.0",
]
The agent extends CopilotKitState for shared state and uses the standard ReAct pattern:
from copilotkit import CopilotKitState
from langchain.tools import tool
from langchain_core.messages import SystemMessage
from langchain_core.runnables import RunnableConfig
from langchain_openai import ChatOpenAI
from langgraph.checkpoint.memory import MemorySaver
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode
from langgraph.types import Command
from typing_extensions import Literal
from src.util import should_route_to_tool_node
class AgentState(CopilotKitState):
proverbs: list[str]
@tool
def get_weather(location: str):
"""Get the weather for a given location."""
return f"The weather for {location} is 70 degrees."
tools = [get_weather]
async def chat_node(
state: AgentState, config: RunnableConfig
) -> Command[Literal["tool_node", "__end__"]]:
model = ChatOpenAI(model="gpt-4o")
# Bind both frontend (CopilotKit) actions and backend tools
fe_tools = state.get("copilotkit", {}).get("actions", [])
model_with_tools = model.bind_tools([*fe_tools, *tools])
system_message = SystemMessage(
content=f"You are a helpful assistant. The current proverbs are {state.get('proverbs', [])}."
)
response = await model_with_tools.ainvoke(
[system_message, *state["messages"]], config,
)
tool_calls = response.tool_calls
if tool_calls and should_route_to_tool_node(tool_calls, fe_tools):
return Command(goto="tool_node", update={"messages": response})
return Command(goto="__end__", update={"messages": response})
workflow = StateGraph(AgentState)
workflow.add_node("chat_node", chat_node)
workflow.add_node("tool_node", ToolNode(tools=tools))
workflow.add_edge("tool_node", "chat_node")
workflow.set_entry_point("chat_node")
graph = workflow.compile(checkpointer=MemorySaver())
Key pattern: CopilotKitState provides the copilotkit field containing actions (frontend tools). You must bind both frontend actions and backend tools to the model, then route frontend tool calls back to CopilotKit (not the ToolNode).
from fastapi import FastAPI
from copilotkit import LangGraphAGUIAgent
from ag_ui_langgraph import add_langgraph_fastapi_endpoint
from src.agent import graph
app = FastAPI()
add_langgraph_fastapi_endpoint(
app=app,
agent=LangGraphAGUIAgent(
name="sample_agent",
description="An example agent.",
graph=graph,
),
path="/",
)
import {
CopilotRuntime,
ExperimentalEmptyAdapter,
copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import { LangGraphHttpAgent } from "@copilotkit/runtime/langgraph";
import { NextRequest } from "next/server";
const runtime = new CopilotRuntime({
agents: {
sample_agent: new LangGraphHttpAgent({
url: process.env.AGENT_URL || "http://localhost:8123",
}),
},
});
export const POST = async (req: NextRequest) => {
const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
runtime,
serviceAdapter: new ExperimentalEmptyAdapter(),
endpoint: "/api/copilotkit",
});
return handleRequest(req);
};
Use LangGraphHttpAgent (from @copilotkit/runtime/langgraph) for self-hosted agents. The default port is 8123.
This is the langgraph-python example pattern. Uses LangGraphAgent which connects to a LangGraph deployment (local or cloud).
import { LangGraphAgent } from "@copilotkit/runtime/langgraph";
const defaultAgent = new LangGraphAgent({
deploymentUrl:
process.env.LANGGRAPH_DEPLOYMENT_URL || "http://localhost:8123",
graphId: "sample_agent",
langsmithApiKey: process.env.LANGSMITH_API_KEY || "",
});
const runtime = new CopilotRuntime({
agents: { default: defaultAgent },
a2ui: { injectA2UITool: true },
mcpApps: {
servers: [
{
type: "http",
url: process.env.MCP_SERVER_URL || "https://mcp.excalidraw.com",
serverId: "example_mcp_app",
},
],
},
});
Key difference from self-hosted: LangGraphAgent uses deploymentUrl and graphId (and optionally langsmithApiKey), while LangGraphHttpAgent uses a plain url.
This is the langgraph-js example pattern. The agent is a TypeScript LangGraph graph running in a separate Node.js process.
import { z } from "zod";
import { tool } from "@langchain/core/tools";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage, SystemMessage } from "@langchain/core/messages";
import { MemorySaver, START, StateGraph } from "@langchain/langgraph";
import { ChatOpenAI } from "@langchain/openai";
import {
convertActionsToDynamicStructuredTools,
CopilotKitStateAnnotation,
} from "@copilotkit/sdk-js/langgraph";
import { Annotation } from "@langchain/langgraph";
const AgentStateAnnotation = Annotation.Root({
...CopilotKitStateAnnotation.spec,
proverbs: Annotation<string[]>,
});
export type AgentState = typeof AgentStateAnnotation.State;
const getWeather = tool(
(args) => `The weather for ${args.location} is 70 degrees.`,
{
name: "getWeather",
description: "Get the weather for a given location.",
schema: z.object({ location: z.string() }),
},
);
const tools = [getWeather];
async function chat_node(state: AgentState, config) {
const model = new ChatOpenAI({ temperature: 0, model: "gpt-4o" });
const modelWithTools = model.bindTools!([
...convertActionsToDynamicStructuredTools(state.copilotkit?.actions ?? []),
...tools,
]);
const systemMessage = new SystemMessage({
content: `You are a helpful assistant. The current proverbs are ${JSON.stringify(state.proverbs)}.`,
});
const response = await modelWithTools.invoke(
[systemMessage, ...state.messages],
config,
);
return { messages: response };
}
function shouldContinue({ messages, copilotkit }: AgentState) {
const lastMessage = messages[messages.length - 1] as AIMessage;
if (lastMessage.tool_calls?.length) {
const actions = copilotkit?.actions;
const toolCallName = lastMessage.tool_calls![0].name;
if (!actions || actions.every((action) => action.name !== toolCallName)) {
return "tool_node";
}
}
return "__end__";
}
const workflow = new StateGraph(AgentStateAnnotation)
.addNode("chat_node", chat_node)
.addNode("tool_node", new ToolNode(tools))
.addEdge(START, "chat_node")
.addEdge("tool_node", "chat_node")
.addConditionalEdges("chat_node", shouldContinue);
export const graph = workflow.compile({ checkpointer: new MemorySaver() });
Key JS-specific patterns:
CopilotKitStateAnnotation from @copilotkit/sdk-js/langgraph to include CopilotKit stateconvertActionsToDynamicStructuredTools() to convert frontend actions to LangChain toolscopilotkit.actions to determine whether a tool call should route to tool_node (backend) or __end__ (frontend)Same as the Platform pattern -- uses LangGraphAgent with deploymentUrl and graphId.
The JS variant uses a Turborepo monorepo:
apps/
web/ # Next.js frontend
agent/ # LangGraph agent (Node.js)
pnpm-workspace.yaml
turbo.json
Run pnpm dev to start both apps via Turborepo.