On this article, you’ll discover ways to construct an entire agentic workflow in Python with LangGraph, from a single mannequin name to a tool-using agent with persistent dialog reminiscence.
Matters we’ll cowl embrace:
- How state, nodes, and edges mix to outline the execution stream of a LangGraph agent.
- Find out how to register a software and route the mannequin’s software calls by way of the graph’s reasoning loop.
- How a checkpointer persists dialog historical past throughout separate graph invocations.
Let’s not waste any extra time.

Introduction
Most AI agent setups deal with the single-turn case nicely: take a query, name a mannequin, and return a solution. The more durable issues seem quickly after that. An agent might have to question your database, bear in mind the context from earlier messages, or provide you with visibility into precisely what the mannequin determined and why. Fixing these challenges with out constructing customized plumbing for each use case is the place many implementations start to interrupt down.
LangGraph offers a clear construction for dealing with every of those issues. An agent is represented as a graph, the place nodes are models of labor, edges outline what runs subsequent, and a shared state object carries the whole message historical past by way of each step. The mannequin runs inside a node, so each reasoning step, software name, and response turns into a part of the graph’s state. That makes your complete execution stream seen, inspectable, and out there to any node that runs afterward.
On this article, you’ll discover ways to perceive the state, node, and edge primitives that each LangGraph graph is constructed on; handle dialog historical past mechanically with MessagesState; name a language mannequin inside a node and join it to a graph; register a software and route software calls again by way of the mannequin; hint the whole message sequence to see precisely what the mannequin does at every step; and persist conversations throughout separate invocations with a checkpointer. We’ll construct the graph from the bottom up, beginning with the set up steps.
Setting Up
Set up the required packages:
|
pip set up langgraph langchain–openai python–dotenv |
Then create a .env file in your mission root together with your OpenAI API key:
|
OPENAI_API_KEY=“your_key_here” |
Load it on the prime of your script earlier than any LangChain or LangGraph imports:
|
from dotenv import load_dotenv load_dotenv() |
python-dotenv reads the .env file and units the important thing as an atmosphere variable.
Understanding State, Nodes, and Edges
Each LangGraph graph is constructed from the next three elements. Getting them proper upfront saves confusion when the graph will get extra advanced.
State is a TypedDict that acts because the shared reminiscence for your complete graph. Each node reads from it and writes updates again to it. Nothing passes between nodes another method. Fields you don’t replace in a node keep unchanged; you solely return what you need to modify.
Nodes are plain Python features. A node takes the present state as its argument and returns a dictionary of the fields it desires to replace. Registering a perform with add_node is what makes it a part of the graph with out the necessity for a particular decorator or base class. For those who cross simply the perform with out a identify string, LangGraph makes use of the perform identify mechanically.
Edges outline execution order. add_edge(A, B) means: after node A finishes, run node B. add_conditional_edges means: after node A finishes, name a routing perform and go wherever it factors. Each graph wants START as its entry level and no less than one path to END.
By default, when a node returns a worth for a state subject, that worth replaces what was there. For fields that ought to accumulate throughout nodes — a log, a message historical past — you annotate the sphere with a reducer perform. Within the following instance, operator.add on a listing subject means append, not substitute:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from typing import Annotated import operator from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END
class TicketState(TypedDict): customer_message: str log: Annotated[list, operator.add]
def log_received(state: TicketState) -> dict: return {“log”: [f“Received: {state[‘customer_message’]}”]}
def log_assigned(state: TicketState) -> dict: return {“log”: [“Assigned to support queue”]}
builder = StateGraph(TicketState) builder.add_node(“log_received”, log_received) builder.add_node(“log_assigned”, log_assigned) builder.add_edge(START, “log_received”) builder.add_edge(“log_received”, “log_assigned”) builder.add_edge(“log_assigned”, END) graph = builder.compile()
outcome = graph.invoke({“customer_message”: “My bill seems to be incorrect”, “log”: []}) print(outcome) |
This outputs:
|
{‘customer_message’: ‘My bill seems to be incorrect’, ‘log’: [‘Received: My invoice looks wrong’, ‘Assigned to support queue’]} |
Each nodes wrote to log, and each entries are there. customer_message got here by way of untouched as a result of neither node returned it. That is precisely how MessagesState handles its messages subject, utilizing a barely extra specialised reducer referred to as add_messages that additionally handles deduplication and ordering of message objects.
Managing Dialog Historical past with MessagesState
Each node in a LangGraph graph reads the present state and writes updates again to it. For a conversational agent, state wants to hold the complete message historical past — person inputs, mannequin responses, software outputs — so the mannequin at all times has the context it wants when deciding what to do subsequent.
LangGraph ships a built-in state sort for precisely this: MessagesState. It’s a TypedDict with a single messages subject that makes use of the add_messages reducer as an alternative of plain overwriting. Each time a node returns new messages, they get appended to the prevailing listing slightly than changing it. You don’t need to sew collectively dialog historical past manually.
|
from langgraph.graph import MessagesState |
That is the state definition you’d want for many single-agent graphs. You’ll be able to lengthen it with further fields, say a customer_id, a precedence flag, something your nodes want. However messages is already there and already wired to build up.

Calling the Mannequin Inside a Node
With the state in place, the core node of any LangGraph agent is a perform that passes the present message listing to a mannequin and appends its response. The mannequin returns an AIMessage; returning it inside a dict keyed to “messages” is all it takes so as to add it to state.
|
from langchain_openai import ChatOpenAI from langchain_core.messages import SystemMessage
llm = ChatOpenAI(mannequin=“gpt-4o-mini”)
def run_model(state: MessagesState) -> dict: system = SystemMessage(“You’re a help agent for a SaaS product. “ “Be concise and useful.”) response = llm.invoke([system] + state[“messages”]) return {“messages”: [response]} |
ChatOpenAI wraps the OpenAI API with LangChain’s customary chat mannequin interface. Swapping to a unique supplier — Anthropic, Google, an area mannequin by way of Ollama — means altering the import and the mannequin string; the remainder of the node stays the identical. The SystemMessage units the mannequin’s function on each name with out being saved in state, protecting the persistent historical past clear.
Wire it right into a graph and run it:
|
from langgraph.graph import StateGraph, START, END from langchain_core.messages import HumanMessage
builder = StateGraph(MessagesState) builder.add_node(“run_model”, run_model) builder.add_edge(START, “run_model”) builder.add_edge(“run_model”, END)
graph = builder.compile()
outcome = graph.invoke({“messages”: [HumanMessage(“My dashboard isn’t loading. What should I try?”)]}) print(outcome[“messages”][–1].content material) |
outcome["messages"] is the complete listing: the unique HumanMessage plus the AIMessage the mannequin produced. [-1] will get the newest one.
Registering a Instrument and Routing Instrument Calls
The mannequin can reply basic questions from its coaching information, however something particular to your information — account particulars, subscription tier, ticket historical past — requires a software name. The mannequin decides when a software is required; your code defines what it does.
Outline a software with the @software decorator:
|
from langchain_core.instruments import software
@software def get_customer_tier(customer_id: str) -> str: “”“Lookup the subscription tier for a buyer by their ID. Returns ‘free’, ‘professional’, or ‘enterprise’.”“” tiers = { “cust_1001”: “enterprise”, “cust_2002”: “professional”, “cust_3003”: “free”, } return tiers.get(customer_id, “not discovered”) |
The docstring is what the mannequin reads when deciding whether or not to name this software and what arguments to cross. Preserve it exact as a result of imprecise docstrings result in missed calls or malformed arguments.
Bind the software to the mannequin so it is aware of the software exists, and replace the node:
|
instruments = [get_customer_tier] llm_with_tools = llm.bind_tools(instruments)
def run_model(state: MessagesState) -> dict: system = SystemMessage(“You’re a help agent for a SaaS product. “ “Use out there instruments while you want account-specific data.”) response = llm_with_tools.invoke([system] + state[“messages”]) return {“messages”: [response]} |
bind_tools sends the software’s schema to the mannequin alongside each request. When the mannequin decides to make use of it, the response comes again as an AIMessage with a tool_calls subject populated slightly than plain textual content in content material.

