Skip to content
SDB
Agentic AI

Chapter 04 · intermediate · 35 min

LangGraph — Stateful Agent Workflows

Model agents as state machines with fine-grained control over complex, cyclic workflows

Subhendu Datta BhowmikAI Tutorials

What Is LangGraph?

LangGraph is a framework from the LangChain team for building stateful, multi-actor applications with LLMs. Unlike simple chains (which are linear), LangGraph models agent workflows as directed graphs — nodes for processing steps, edges for transitions, and shared state that flows through the graph.

This makes LangGraph ideal for:

  • Agents with conditional logic ("if the code fails, retry; else deploy")
  • Cycles — loops where an agent checks its work and tries again
  • Human-in-the-loop — pause execution for human approval at key points
  • Multi-agent architectures where different agents pass work to each other

Key Concepts

ConceptDescription
StateGraphThe graph that defines the workflow
StateA typed dict that flows between all nodes
NodeA function that receives state and returns updates
EdgeA connection from one node to the next
Conditional EdgeRoutes to different nodes based on state
CheckpointA saved snapshot of state (enables resumption)
ENDSpecial node that terminates the graph
Basic LangGraph Agent — Defining State and Nodespython
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
import json

# -------------------------------------------------------
# 1. Define the shared state
# -------------------------------------------------------
class AgentState(TypedDict):
    # add_messages is a reducer: new messages are appended, not overwritten
    messages: Annotated[list, add_messages]
    # Other state fields can be plain values (last write wins)
    iteration: int
    final_answer: str | None

# -------------------------------------------------------
# 2. Define tools
# -------------------------------------------------------
@tool
def search_web(query: str) -> str:
    """Search the internet for current information on a topic."""
    # Replace with real search API
    return f"Top results for '{query}': [result 1], [result 2], [result 3]"

@tool
def run_python(code: str) -> str:
    """Execute Python code and return stdout + any errors."""
    import io, sys, contextlib
    output = io.StringIO()
    try:
        with contextlib.redirect_stdout(output):
            exec(code, {})
        return output.getvalue() or "Code ran successfully (no output)"
    except Exception as e:
        return f"Error: {type(e).__name__}: {e}"

tools = [search_web, run_python]
tools_by_name = {t.name: t for t in tools}

# -------------------------------------------------------
# 3. Initialize the LLM with tools bound
# -------------------------------------------------------
llm = ChatAnthropic(model="claude-sonnet-4-6", temperature=0)
llm_with_tools = llm.bind_tools(tools)

# -------------------------------------------------------
# 4. Define nodes
# -------------------------------------------------------
def call_model(state: AgentState) -> dict:
    """The reasoning node — calls the LLM."""
    response = llm_with_tools.invoke(state["messages"])
    return {
        "messages": [response],
        "iteration": state.get("iteration", 0) + 1,
    }

def execute_tools(state: AgentState) -> dict:
    """The action node — executes tool calls from the last AI message."""
    last_message = state["messages"][-1]
    tool_results = []

    for tool_call in last_message.tool_calls:
        tool = tools_by_name[tool_call["name"]]
        result = tool.invoke(tool_call["args"])
        tool_results.append(
            ToolMessage(
                content=str(result),
                tool_call_id=tool_call["id"],
                name=tool_call["name"],
            )
        )

    return {"messages": tool_results}

# -------------------------------------------------------
# 5. Define routing logic (conditional edges)
# -------------------------------------------------------
def should_continue(state: AgentState) -> str:
    """Decide what to do after the model responds."""
    last_message = state["messages"][-1]

    # If the model wants to use tools, go to tool execution
    if hasattr(last_message, "tool_calls") and last_message.tool_calls:
        return "execute_tools"

    # If we've iterated too many times, stop
    if state.get("iteration", 0) >= 10:
        return END

    # Otherwise, we're done
    return END
Building and Running the Graphpython
# -------------------------------------------------------
# 6. Build the graph
# -------------------------------------------------------
workflow = StateGraph(AgentState)

# Add nodes
workflow.add_node("call_model", call_model)
workflow.add_node("execute_tools", execute_tools)

# Set entry point
workflow.set_entry_point("call_model")

# Add conditional edge from call_model
workflow.add_conditional_edges(
    "call_model",
    should_continue,
    {
        "execute_tools": "execute_tools",
        END: END,
    }
)

# After tools execute, always go back to the model
workflow.add_edge("execute_tools", "call_model")

# Compile the graph
app = workflow.compile()

