Skip to main content

LangGraph Basics

TL;DR
  • LangGraph models a workflow as a graph: a shared State, nodes (functions), and edges (flow).
  • Conditional edges branch β€” that's how you build routers and agent loops.
  • A tool-calling chatbot is just LLM node ⇄ ToolNode, with tools_condition deciding when to stop.

Chains run in a straight line; LangGraph lets flow branch, loop, and hold state β€” which is what agents need. This page builds it up from a toy graph to a tool-using chatbot, one submodule per idea, ending with a cheat sheet.

LangGraph: a simple conditional graph on top, a tool-calling chatbot graph below, both over a shared State

Reading the diagram: top β€” a simple graph where a conditional edge randomly routes to cricket or badminton. Bottom β€” a chatbot where tools_condition routes to a ToolNode when the LLM asks for a tool, then loops the result back. Both share one State object.

State β€” the shared data​

State is a TypedDict that every node reads and updates. It's the graph's memory.

from typing_extensions import TypedDict

class State(TypedDict):
graph_info: str

Each node receives the state and returns a partial update; by default the returned value overrides that key.

Nodes β€” just Python functions​

A node takes the state and returns a dict updating one or more keys.

def start_play(state: State):
return {"graph_info": state["graph_info"] + " I am planning to play"}

def cricket(state: State):
return {"graph_info": state["graph_info"] + " Cricket"}

def badminton(state: State):
return {"graph_info": state["graph_info"] + " Badminton"}

Edges & conditional edges (routing)​

Normal edges connect nodes in order. A conditional edge calls a function that returns the name of the next node β€” this is branching / routing.

import random
from typing import Literal

def random_play(state: State) -> Literal["cricket", "badminton"]:
return "cricket" if random.random() > 0.5 else "badminton"

The router function decides the path at runtime based on the state.

Build, compile, invoke​

Wire nodes and edges onto a StateGraph, mark START and END, then compile().

from langgraph.graph import StateGraph, START, END

graph = StateGraph(State)
graph.add_node("start_play", start_play)
graph.add_node("cricket", cricket)
graph.add_node("badminton", badminton)

graph.add_edge(START, "start_play")
graph.add_conditional_edges("start_play", random_play) # branch here
graph.add_edge("cricket", END)
graph.add_edge("badminton", END)

app = graph.compile()
app.invoke({"graph_info": "Hey My name is Krish"})

START feeds input in, END terminates, and compile() validates the structure. You can render it as a Mermaid diagram with app.get_graph().draw_mermaid_png().

The add_messages reducer​

For chat, state holds a growing list of messages β€” you want new messages appended, not overwritten. The add_messages reducer does exactly that.

from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.messages import AnyMessage
from langgraph.graph.message import add_messages

class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages] # append, don't replace

The Annotated[..., add_messages] part is the key difference from the toy graph β€” it tells LangGraph to merge updates into the list.

A tool-calling chatbot​

Put it together: an LLM bound to tools (arxiv, wikipedia, Tavily), a ToolNode to run them, and tools_condition to route β€” call tools when the LLM requests them, otherwise finish.

from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode, tools_condition
from langchain_community.tools import ArxivQueryRun, WikipediaQueryRun
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_groq import ChatGroq

tools = [ArxivQueryRun(...), WikipediaQueryRun(...), TavilySearchResults()]
llm_with_tools = ChatGroq(model="qwen-qwq-32b").bind_tools(tools)

def tool_calling_llm(state: State):
return {"messages": [llm_with_tools.invoke(state["messages"])]}

builder = StateGraph(State)
builder.add_node("tool_calling_llm", tool_calling_llm)
builder.add_node("tools", ToolNode(tools))

builder.add_edge(START, "tool_calling_llm")
builder.add_conditional_edges("tool_calling_llm", tools_condition) # tool call? β†’ tools : END
builder.add_edge("tools", "tool_calling_llm") # loop the tool result back to the LLM
graph = builder.compile()

graph.invoke({"messages": HumanMessage(content="Recent AI news for March 3rd 2025")})

tools_condition is the prebuilt router: if the last AI message contains a tool call it goes to the ToolNode, otherwise to END. The edge from tools back to the LLM is what makes it a loop β€” the model sees each tool result and decides what to do next.

Cheat sheet​

ConceptCode
Stateclass State(TypedDict): ...
Message statemessages: Annotated[list[AnyMessage], add_messages]
Add nodegraph.add_node("name", fn)
Straight edgegraph.add_edge("a", "b")
Branch / routergraph.add_conditional_edges("node", router_fn)
Tool loopToolNode(tools) + tools_condition + edge back to the LLM
Rungraph.compile().invoke({...})
⚠ Common mistakes
  • Forgetting add_messages on the messages key β€” new messages overwrite the list instead of appending, and the conversation resets every step.
  • No edge from tools back to the LLM β€” the tool result never reaches the model, so it can't answer.
  • A conditional-edge function that returns something other than a valid node name β€” the graph won't know where to go.
  • Forgetting to compile() before invoke β€” you run the compiled app, not the builder.

Quick self-check

What are the three building blocks of a LangGraph?

State (shared TypedDict), nodes (functions that update state), and edges (flow between nodes, including conditional edges for branching).

Why use add_messages on the messages key?

It's a reducer that appends new messages to the list instead of overwriting it β€” so conversation history accumulates across steps.

What does tools_condition do?

It's a prebuilt router: if the latest AI message has a tool call it routes to the ToolNode, otherwise to END.

What makes the chatbot a loop rather than a straight line?

The edge from the tools node back to the LLM node β€” the model sees each tool result and can decide to call another tool or finish.

Related: Updated LangChain (v1) Β· Hybrid Search Β· Glossary

Next: LangChain vs LangGraph vs LangSmith vs Langflow β†’