LangGraph
Use Confident AI for LLM observability and evals for LangGraph
Overview
LangGraph is a framework for building reactive, multi-agent systems. Confident AI traces and evaluates your LangGraph agents automatically through confident-trace, Confident AI's OpenTelemetry-native tracing SDK for Python and TypeScript — call init() once and your graph code stays exactly as it is.
The integration captures the following spans from your LangGraph agent:
- Graph and node spans — the root span for each
invoke/streamcall (including subgraphs), plus one span per node and intermediate runnable - LLM spans — model name, token usage, finish reasons, and input/output messages (including tool calls made by the model)
- Tool spans — tool name, input parameters, and output, nested under the node that ran them
- Retriever spans — query input and retrieved document text
| Runtime | Requirements | Setup |
|---|---|---|
| Python | Python 3.10+, LangGraph 1.x | Call init() before invoking the graph |
| TypeScript | Node.js 22+, @langchain/langgraph >=1.4.14 <2, @langchain/core >=1.2.9 <2 | Call init() and launch your entry point with the preload |
Auto-Instrument
Install Dependencies
Run the following command to install
confident-tracealongside LangGraph:pip install confident-trace 'langgraph>=1,<2' 'langchain-openai>=1,<2'tsxis only needed if you run TypeScript source directly.npm install confident-trace '@langchain/langgraph@>=1.4.14 <2' '@langchain/core@>=1.2.9 <2' @langchain/openai@1 npm install -D tsxyarn add confident-trace '@langchain/langgraph@>=1.4.14 <2' '@langchain/core@>=1.2.9 <2' @langchain/openai@1 yarn add -D tsxSet Your API Keys
Get your Confident AI Project API key and set it as an environment variable, along with your model provider's key:
export CONFIDENT_API_KEY="<your-confident-project-key>" export OPENAI_API_KEY="<your-openai-key>"Instrument LangGraph
Call
init()once before invoking your graph. It detects LangGraph automatically and attaches its callback handler for you — there's no handler to pass inconfig, and no tracing extra to install.main.py from confident_trace import init, shutdown from langchain_openai import ChatOpenAI from langgraph.graph import END, START, MessagesState, StateGraph init() model = ChatOpenAI(model="gpt-4.1-mini") def assistant(state: MessagesState): return {"messages": [model.invoke(state["messages"])]} graph = ( StateGraph(MessagesState) .add_node("assistant", assistant) .add_edge(START, "assistant") .add_edge("assistant", END) .compile() ) try: result = graph.invoke({"messages": [{"role": "user", "content": "what is the weather in sf"}]}) print(result["messages"][-1].content) finally: shutdown()src/index.ts import { init } from "confident-trace"; import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph"; import { ChatOpenAI } from "@langchain/openai"; const runtime = init(); const model = new ChatOpenAI({ model: "gpt-4.1-mini" }); const graph = new StateGraph(MessagesAnnotation) .addNode("assistant", async (state) => ({ messages: [await model.invoke(state.messages)], })) .addEdge(START, "assistant") .addEdge("assistant", END) .compile(); try { const result = await graph.invoke({ messages: [{ role: "user", content: "what is the weather in sf" }], }); console.log(result.messages.at(-1)?.content); } finally { await runtime.shutdown(); }TypeScript needs one more thing: launch your entry point with the
confident-trace/registerpreload so the SDK can hook@langchain/langgraphas Node loads it.init()handles export, the preload handles instrumentation — you need both.Run LangGraph
Run your script to send the trace to Confident AI:
python main.py# Running TypeScript source directly node --import tsx --import confident-trace/register src/index.ts # Running compiled JavaScript node --import confident-trace/register dist/index.jsTo make this your normal startup command, add it to your
package.jsonscripts:package.json { "scripts": { "start": "node --import confident-trace/register dist/index.js", "dev": "node --import tsx --import confident-trace/register src/index.ts" } }Done ✅. Open the Observatory in your Confident AI project to inspect the trace and its graph, node, and model spans.
What Gets Captured
The integration mirrors the hierarchy LangGraph reports through its callbacks:
- Graph invocation — the root span for each
invoke/streamcall, including subgraphs. - Nodes and runnables — one span per node and each intermediate runnable inside it.
- Model calls — LLM spans with model name, token usage, finish reasons, and normalized input/output messages.
- Tool executions — tool spans, parented under the node that ran them, normally alongside the model that requested them.
- Retrievers — retriever spans with retrieved document text.
Graph state, tool values, and document text follow the content policy. Size limits are disabled by default, but you can configure a limit or redact content before export.
Trace a LangGraph Server Deployment
If you deploy your graph with the LangGraph server (langgraph dev or LangGraph Platform), the server executes the graph in its own process — so tracing has to be initialized inside that process, not in whatever client is calling it. The pattern is the same as the quickstart: call init() once in the module that exports your graph.
Initialize tracing in your graph module
Call
init()at the top of the file that builds and exports the graph. The server imports this module once at startup, soinit()runs once and every run the server executes is traced.agent.py from confident_trace import init from langchain.agents import create_agent from langchain_openai import ChatOpenAI init() def get_weather(city: str) -> str: """Returns the weather in a city""" return f"It's always sunny in {city}!" graph = create_agent( model=ChatOpenAI(model="gpt-4.1-mini"), tools=[get_weather], system_prompt="You are a helpful assistant", )agent.ts import { init } from "confident-trace"; import { createAgent } from "langchain"; import { ChatOpenAI } from "@langchain/openai"; import { tool } from "@langchain/core/tools"; import { z } from "zod"; init(); const getWeather = tool( async ({ city }: { city: string }) => `It's always sunny in ${city}!`, { name: "get_weather", description: "Returns the weather in a city", schema: z.object({ city: z.string() }), }, ); export const graph = createAgent({ model: new ChatOpenAI({ model: "gpt-4.1-mini" }), tools: [getWeather], systemPrompt: "You are a helpful assistant", });Register the graph in langgraph.json
Point the
graphsentry at the exported graph variable, and make sureCONFIDENT_API_KEYis in theenvfile the server loads.{ "dependencies": ["."], "graphs": { "agent": "./agent.py:graph" }, "env": ".env" }{ "node_version": "22", "dependencies": ["."], "graphs": { "agent": "./agent.ts:graph" }, "env": ".env" }Start the LangGraph server
Run the server. Every request it runs against the graph is traced to Confident AI.
pip install -U "langgraph-cli[inmem]" langgraph devThe server owns the
nodecommand, so you can't add--importto it directly. Use Node's standardNODE_OPTIONSvariable to apply the preload instead:NODE_OPTIONS="--import confident-trace/register" npx @langchain/langgraph-cli dev
Conversations and Checkpoints
If your graph is compiled with a checkpointer, you're already passing a thread_id — but it's worth being clear that there are two different thread_ids here, belonging to two different systems:
configurable.thread_idis LangGraph's. It selects the checkpoint so the graph remembers earlier turns. Tracing has no say in it.- The trace's thread ID is Confident AI's. It groups each turn's trace into one thread in the Observatory so you can view and evaluate the whole conversation.
Use the same string for both, so the memory the graph sees and the conversation you inspect line up. Each invocation is still its own trace; the thread just groups them. A checkpoint resume after an interrupt starts a new trace rather than continuing the previous one.
These snippets replace the graph.invoke call inside the quickstart's try block and assume the graph was compiled with a checkpointer.
The callback bridge reads configurable.thread_id from LangGraph's run metadata and stamps it on the graph's spans as the conversation ID, so a bare graph.invoke is enough:
thread_id = "conversation-42"
config = {"configurable": {"thread_id": thread_id}}
for prompt in ("Hello", "What did I just say?"):
result = graph.invoke(
{"messages": [{"role": "user", "content": prompt}]}, config
)
print(result["messages"][-1].content)Also supported: ainvoke, stream / astream, batch / abatch, and astream_events v2.
The TypeScript integration doesn't read configurable.thread_id — it only drives graph memory. Wrap each turn in a span and set the trace's thread ID yourself with updateTrace({ threadId }), reusing the same variable:
import { withSpan, updateTrace } from "confident-trace";
const threadId = "conversation-42";
const config = { configurable: { thread_id: threadId } };
for (const prompt of ["Hello", "What did I just say?"]) {
await withSpan({ name: "turn", type: "agent" }, async () => {
updateTrace({ threadId, input: prompt });
const result = await graph.invoke(
{ messages: [{ role: "user", content: prompt }] }, config,
);
const answer = result.messages.at(-1)?.content;
updateTrace({ output: answer });
console.log(answer);
});
}Set Trace Span Properties
Use a trace context to add properties you know before the call starts. It creates no extra span; the trace started by graph.invoke() inherits the tags, metadata, and user ID.
from confident_trace import init, trace_context
init()
with trace_context(
tags=["support"],
metadata={"release": "2026-09"},
user_id="user-42",
):
result = graph.invoke({"messages": [{"role": "user", "content": "Hello"}]})import { init, traceContext } from "confident-trace";
init();
const result = await traceContext(
{
tags: ["support"],
metadata: { release: "2026-09" },
userId: "user-42",
},
() => graph.invoke({ messages: [{ role: "user", content: "Hello" }] }),
);See trace context for every supported trace property and update behavior.
Instrumenting Multi-Turn
You do not need turn() when one LangGraph entry-point call is already one conversational turn—the integration creates that turn's trace automatically. Use turn() when you want to define the boundary yourself, such as grouping two sequential LangGraph calls into one turn. Reuse the same thread ID on later turns to group them into one conversation.
from confident_trace import init, turn
init()
with turn("support-turn", thread_id="chat-42"):
first = graph.invoke({"messages": [{"role": "user", "content": "Find my account."}]})
second = graph.invoke({"messages": first["messages"] + [{"role": "user", "content": "Summarize it."}]})import { init, turn } from "confident-trace";
init();
const second = await turn({ name: "support-turn", threadId: "chat-42" }, async () => {
const first = await graph.invoke({ messages: [{ role: "user", content: "Find my account." }] });
return graph.invoke({ messages: [...first.messages, { role: "user", content: "Summarize it." }] });
});See threads for thread I/O, turn IDs, and user IDs.
Troubleshooting
- No trace: make sure
init()runs before the graph executes, and that the process reachesshutdown()so buffered spans are flushed. - Duplicate spans: you have two instrumentors on the same graph. Let
confident-tracemanage its own handlers — don't add a manualConfidentLangGraphCallbackHandleralongside automatic mode, and don't attach a second provider instrumentor. - Incomplete streams: consume or close graph and model streams before
shutdown(). - Separate traces per turn: expected. Turns sharing a thread ID are grouped as a thread, and a checkpoint resume is a new trace.
- Missing spans in thread pools: submit work with
copy_context().runso the active context reaches the worker.
- No trace: make sure
init()runs before the graph executes, that your start command includes--import confident-trace/register, and that the process reachesshutdown()so buffered spans are flushed.runtime.getInstrumentationStatus()tells you whether the LangGraph hook attached. - Duplicate spans: you have two instrumentors on the same graph. Let
confident-tracemanage its own handlers — don't add a manualConfidentLangGraphCallbackHandleralongside automatic mode, and don't attach a second provider instrumentor. - Incomplete streams: consume or close graph and model streams before
shutdown(). - Separate traces per turn: expected. Turns sharing a thread ID are grouped as a thread, and a checkpoint resume is a new trace. Remember to call
updateTrace({ threadId })on each turn.
For general setup issues, see troubleshooting.
Disable LangGraph Instrumentation
Pass init() a list of integration identifiers to opt in to only those integrations. The identifier for LangGraph is "langgraph" in Python and TypeScript; omit it to disable this integration. An empty list disables all automatic instrumentation:
from confident_trace import init
init(instrumentations=())
# Use ("langgraph",) to opt in; omit "langgraph" to disable it.import { init } from "confident-trace";
init({ instrumentations: [] });
// Use ["langgraph"] to opt in; omit "langgraph" to disable it.This turns off Confident AI's automatic instrumentation; calls made after initialization are not instrumented by this integration.
Next Steps
Threads
Group checkpointed conversations into threads, set turn I/O, and evaluate entire conversations as a single unit.
Online Evals
Run evaluations on traces and spans in real-time as they're ingested into Confident AI to monitor AI quality in production.
Last updated on