# -------------------------------------------------------
# 7. Run the agent
# -------------------------------------------------------
initial_state = {
    "messages": [HumanMessage(content="Search for the latest LangGraph release notes and summarize the key new features.")],
    "iteration": 0,
    "final_answer": None,
}

for event in app.stream(initial_state, stream_mode="values"):
    last_msg = event["messages"][-1]
    if hasattr(last_msg, "content") and isinstance(last_msg.content, str):
        print(f"[{type(last_msg).__name__}]: {last_msg.content[:200]}")

Human-in-the-Loop with Checkpoints

One of LangGraph's killer features is checkpointing — saving state so you can pause execution and wait for a human decision before continuing. This is essential for high-stakes agentic tasks.

Human-in-the-Loop Patternpython
from langgraph.checkpoint.memory import MemorySaver

# The interrupt_before parameter causes the graph to pause
# before executing the specified node
checkpointer = MemorySaver()

app_with_human = workflow.compile(
    checkpointer=checkpointer,
    interrupt_before=["execute_tools"],  # pause before executing any tool
)

# Thread ID lets you resume the same conversation
thread_config = {"configurable": {"thread_id": "my-thread-1"}}

# Start the run — it will pause before tool execution
initial_state = {
    "messages": [HumanMessage(content="Delete all files in /tmp/old_logs/")],
    "iteration": 0,
    "final_answer": None,
}

# Run until the first interrupt
for event in app_with_human.stream(initial_state, config=thread_config):
    last_msg = event["messages"][-1]
    if hasattr(last_msg, "tool_calls") and last_msg.tool_calls:
        print("\n--- HUMAN APPROVAL REQUIRED ---")
        for tc in last_msg.tool_calls:
            print(f"Tool: {tc['name']}")
            print(f"Args: {tc['args']}")
        print("-------------------------------")

# Human inspects the tool call and approves (or rejects)
approval = input("Approve this action? (yes/no): ")

if approval.lower() == "yes":
    # Resume from the checkpoint
    for event in app_with_human.stream(None, config=thread_config):
        print(event["messages"][-1].content)
else:
    print("Action rejected by human reviewer.")

Multi-Agent Graphs (Supervisor Pattern)

LangGraph natively supports multi-agent architectures where a supervisor agent routes tasks to specialized worker agents:

Supervisor Multi-Agent Patternpython
from langgraph.graph import StateGraph, END
from typing import Literal

class SupervisorState(TypedDict):
    messages: Annotated[list, add_messages]
    next_agent: str  # which worker to call next

# Specialized worker agents (each is its own compiled graph or function)
def research_agent(state: SupervisorState) -> dict:
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def coding_agent(state: SupervisorState) -> dict:
    # This agent specializes in writing and running code
    response = llm_with_tools.invoke(state["messages"])
    return {"messages": [response]}

def supervisor_node(state: SupervisorState) -> dict:
    """Decides which worker to call next, or whether to finish."""
    system_prompt = """You are a supervisor managing a research agent and a coding agent.
Given the conversation, decide who should act next.
Respond with JSON: {"next": "research_agent" | "coding_agent" | "FINISH"}"""

    response = llm.invoke([
        {"role": "system", "content": system_prompt},
        *state["messages"]
    ])

    decision = json.loads(response.content)
    return {"next_agent": decision["next"]}

def route_from_supervisor(state: SupervisorState) -> str:
    return state["next_agent"]

# Build the supervisor graph
graph = StateGraph(SupervisorState)
graph.add_node("supervisor", supervisor_node)
graph.add_node("research_agent", research_agent)
graph.add_node("coding_agent", coding_agent)

graph.set_entry_point("supervisor")
graph.add_conditional_edges(
    "supervisor",
    route_from_supervisor,
    {
        "research_agent": "research_agent",
        "coding_agent": "coding_agent",
        "FINISH": END,
    }
)
graph.add_edge("research_agent", "supervisor")
graph.add_edge("coding_agent", "supervisor")

supervisor_app = graph.compile()

Knowledge check

In LangGraph, what is the purpose of a "conditional edge"?

Summary

LangGraph gives you the building blocks for production-grade agentic systems:

  1. State flows through the entire graph — all nodes can read and write it
  2. Nodes are pure functions: receive state, return state updates
  3. Conditional edges enable cycles, branching, and early stopping
  4. Checkpoints enable human-in-the-loop and fault tolerance
  5. Supervisor pattern enables scalable multi-agent orchestration

In the next chapter, we'll explore AutoGen — Microsoft's framework for multi-agent conversation, with a different philosophy centered on conversational agents that talk to each other.

Agentic AI