Developer desk with laptop, cables, and a microcontroller suggesting an agent tool harness
A graph is a harness with the control flow written down.

LangGraph is a Python library, from the LangChain team, for building AI agents as graphs: you define a shared state, write nodes that update it, and connect them with edges, including conditional edges that decide the next step at run time. It exists because real agents are not straight lines. They branch, retry and sometimes need to stop and hand off to a person.

This tutorial builds a small support-triage agent and shows the actual output of running it. The nodes are plain Python functions, so it runs offline and you can see the control flow without a model call in the way. To make it a real agent, you swap one node for a model call. I tested it on langgraph 1.2.14 and Python 3.10.

What are the core LangGraph concepts?

ConceptWhat it isIn the example
StateA typed dictionary every node can read and updatequestion, intent, draft, attempts, answer
NodeA function that takes the state and returns changes to itclassify, draft, verify, escalate
EdgeA fixed link from one node to the nextdraft always goes to verify
Conditional edgeA function that picks the next node from the stateunknown intent goes to escalate
CheckpointerSaves state after each step, per threadMemorySaver

A complete LangGraph example

The graph classifies a question, drafts an answer from a tiny knowledge base, verifies the draft, and retries up to three times before escalating to a human. The first draft is deliberately truncated so you can watch the retry loop fire.

from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import MemorySaver

KB = {"refund": "Refunds are issued to the original payment method.",
      "hours": "Support is open 9am-6pm IST, Monday to Friday."}

class State(TypedDict):
    question: str
    intent: str
    draft: str
    attempts: int
    answer: str

def classify(s):
    q = s["question"].lower()
    intent = "refund" if "refund" in q else "hours" if ("open" in q or "hours" in q) else "unknown"
    return {"intent": intent, "attempts": 0}

def draft(s):
    n = s["attempts"] + 1
    text = KB.get(s["intent"], "")
    if n == 1:                       # first draft is deliberately sloppy
        text = text.split(".")[0][:25]
    return {"draft": text, "attempts": n}

def verify(s):                       # a check the model cannot argue with
    ok = s["draft"].endswith(".")
    return {"answer": s["draft"]} if ok else {}

def escalate(s):
    return {"answer": "Handing you to a human."}

def route_intent(s):
    return "draft" if s["intent"] != "unknown" else "escalate"

def route_verify(s):
    if s.get("answer"): return END
    return "draft" if s["attempts"] < 3 else "escalate"

g = StateGraph(State)
for name, fn in [("classify", classify), ("draft", draft), ("verify", verify), ("escalate", escalate)]:
    g.add_node(name, fn)
g.add_edge(START, "classify")
g.add_conditional_edges("classify", route_intent, ["draft", "escalate"])
g.add_edge("draft", "verify")
g.add_conditional_edges("verify", route_verify, ["draft", "escalate", END])
g.add_edge("escalate", END)
app = g.compile(checkpointer=MemorySaver())

for i, q in enumerate(["How do refunds work?", "When are you open?", "Can you fix my router?"]):
    print("Q:", q)
    for step in app.stream({"question": q}, {"configurable": {"thread_id": f"t{i}"}}, stream_mode="updates"):
        for node, upd in step.items():
            print(f"   {node:9} -> {upd}")
    print()
print(app.get_graph().draw_mermaid())

What does the run look like?

Streaming with stream_mode="updates" prints what each node changed. This is the real output:

Q: How do refunds work?
   classify  -> {'intent': 'refund', 'attempts': 0}
   draft     -> {'draft': 'Refunds are issued to the', 'attempts': 1}
   verify    -> None
   draft     -> {'draft': 'Refunds are issued to the original payment method.', 'attempts': 2}
   verify    -> {'answer': 'Refunds are issued to the original payment method.'}

Q: Can you fix my router?
   classify  -> {'intent': 'unknown', 'attempts': 0}
   escalate  -> {'answer': 'Handing you to a human.'}

Three things are worth noticing. First, verify failed the sloppy first draft, the graph looped back to draft, and the second attempt passed. Second, the unknown question never reached draft: the conditional edge routed it straight to escalate. Third, when verify returns an empty dict, the stream reports it as None, which looks like a bug until you know a node that changes nothing prints that way.

The compiled graph can also describe itself. app.get_graph().draw_mermaid() printed these edges, and the full output renders as a flowchart in any Mermaid viewer:

graph TD;
	__start__ --> classify;
	classify -.-> draft;
	classify -.-> escalate;
	draft --> verify;
	verify -.-> __end__;
	verify -.-> draft;
	verify -.-> escalate;
	escalate --> __end__;

Dotted lines are conditional edges. That picture is the main reason to use a graph: the control flow is data you can print, review and test, instead of being buried inside a while loop.

Why bound the loop?

The line return "draft" if s["attempts"] < 3 else "escalate" is the most important line in the file. An agent that retries until it is satisfied will, sooner or later, retry forever and spend money doing it. Every cycle in a graph needs a counter and a place to go when the counter runs out.

How do I turn this into a real AI agent?

Replace the body of draft with a model call that receives the question and the knowledge-base text, and keep verify as code that the model cannot argue with: a format check, a schema validation, a test run, a lookup that confirms the order number exists. Add a retrieval node in front of it and you have the RAG pattern from my retrieval-augmented generation walkthrough. Add tools and you have the loop described in What Is an AI Agent Harness?

LangGraph or a plain loop?

If your agent is one loop with a few tools, a plain loop is simpler and I would start there. Reach for a graph when you have several distinct steps, branches that depend on earlier results, a need to pause for a human, or a requirement to resume after a crash, which is what the checkpointer is for. I compared the shapes in Agent Loops, Swarms, and Graphs, where I ran each one rather than theorising.

Need an AI agent built for your product?

Designing the graph, writing the verification steps and setting up the evals are the parts that decide whether an agent works in production. That is the work on my AI consulting page, and a 30-minute call is the way to start.

Method note: the code above is the exact file I ran on 8 October 2026 with Python 3.10 and langgraph 1.2.14; the output blocks are copied from that run. I used AI assistance to help write and edit this post and checked every output against the run.