Skip to content

Multi-Agent Workflows & State Management

A single agent looping through a task works fine until the task genuinely needs more than one kind of expertise, research and writing, or diagnosis and fixing. Wiring multiple agents together as separate functions that just call each other gets messy fast: no shared memory, no way to resume if one step fails, no clean place to pause for a human to approve something risky. This lesson covers the real fix, modeling multi-agent coordination as a graph with shared state, using LangGraph, the framework most production multi-agent systems are actually built on.

Owl mascot

What you'll learn

  • LangGraph's state/nodes/edges/checkpointer model
  • Supervisor-worker patterns for coordinating multiple agents
  • Human-in-the-loop approval gates inside a cyclical graph

How it works

Wise Owl and Garden Gnome Control Room

A simple pipeline where output A feeds into input B breaks down the moment real agent reasoning needs to loop back, backtrack on a bad result, or run two things in parallel. LangGraph models this as a graph instead of a straight line, nodes do work, edges decide what happens next, and a shared state object carries context between every step.

A. Core Graph Components

  1. Nodes: Python functions executing specific computational steps (LLM calls, tool executions, or state mutations).
  2. Edges: Direct connection pathways. Conditional Edges execute routing decisions using an LLM or python conditional check to determine the next node.
  3. State Schema: A persistent, transactionally managed memory store passed to every node. It uses Reducer Functions (such as add_messages) to control how updates are appended to or merged into the state database.
  4. Checkpointers: Snapshots that serialize the active state of the graph and write it to database storage (PostgreSQL/Redis) at every node transition.

B. Micro-Step Persistence & Handshakes

Checkpointers provide thread isolation. If an agent loops over many steps, the state is persisted after every node execution. This allows:

  • Reentrancy: Resuming execution from the exact node failure point.
  • Time Travel: Reloading previous thread states to audit agent decision trees.
  • Thread Isolation: Spawning separate execution threads for different users sharing the same graph structure.

Options & when to use each

TopologyRouting LatencyState Conflict RiskToken EfficiencyComplexityPrimary Production Bottleneck
Linear ChainLow (< 200ms)LowHighLowRigid error propagation (cannot backtrack)
Supervisor-WorkerModerateLowModerateModerateRouter model intelligence constraints
Decentralized NetHighHigh (Race conditions)Low (Info inflation)HighCyclic infinite loops and state overrides

Build it: a supervisor routing to two workers

A central supervisor node decides, based on the conversation so far, whether the next step needs a researcher or a writer, the same pattern you'd use for the Foreman capstone's Owl/Gnome split later in this course.

  1. Initialize Folder:
    bash
    mkdir -p ~/AI_BOOTCAMP/labs/langgraph
    cd ~/AI_BOOTCAMP/labs/langgraph
  2. Define Graph Structure: Create agent_graph.py:
python
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver

### 1. Define the Shared State Schema
class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    next_step: str
    decision_payload: str

### 2. Define Node Functions
def supervisor_node(state: AgentState):
    print("🤖 Supervisor evaluating routing...")
    messages = state["messages"]
    last_message = messages[-1].content if messages else ""
    
    # Simple routing logic (in production, use a structured LLM call)
    if "research" in last_message.lower() and state["next_step"] != "researcher":
        return {"next_step": "researcher"}
    elif "draft" in last_message.lower() and state["next_step"] != "writer":
        return {"next_step": "writer"}
    return {"next_step": "end"}

def researcher_node(state: AgentState):
    print("🔍 Researcher executing data query...")
    return {
        "messages": [{"role": "assistant", "content": "RESEARCH_RESULT: Found 12 PostgreSQL instances."}],
        "next_step": "supervisor"
    }

def writer_node(state: AgentState):
    print("✍️ Writer drafting report...")
    return {
        "messages": [{"role": "assistant", "content": "DRAFT_RESULT: Report finalized successfully."}],
        "next_step": "supervisor"
    }

### 3. Compile the State Graph
workflow = StateGraph(AgentState)

### 4. Add Nodes
workflow.add_node("supervisor", supervisor_node)
workflow.add_node("researcher", researcher_node)
workflow.add_node("writer", writer_node)

### 5. Define Edges
workflow.add_edge(START, "supervisor")

### Conditional routing edge
workflow.add_conditional_edges(
    "supervisor",
    lambda state: state["next_step"],
    {
        "researcher": "researcher",
        "writer": "writer",
        "end": END
    }
)

workflow.add_edge("researcher", "supervisor")
workflow.add_edge("writer", "supervisor")

### 6. Add In-Memory State Checkpointer
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

What goes wrong

| Mistake | How you notice it | The fix | | :--- | :--- | :--- | :--- | | Infinite Graph Cycle | Loops forever without routing to END. | Supervisor failed to evaluate termination condition. | Implement a max_iterations counter in graph state; raise ValueError if exceeded. | | State Corruption | Keys overwritten by worker nodes. | Workers returned un-annotated state keys. | Use strict Pydantic schemas for state inputs/outputs; isolate worker update keys. | | Checkpoint Error | PicklingError: Can't pickle object | Memory checkpointer attempted to serialize non-serializable object (database handles). | Keep database connections, file handles, and LLM clients outside the shared state. | | Deadlock Pause | Graph halts on breakpoint indefinitely. | Graph expects external resume input, but client failed to send. | Configure background cron cleaners to audit stalled database thread IDs. |


Confirm it worked

To verify your graph's routing and state serialization:

  1. Execute the Test Run: Add verification code to agent_graph.py and run the script:
    python
    if __name__ == "__main__":
        config = {"configurable": {"thread_id": "session_abc123"}}
        print("--- Beginning Graph Execution ---")
        
        # Trigger with a prompt that requests research first
        for event in app.stream(
            {"messages": [{"role": "user", "content": "Run research and then draft it."}], "next_step": "supervisor"},
            config
        ):
            print(event)
  2. Observe Node Transitions: Audit the stdout logging pattern. Confirm that the sequence strictly matches: SupervisorResearcherSupervisorWriterSupervisorEND.
  3. Inspect State Merges: Observe how add_messages appends data. Print the final state messages list:
    python
    final_state = app.get_state(config)
    print(f"Total Messages Count: {len(final_state.values['messages'])}")
    Confirm that the list contains 3 messages (User prompt + Researcher output + Writer output) rather than just the last worker output.

Next: Day 4 , Security & Evaluation , this multi-agent system is only trustworthy once its output is validated and its execution is guarded.