Add a ToolNode to deal with execution and wire the routing:
|
from langgraph.prebuilt import ToolNode, tools_condition
tool_node = ToolNode(instruments)
builder = StateGraph(MessagesState) builder.add_node(“run_model”, run_model) builder.add_node(“instruments”, tool_node)
builder.add_edge(START, “run_model”) builder.add_conditional_edges(“run_model”, tools_condition) builder.add_edge(“instruments”, “run_model”)
graph = builder.compile() |
ToolNode reads the tool_calls from the final AIMessage, runs the matching perform with the arguments the mannequin specified, and wraps the lead to a ToolMessage appended to state. tools_condition checks the final AIMessage after each mannequin name. If tool_calls is non-empty it routes to “instruments“, in any other case it routes to “__end__“. The sting from “instruments” again to “run_model” is what closes the loop: it sends the software outcome again to the mannequin so it may produce a closing reply.
Tracing the Reasoning Loop
Earlier than shifting on, contemplate what truly occurs contained in the graph when the mannequin makes use of a software, as a result of there’s extra occurring than the ultimate output suggests.
|
outcome = graph.invoke({“messages”: [ HumanMessage(“Can you check what plan customer cust_1001 is on?”) ]})
for msg in outcome[“messages”]: print(sort(msg).__name__, “:”, msg.content material or msg.tool_calls) |
Pattern output:
|
HumanMessage : Can you test what plan buyer cust_1001 is on? AIMessage : [{‘name’: ‘get_customer_tier’, ‘args’: {‘customer_id’: ‘cust_1001’}, ‘id’: ‘call_Rx7kLmNpQ2wJtA3s’, ‘type’: ‘tool_call’}] ToolMessage : enterprise AIMessage : Buyer cust_1001 is on the enterprise plan. |
Right here, we now have 4 messages and two mannequin calls. The primary mannequin name produces an AIMessage with tool_calls populated and content material empty. The mannequin is signaling what it desires to do, not answering but. tools_condition sees that, routes to ToolNode, which runs get_customer_tier("cust_1001") and appends a ToolMessage with the outcome.
The sting again to run_model fires once more. Now the mannequin has all three prior messages in context, understands the lookup succeeded, and writes the ultimate AIMessage with the reply in content material. tools_condition runs yet another time, finds no software calls, and ends the graph.
This loop — mannequin name, software execution, mannequin name once more — is the usual ReAct sample. Each software use prices two mannequin calls: one to determine what to lookup, one to interpret the outcome. That’s a helpful factor to know when desirous about latency and price as you add extra instruments.
Persisting Conversations Throughout Calls
Each graph.invoke() above begins with a recent graph state. With out persistence, the mannequin doesn’t bear in mind earlier exchanges.
To persist state between calls, connect a checkpointer when compiling the graph:
|
from langgraph.checkpoint.reminiscence import InMemorySaver
checkpointer = InMemorySaver() graph = builder.compile(checkpointer=checkpointer) |
Then cross the identical thread_id on each invocation:
|
config = {“configurable”: {“thread_id”: “ticket-7741”}}
graph.invoke( {“messages”: [HumanMessage(“Hi, I can’t access my account.”)]}, config, )
outcome = graph.invoke( {“messages”: [HumanMessage(“My ID is cust_2002, can you check my plan?”)]}, config, )
print(outcome[“messages”][–1].content material) |
Pattern output:
|
You‘re on the professional plan, cust_2002. Because you’re having bother accessing your account, I‘d advocate resetting your password first. Professional accounts additionally have precedence help out there if the subject continues. |
The second invocation sees the dialog from the primary as a result of the checkpointer restored the thread’s state earlier than execution and saved the up to date state afterward. Utilizing a unique thread_id begins with a separate, empty state.
InMemorySaver shops checkpoints in course of reminiscence, making it helpful for growth and testing. In manufacturing, you usually substitute it with a persistent checkpointer backed by a database or different sturdy storage. The remainder of your graph code stays the identical.

Checkpointers persist graph state for a thread. In case your software additionally must persist information independently of any dialog, resembling person profiles, preferences, or long-term recollections shared throughout a number of threads, use a Retailer. Shops complement checkpointers by offering sturdy application-level storage that graphs can entry throughout execution.
Wrapping Up
On this article, you constructed an entire LangGraph agent from the bottom up. Alongside the way in which, you realized how state flows by way of a graph, how nodes execute work, how instruments match into the execution loop, and the way a checkpointer preserves conversations throughout separate invocations. Those self same constructing blocks scale from easy chatbots to way more subtle agent workflows.
Considered one of LangGraph’s strengths is that every piece is impartial. You’ll be able to swap language fashions, register new instruments, or change how conversations are persevered with out redesigning the remainder of the graph. Every little thing communicates by way of shared state, which retains the graph predictable and straightforward to increase.
The identical concepts additionally carry over to multi-agent programs. A coordinator that routes requests to specialist brokers remains to be a graph with state, nodes, and conditional edges. The structure turns into bigger, however the underlying primitives keep the identical.
For those who’d prefer to discover additional, the next assets are place to proceed:
Comfortable constructing!

