LangGraph Tutorial (Python): adding observability for advanced developers

By Cyprian AaronsUpdated 2026-04-22
langgraphadding-observability-for-advanced-developerspython

This tutorial shows how to add real observability to a LangGraph app in Python using structured state, node-level metadata, and LangSmith tracing. You need this when a graph starts branching, retries kick in, or you need to answer basic questions like “which node failed,” “what inputs produced this output,” and “how long did each step take?”

What You'll Need

  • Python 3.10+
  • langgraph
  • langchain-core
  • langchain-openai
  • langsmith
  • An OpenAI API key
  • A LangSmith API key
  • Environment variables set for tracing:
    • OPENAI_API_KEY
    • LANGSMITH_API_KEY
    • LANGSMITH_TRACING=true
    • LANGSMITH_PROJECT=langgraph-observability-demo

Step-by-Step

  1. Start with a graph that carries enough state to be useful in traces.
    If your state is vague, your observability will be vague too. Keep the state typed and include fields you actually want to inspect later.
from typing import TypedDict, Annotated
import operator

from langgraph.graph import StateGraph, START, END

class AgentState(TypedDict):
    question: str
    draft: str
    review_notes: str
    final_answer: str
    steps: Annotated[list[str], operator.add]
  1. Add nodes that produce traceable outputs and update the state in small increments.
    Small nodes are easier to inspect in LangSmith because each node becomes a distinct span with clear inputs and outputs.
def draft_node(state: AgentState) -> dict:
    draft = f"Draft answer for: {state['question']}"
    return {
        "draft": draft,
        "steps": ["draft_created"],
    }

def review_node(state: AgentState) -> dict:
    notes = "Looks good, but add one concrete example."
    return {
        "review_notes": notes,
        "steps": ["review_completed"],
    }

def finalize_node(state: AgentState) -> dict:
    final = f"{state['draft']}\n\nReview notes: {state['review_notes']}"
    return {
        "final_answer": final,
        "steps": ["finalized"],
    }
  1. Wire the graph with explicit edges so the execution path is visible.
    When you inspect a trace later, the graph structure should match what you see in runtime behavior.
builder = StateGraph(AgentState)

builder.add_node("draft", draft_node)
builder.add_node("review", review_node)
builder.add_node("finalize", finalize_node)

builder.add_edge(START, "draft")
builder.add_edge("draft", "review")
builder.add_edge("review", "finalize")
builder.add_edge("finalize", END)

graph = builder.compile()
  1. Enable LangSmith tracing before you invoke the graph.
    This is the part that turns your local execution into searchable runs with node timing, inputs, outputs, and errors.
import os
from langchain_core.runnables import RunnableConfig

os.environ["LANGSMITH_TRACING"] = "true"
os.environ["LANGSMITH_PROJECT"] = "langgraph-observability-demo"

config = RunnableConfig(
    tags=["observability", "langgraph"],
    metadata={
        "service": "support-agent",
        "env": "dev",
        "owner": "platform-team",
    },
)
  1. Run the graph and inspect both the result and the trace metadata.
    In production, this is where you correlate a user request with a specific run ID or project tag in LangSmith.
result = graph.invoke(
    {"question": "How do I reset my MFA device?", "steps": []},
    config=config,
)

print(result["final_answer"])
print(result["steps"])
  1. Add an LLM-backed node when you want token usage and model-level visibility.
    This is where observability becomes useful for cost control and prompt debugging, because LangSmith can show prompt content, model calls, latency, and failures.
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

def llm_node(state: AgentState) -> dict:
    response = llm.invoke(
        [HumanMessage(content=f"Answer clearly: {state['question']}")]
    )
    return {
        "draft": response.content,
        "steps": ["llm_called"],
    }

Testing It

Run the script once with valid API keys set in your shell. You should see a final answer printed locally, and a corresponding run should appear in LangSmith under the project name you configured.

Check that each node shows up as its own span or step in the trace. The important signals are input state at each node, output diffs, execution order, and latency per node.

If you swap draft_node for llm_node, confirm that token usage and prompt content are visible in the trace. That tells you tracing is capturing model calls instead of only Python function boundaries.

If something fails, LangSmith should show exactly which node raised the error and what state it received. That’s the difference between debugging a graph and guessing at logs.

Next Steps

  • Add conditional edges so you can trace branching decisions and failure paths.
  • Attach custom tags per customer or workflow type so traces are easy to filter in LangSmith.
  • Wrap external tools or database calls as separate nodes to isolate latency and failure hotspots.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides