Home / Articles

A Standard Seven-Section Layout for Every LangGraph Agent

A fixed file structure for LangGraph agents, from imports and state to tools, nodes, edges, assembly and invocation, with templates and a multi-agent extension.

Publisher: REACTAPP.TOP19 min read4469 words

This article is published in English.

Sources: The LangGraph Mental Model: A standardized architecture guide for every agent you’ll ever build

Anyone who has studied LangGraph for a while knows the odd feeling: the idea makes sense, and the code still won't come. A graph holds nodes, nodes do work, edges decide where to go next, and state carries memory. You could sketch it on a whiteboard in five minutes. Then you open an empty file and nothing arrives. The imports have slipped your mind, the class you want has no obvious home, and there seem to be five ways of writing the same thing.

Why the code feels harder than the concept

The underlying reason is that LangGraph imposes no structure of its own. It hands you capable building blocks and leaves the arrangement to you. Every tutorial and every repository lays things out differently, so your memory never settles on a single scaffold.

The fix proposed here is to pick one standard layout and use it for every agent, whether it's a tiny chatbot or a pipeline with many cooperating agents. Think of it as a template you can memorize and recall without effort, a bit like the diagrams that show what an agent has already done, except aimed at source code instead of runtime history.

The whole file at a glance

Each LangGraph application can be written as a single file with seven sections in a fixed order, from top to bottom:

langgraph_agent.py
│
├── MODULE 1: IMPORTS & CONFIGURATION
│   └── All your libraries, API keys, model setup
│
├── MODULE 2: STATE
│   └── The TypedDict that defines your agent's memory
│
├── MODULE 3: TOOLS (optional, but common)
│   └── Functions decorated with @tool that the LLM can call
│
├── MODULE 4: NODES
│   └── Functions that do the actual work at each graph step
│
├── MODULE 5: EDGES & ROUTING
│   └── Functions that decide what happens next
│
├── MODULE 6: GRAPH ASSEMBLY
│   └── Where you build, wire, and compile the graph
│
└── MODULE 7: ENTRYPOINT
    └── The __main__ block or invoke() call that runs everything

That's the skeleton. When you begin a project, open a file and type the seven section titles as comments, then fill them in one at a time. The rest of this piece goes through each section, covering what it's for, the names you need to recognize, and a template to copy.

Section 1: imports and configuration

What it's for

Consider this section the toolbox. A carpenter lays out every tool before building anything, and here you're telling Python which outside capabilities you need and setting up the model the agent will use.

Names to know

  • ChatOpenAI or ChatAnthropic is the language model class, the agent's brain. Create it once here and reuse it everywhere.
  • StateGraph is the core class that builds the graph, like the blueprint paper you draw on.
  • END is a special constant for the terminal point. An edge that points to it stops the run.
  • MemorySaver is a checkpointer that keeps memory across several conversation turns.

Template

# --- Standard Library ---
import os
from typing import TypedDict, Annotated, Literal
# --- LangChain Core ---
from langchain_openai import ChatOpenAI          # or ChatAnthropic, ChatGroq, etc.
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
# --- LangGraph Core ---
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages  # The message reducer
from langgraph.prebuilt import ToolNode            # Pre-built node for tool execution
from langgraph.checkpoint.memory import MemorySaver
# --- Configuration ---
# Always name your model variable 'llm' - it's the standard in every node
llm = ChatOpenAI(
    model="gpt-4o",         # or "claude-3-5-sonnet-20241022", etc.
    temperature=0,          # 0 = deterministic; raise for creativity
    api_key=os.environ.get("OPENAI_API_KEY")
)

Why it's laid out this way

The imports are divided into four groups: the standard library, LangChain (the model layer), LangGraph (the graph layer) and configuration. That follows the layering of the libraries themselves, where LangChain supplies AI primitives and LangGraph handles orchestration. Keeping the groups apart makes it quick to see what belongs where when something breaks.

The most useful habit here is to call the model llm. Every node function will refer to that variable later, and if one project says model while another says chat, the code stops looking uniform and becomes harder to skim.

Section 2: state

What it's for

State is the agent's working memory, a single source of truth that travels through the entire graph. Each node can read it and write to it, while the engine manages its lifecycle. Of everything in LangGraph, getting State right is probably the most important.

An office whiteboard is a good picture. Whoever enters the room, which is to say each node, can read what's up there and add something, and the board stays put for the whole meeting, which is the whole graph run.

Names to know

  • TypedDict is a Python class that describes the shape of the state, rather like a schema. Your State class inherits from it.
  • Annotated is a type-hint wrapper. LangGraph uses it to attach a reducer to a field, meaning a function that decides how a new value merges with what's already there.
  • add_messages is the built-in reducer for the messages field. Instead of replacing the list each time a node adds a message, it appends. It's the reducer you'll use most.
  • operator.add is another common reducer, for fields that should accumulate by addition, such as a list of results or a counter.

Template

# ============================================================
# MODULE 2: STATE
# ============================================================
class AgentState(TypedDict):
    # 'messages' is the heartbeat of almost every LangGraph agent.
    # Annotated[list, add_messages] means: "this is a list, and when
    # a node writes to it, append - don't replace."
    messages: Annotated[list[BaseMessage], add_messages]

    # Add custom fields below for your specific agent's needs.
    # Fields without a reducer are REPLACED each time a node writes to them.

    # Example: a simple string field (gets replaced each write)
    current_task: str

    # Example: a list you want to accumulate (use operator.add as reducer)
    # results: Annotated[list[str], operator.add]

    # Example: a counter
    # iteration_count: int

Thinking about reducers

Reducers confuse many beginners, so here's an analogy. Imagine the state as a paper form passed around an office. The messages field is a pad of sticky notes: everyone who handles the form adds a note, and nobody removes the earlier ones. That's add_messages.

A field with no reducer is a single text box. Each person overwrites whatever the last one wrote, which is LangGraph's default: the last write wins.

A simple rule follows. Give the messages field Annotated[list, add_messages] every time, pick Annotated[list, operator.add] for each additional list that should keep growing, and leave all remaining fields bare.

Section 3: tools

What it's for

Tools are the agent's hands. The LLM is the brain, which reasons and decides, yet alone it can't act in the world. With tools it can browse the web, hit an API, execute code, look things up in a database, or run whatever Python function you define.

This section is optional. An agent that only produces text and never touches external systems can skip it, though most real agents need tools.

Names to know

  • @tool is a LangChain decorator that turns an ordinary Python function into a tool LangGraph understands. The function's docstring becomes the description the LLM reads when deciding whether to use it.
  • ToolNode is a ready-made node, found in langgraph.prebuilt, that runs whichever tools the LLM chose to call, so you don't write the execution logic yourself.
  • bind_tools is a method on your LLM instance that attaches tools to it. Afterwards the model knows what exists and can request a call by emitting a special tool_calls message instead of plain text.

Template

# ============================================================
# MODULE 3: TOOLS
# ============================================================
@tool
def search_web(query: str) -> str:
    """Search the web for current information about a topic.

    Use this when you need real-time information that is not in
    your training data, such as recent news or live prices.

    Args:
        query: The search query string.

    Returns:
        A string containing search results.
    """
    # Your actual implementation here (e.g., Tavily, SerpAPI, etc.)
    # Placeholder for illustration:
    return f"Search results for: {query}"

@tool
def calculate(expression: str) -> str:
    """Evaluate a mathematical expression and return the result.

    Use this for any arithmetic, algebra, or numerical computation.

    Args:
        expression: A valid Python math expression as a string, e.g. '2 + 2 * 10'

    Returns:
        The computed result as a string.
    """
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

# Collect all tools into a list - this is the pattern you always follow
tools = [search_web, calculate]
# Bind tools to the LLM so it knows they exist and can choose to call them
llm_with_tools = llm.bind_tools(tools)
# Create the pre-built ToolNode that will execute tool calls automatically
tool_node = ToolNode(tools)

The docstring matters more than it looks

Beginners often miss this: the model relies on your docstring to choose a tool. It reads the text inside the triple quotes. If the docstring is vague or missing, the LLM won't know when your tool is the right one, so write clear, specific descriptions that say what the tool does and when to use it.

Section 4: nodes

What it's for

Nodes are the workers. Each is a Python function that receives the current State, does something such as calling an LLM, processing data or invoking a tool, and returns a dictionary of updates to the State. That is the complete job description.

A node never decides what happens afterward. It does its work and updates the state, and the routing that follows is handled in section 5.

Names to know

  • state is the parameter every node receives, a dictionary shaped like your AgentState. Inside you read values such as state["messages"] or state["current_task"].
  • HumanMessage, AIMessage and SystemMessage are the three message types you'll use constantly. The first is what the user said, the second is the model's reply, and the third carries your instructions to the model, the system prompt.
  • invoke is how you call the LLM, or llm_with_tools, to get an answer. You pass a list of messages and receive an AIMessage.
  • The return value is always a dictionary whose keys match field names in AgentState. Its values are updates, not the full state, and LangGraph merges them using the reducers you defined.

Template

# ============================================================
# MODULE 4: NODES
# ============================================================
# ── Node: Agent (the reasoning brain) ───────────────────────
def agent_node(state: AgentState) -> dict:
    """The central reasoning node. Calls the LLM and decides
    whether to respond or call a tool."""

    # Build the message list to send to the LLM.
    # Always include a system message to set behavior.
    system_prompt = SystemMessage(content=(
        "You are a helpful assistant. Use the available tools "
        "when you need real-time information or computation. "
        "Respond clearly and concisely."
    ))

    # The LLM receives the system prompt + all previous messages in state
    messages_to_send = [system_prompt] + state["messages"]

    # Call the LLM. Use llm_with_tools if you have tools; plain llm if not.
    response = llm_with_tools.invoke(messages_to_send)

    # Return the LLM's response as a state update.
    # add_messages will APPEND this AIMessage to state["messages"].
    return {"messages": [response]}

# ── Node: Summarizer (example of a non-LLM processing node) ─
def summarize_node(state: AgentState) -> dict:
    """Summarizes the conversation so far to keep context short.
    This shows that nodes don't have to call an LLM - they can
    do any Python processing."""

    all_messages = state["messages"]

    # Summarize with the LLM (a different prompt, same LLM)
    summary_prompt = [
        SystemMessage(content="Summarize the following conversation in 2-3 sentences."),
        HumanMessage(content=str(all_messages))
    ]

    summary_response = llm.invoke(summary_prompt)

    # Replace messages with a fresh start containing just the summary
    return {
        "messages": [AIMessage(content=f"[Summary] {summary_response.content}")]
    }

A picture for nodes

Picture a node as a relay runner. It takes the baton, which is the state, runs its stretch, meaning it does the work, and passes the baton on with updated contents. It doesn't pick the next runner. That's the job of whoever directs the race, the edges.

Every node signature looks the same: def my_node(state: AgentState) -> dict. The input is the whole state, and the output is a dictionary containing only the fields you want to change.

Section 5: edges and routing

What it's for

Edges are the graph's decision logic. They link nodes together and determine how execution flows. LangGraph has two kinds.

Static edges join one node to another with no condition: "after the summarizer, go to the agent." No decision, just a direct wire.

Conditional edges look at the state and return a string naming the next node. This is where the agent's intelligence about its own flow lives. Should it call a tool, finish, or loop back for another try?

Names to know

  • add_edge(A, B) creates a static edge, meaning control always goes from A to B.
  • add_conditional_edges(source, routing_function, mapping) creates a branch leaving source. The routing function receives the current state and returns a string, and the mapping dictionary translates that string into the real name of the next node.
  • tools_condition is a ready-made routing function from langgraph.prebuilt. It inspects the last message in the state for tool calls. If there are some, it routes to "tools", and if not, to END. This is the standard pattern for tool-using agents and worth memorizing.
  • START is the constant for the beginning of the graph. Setting an entry point means adding an edge from START to the first node.

Template

# ============================================================
# MODULE 5: EDGES & ROUTING
# ============================================================
# Import the pre-built tool routing function
from langgraph.prebuilt import tools_condition
# ── Custom Routing Function Example ─────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "summarize", "__end__"]:
    """Custom router for the agent node.

    Routing functions always:
    1. Receive the current state as input
    2. Return a string that maps to the next node (or END)

    The return values must match the keys in add_conditional_edges' mapping.
    """

    last_message = state["messages"][-1]  # Look at what the LLM just said

    # Case 1: The LLM decided to call a tool
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"

    # Case 2: The conversation is getting long - summarize before continuing
    if len(state["messages"]) > 20:
        return "summarize"

    # Case 3: The LLM gave a direct answer - we're done
    return "__end__"  # LangGraph's internal name for END

A picture for routing

Think of a routing function as a railway signal operator. A train arrives at a station when a node finishes. The operator checks the manifest, which is the current state, and throws the right switch so the train takes the correct track. The operator never moves the train; LangGraph does that.

One rule to keep in mind: routing functions never modify state. They only read it. Any change belongs in a node, so if you catch yourself wanting to update state while routing, move that logic into a node.

Section 6: graph assembly

What it's for

Here everything comes together. Treat this section as building day. Sections 1 through 5 were preparation, with materials defined and pieces cut and laid out, and now you put the structure together by wiring the pieces and compiling.

Assembly always follows the same five steps, which are worth committing to memory: initialize, register nodes, set the entry, wire edges, compile.

Names to know

  • StateGraph(AgentState) creates the builder using your State class as the schema, so every node must return updates compatible with it.
  • add_node(name, function) registers a node. The name is the string you'll use inside edges, and the function is the node from section 4.
  • set_entry_point(name) says which node runs first when the graph is invoked.
  • add_edge(A, B) wires a direct, unconditional link from A to B.
  • add_conditional_edges(source, router, mapping) adds a conditional branch from the source, using your routing function and a mapping from returned strings to destination names.
  • compile(checkpointer=...) seals the graph into a runnable object (a CompiledGraph). Nodes and edges can't be added afterward. The optional checkpointer turns on persistent memory.

Template

# ============================================================
# MODULE 6: GRAPH ASSEMBLY
# ============================================================
# ── Step 1: Initialize ──────────────────────────────────────
# Always pass your State class to StateGraph
graph_builder = StateGraph(AgentState)
# ── Step 2: Register All Nodes ──────────────────────────────
# Format: add_node("node_name_as_string", node_function)
# The string name is what you use in ALL edge definitions
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("tools", tool_node)      # The pre-built ToolNode from Module 3
graph_builder.add_node("summarize", summarize_node)
# ── Step 3: Set Entry Point ─────────────────────────────────
# Which node runs first when we invoke the graph?
graph_builder.set_entry_point("agent")
# ── Step 4: Wire the Edges ──────────────────────────────────
# Conditional edge from agent: check if we need tools, a summary, or we're done
graph_builder.add_conditional_edges(
    "agent",          # Source node
    should_continue,  # Routing function from Module 5
    {
        "tools": "tools",           # If router returns "tools" → go to tools node
        "summarize": "summarize",   # If router returns "summarize" → go to summarize node
        "__end__": END,             # If router returns "__end__" → stop the graph
    }
)
# Static edge: after tools run, always go back to agent (the ReAct loop)
graph_builder.add_edge("tools", "agent")
# Static edge: after summarization, always return to agent
graph_builder.add_edge("summarize", "agent")
# ── Step 5: Compile ─────────────────────────────────────────
# Without checkpointer: no persistent memory (stateless per invocation)
# With checkpointer: memory persists across turns (stateful conversations)
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)

A picture for assembly

A circuit board is the best comparison. Every add_node call places a component. add_edge and add_conditional_edges solder the wires between components. compile() runs the final inspection and seals the board. After that it's ready for power, which is invoke, and for doing its job.

The template contains the fundamental ReAct loop: agent → tools → agent → tools and so on until the agent decides it's finished and routes to END. This reason-then-act cycle underlies almost every tool-using LangGraph agent, and recognizing it explains most of the graphs you'll meet.

Section 7: entrypoint and invocation

What it's for

This is the launch pad. Everything earlier was definition, and this section is execution: the code that runs the graph, supplies input and collects output.

There are two ways to run it, invoke for a single pass and streaming for progressive output. You also need config, the dictionary carrying runtime parameters, above all the thread_id used for memory.

Names to know

  • graph.invoke(input, config) executes the full graph and gives back the end state. Because it is synchronous, you receive nothing until every step has finished.
  • graph.stream(input, config) also runs the graph, but hands back partial state updates as they occur, a good fit for user-facing apps that need to show live progress.
  • config is the dictionary passed to either call. Its key entry is {"configurable": {"thread_id": "some_id"}}. The thread ID works like a session ID, which tells the checkpointer what memory to restore and store. Two users with different thread IDs never share memory at all.
  • HumanMessage(content=...) is how you wrap user input before handing it to the graph.

Template

# ============================================================
# MODULE 7: ENTRYPOINT & INVOCATION
# ============================================================
if __name__ == "__main__":

    # ── Config: defines this conversation's memory session ──
    # Change thread_id to start a fresh conversation.
    # Keep the same thread_id to continue an existing one.
    config = {"configurable": {"thread_id": "user-session-001"}}

    # ── Single Invocation (synchronous) ─────────────────────
    user_input = "What is the current price of Bitcoin?"

    result = graph.invoke(
        input={"messages": [HumanMessage(content=user_input)]},
        config=config
    )

    # The result is the final state dictionary.
    # Access the last message to get the agent's final answer.
    final_answer = result["messages"][-1].content
    print(f"Agent: {final_answer}")

    # ── Streaming Invocation (for real-time output) ──────────
    for chunk in graph.stream(
        input={"messages": [HumanMessage(content=user_input)]},
        config=config,
        stream_mode="values"  # Yields the full state after each node runs
    ):
        # Each chunk is a state snapshot. The last message shows progress.
        latest = chunk["messages"][-1]
        if hasattr(latest, "content") and latest.content:
            print(f"[Streaming] {latest.content}")

    # ── Multi-turn Conversation Loop ─────────────────────────
    print("\n--- Starting Interactive Session ---")
    while True:
        user_text = input("You: ").strip()
        if user_text.lower() in ("exit", "quit", "bye"):
            break

        response = graph.invoke(
            input={"messages": [HumanMessage(content=user_text)]},
            config=config  # Same config = same memory thread
        )

        print(f"Agent: {response['messages'][-1].content}\n")

The complete template

Below is the full, minimal skeleton to copy into a new project. It's the single file worth bookmarking: swap in your own logic for the placeholder implementations and you have the base of any LangGraph agent.

# ============================================================
# LANGGRAPH CANONICAL AGENT TEMPLATE
# Modules: Imports → State → Tools → Nodes → Edges → Assembly → Entrypoint
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import TypedDict, Annotated, Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
llm = ChatOpenAI(model="gpt-4o", temperature=0)

# ── MODULE 2: STATE ─────────────────────────────────────────
class AgentState(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    # Add your custom fields here

# ── MODULE 3: TOOLS ─────────────────────────────────────────
@tool
def my_tool(input: str) -> str:
    """Describe clearly what this tool does and when the LLM should use it."""
    return f"Result for: {input}"
tools = [my_tool]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)

# ── MODULE 4: NODES ─────────────────────────────────────────
def agent_node(state: AgentState) -> dict:
    """The reasoning node. Calls the LLM, optionally triggers tool calls."""
    messages = [SystemMessage(content="You are a helpful assistant.")] + state["messages"]
    response = llm_with_tools.invoke(messages)
    return {"messages": [response]}

# ── MODULE 5: EDGES & ROUTING ───────────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
    """Decide: did the LLM call a tool, or did it give a final answer?"""
    last_message = state["messages"][-1]
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "tools"
    return "__end__"

# ── MODULE 6: GRAPH ASSEMBLY ────────────────────────────────
graph_builder = StateGraph(AgentState)
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("tools", tool_node)
graph_builder.set_entry_point("agent")
graph_builder.add_conditional_edges(
    "agent",
    should_continue,
    {"tools": "tools", "__end__": END}
)
graph_builder.add_edge("tools", "agent")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)

# ── MODULE 7: ENTRYPOINT ────────────────────────────────────
if __name__ == "__main__":
    config = {"configurable": {"thread_id": "session-001"}}

    while True:
        user_text = input("You: ").strip()
        if not user_text or user_text.lower() in ("exit", "quit"):
            break
        response = graph.invoke(
            {"messages": [HumanMessage(content=user_text)]},
            config=config
        )
        print(f"Agent: {response['messages'][-1].content}\n")

Going further: multi-agent systems

After the single-agent layout feels natural, multi-agent systems stop being scary. The central idea is that each agent is simply a compiled graph, and a compiled graph can itself be a node inside a larger supervisor graph.

Names to know for multi-agent work

  • A supervisor node is an agent whose task isn't to answer the user but to choose which specialist should handle the job. It routes between sub-agents.
  • A subgraph is a compiled graph registered as a node in another graph. It takes and returns state like any ordinary node function.
  • Command is a newer pattern, available from LangGraph 0.2, where one node returns both a state update and a routing instruction in a single value. In some multi-agent designs it removes the need for separate conditional edges.

Structural template

# ── MULTI-AGENT PATTERN ─────────────────────────────────────
# Each specialist is a compiled graph (a subgraph)

# Sub-agent 1: A researcher
researcher_graph = StateGraph(AgentState)
# ... (built with its own nodes, edges, and tools)
researcher = researcher_graph.compile()
# Sub-agent 2: A writer
writer_graph = StateGraph(AgentState)
# ... (built with its own nodes, edges, and tools)
writer = writer_graph.compile()

# ── SUPERVISOR NODE ─────────────────────────────────────────
def supervisor_node(state: AgentState) -> dict:
    """Decides which sub-agent should handle the current task."""
    # The supervisor LLM decides: "researcher" or "writer" or "FINISH"
    response = supervisor_llm.invoke(state["messages"])
    return {"messages": [response], "next_agent": response.content}

def route_to_agent(state: AgentState) -> Literal["researcher", "writer", "__end__"]:
    """Routes to the appropriate sub-agent based on supervisor's decision."""
    return state.get("next_agent", "__end__")

# ── SUPERVISOR GRAPH ────────────────────────────────────────
supervisor_builder = StateGraph(AgentState)
supervisor_builder.add_node("supervisor", supervisor_node)
supervisor_builder.add_node("researcher", researcher)  # Subgraph as a node!
supervisor_builder.add_node("writer", writer)          # Subgraph as a node!
supervisor_builder.set_entry_point("supervisor")
supervisor_builder.add_conditional_edges(
    "supervisor",
    route_to_agent,
    {"researcher": "researcher", "writer": "writer", "__end__": END}
)
supervisor_builder.add_edge("researcher", "supervisor")
supervisor_builder.add_edge("writer", "supervisor")
supervisor_graph = supervisor_builder.compile(checkpointer=MemorySaver())

The multi-agent version has the same structure as the single-agent one, and the same seven sections apply. The sole difference is that some of the "nodes" are whole compiled graphs. The mental model scales without strain.

Quick reference

Here's a condensed card you can print or pin, listing the main ideas in one spot.

  • State: TypedDict defines the shape. Annotated[T, reducer] attaches a reducer to a field. add_messages is the standard message-list reducer and appends rather than replaces. operator.add accumulates lists or numbers.
  • LLM: ChatOpenAI or ChatAnthropic is the class, always bound to the name llm. bind_tools(tools) informs the model of available tools. invoke(messages) calls it and returns a response.
  • Messages: HumanMessage carries user input, AIMessage carries model output, and SystemMessage carries your instructions. They're always passed to invoke as a list.
  • Tools: @tool makes a Python function callable by the model, with the docstring as its description. ToolNode(tools) runs tool calls automatically. tools_condition checks whether the model asked for a tool.
  • Graph: StateGraph(State) makes the builder, add_node(name, fn) registers a node, set_entry_point(name) picks the first one, add_edge(A, B) wires an unconditional link, add_conditional_edges(source, router, mapping) wires a link chosen by the router's return value, compile(checkpointer=...) finalizes, and END marks the terminal point.
  • Invocation: graph.invoke(input, config) returns the final state, graph.stream(input, config) yields updates progressively, config = {"configurable": {"thread_id": "..."}} identifies the memory session, and MemorySaver() is an in-memory checkpointer.

Making it muscle memory

The point isn't to teach LangGraph from nothing. It's to give your brain a steady scaffold you can pick up automatically, the way a musician reaches for a familiar chord progression or a cook sets out ingredients before starting.

Seven sections, imports, state, tools, nodes, edges, assembly and entrypoint, form the progression. The names inside each section are the notes, and the full template is the score.

A basic chatbot and a pipeline with dozens of nodes both fit the same layout. Complexity grows but structure doesn't, and that stability is what makes it a standard.

A sensible way in is to start small with the minimal ReAct loop: one agent node, one tool node and one conditional edge using tools_condition. Make that work first, then bolt on another node, and later another agent. The layout survives at each size.

For persistence backends, human-in-the-loop patterns and deployment options, the official LangGraph documentation is the next place to look.