On this article, you’ll discover ways to construct a whole 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 movement of a LangGraph agent.
- register a instrument and route the mannequin’s instrument calls by 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 effectively: take a query, name a mannequin, and return a solution. The more durable issues seem quickly after that. An agent may 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 supplies a clear construction for dealing with every of those issues. An agent is represented as a graph, the place nodes are items of labor, edges outline what runs subsequent, and a shared state object carries the entire message historical past by each step. The mannequin runs inside a node, so each reasoning step, instrument name, and response turns into a part of the graph’s state. That makes your entire execution movement seen, inspectable, and accessible 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 instrument and route instrument calls again by the mannequin; hint the entire 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 undertaking root along 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 entire graph. Each node reads from it and writes updates again to it. Nothing passes between nodes every other means. 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 needs to replace. Registering a operate with add_node is what makes it a part of the graph with out the necessity for a particular decorator or base class. When you go simply the operate with out a identify string, LangGraph makes use of the operate 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 operate 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 price for a state area, 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 operate. Within the following instance, operator.add on a listing area means append, not exchange:
|
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 mistaken”, “log”: []}) print(outcome) |
This outputs:
|
{‘customer_message’: ‘My bill seems mistaken’, ‘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 untouched as a result of neither node returned it. That is precisely how MessagesState handles its messages area, utilizing a barely extra specialised reducer known 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 total message historical past — person inputs, mannequin responses, instrument outputs — so the mannequin all the time has the context it wants when deciding what to do subsequent.
LangGraph ships a built-in state kind for precisely this: MessagesState. It’s a TypedDict with a single messages area that makes use of the add_messages reducer as a substitute of plain overwriting. Each time a node returns new messages, they get appended to the present record reasonably than changing it. You don’t must 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 possibly can 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 operate that passes the present message record 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 assist 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 normal chat mannequin interface. Swapping to a distinct 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, preserving 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 total record: 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 common questions from its coaching knowledge, however something particular to your knowledge — account particulars, subscription tier, ticket historical past — requires a instrument name. The mannequin decides when a instrument is required; your code defines what it does.
Outline a instrument with the @instrument decorator:
|
from langchain_core.instruments import instrument
@instrument 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 instrument and what arguments to go. Preserve it exact as a result of obscure docstrings result in missed calls or malformed arguments.
Bind the instrument to the mannequin so it is aware of the instrument 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 assist agent for a SaaS product. “ “Use accessible instruments if you want account-specific info.”) response = llm_with_tools.invoke([system] + state[“messages”]) return {“messages”: [response]} |
bind_tools sends the instrument’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 area populated reasonably 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 operate 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 instrument outcome again to the mannequin so it may well produce a last reply.
Tracing the Reasoning Loop
Earlier than shifting on, contemplate what truly occurs contained in the graph when the mannequin makes use of a instrument, as a result of there’s extra happening 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(kind(msg).__name__, “:”, msg.content material or msg.tool_calls) |
Pattern output:
|
HumanMessage : Can you examine 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 have now 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 needs 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 one more time, finds no instrument calls, and ends the graph.
This loop — mannequin name, instrument execution, mannequin name once more — is the usual ReAct sample. Each instrument use prices two mannequin calls: one to resolve what to search for, one to interpret the outcome. That’s a helpful factor to know when fascinated with latency and price as you add extra instruments.
Persisting Conversations Throughout Calls
Each graph.invoke() above begins with a contemporary 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 go 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 hassle accessing your account, I‘d suggest resetting your password first. Professional accounts additionally have precedence assist accessible if the situation 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 distinct 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 sometimes exchange 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 utility additionally must persist knowledge independently of any dialog, equivalent to 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 a whole LangGraph agent from the bottom up. Alongside the best way, you realized how state flows by 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.
One among LangGraph’s strengths is that every piece is impartial. You possibly can swap language fashions, register new instruments, or change how conversations are persevered with out redesigning the remainder of the graph. All the things communicates by shared state, which retains the graph predictable and simple to increase.
The identical concepts additionally carry over to multi-agent methods. A coordinator that routes requests to specialist brokers continues to be a graph with state, nodes, and conditional edges. The structure turns into bigger, however the underlying primitives keep the identical.
When you’d wish to discover additional, the next assets are a great place to proceed:
Completely satisfied constructing!









