# Stateful Agent Orchestration with LangGraph

URL: https://mepritam.dev/references/stateful-agent-orchestration-langgraph/

Design deterministic, cycle-safe multi-agent systems using LangGraph state machines  with explicit transition states, guardrails, and runtime validation.

---

When building autonomous AI agents, developers often begin with simple, unstructured loops. You feed an LLM a list of tools, put it in a while-loop, and let it run until it decides to stop.

Free-form ReAct (Reasoning and Acting) loops frequently fail in production. Without constraints, agents get stuck in infinite recursion, hallucinate tool arguments, drift from the user's goal, or run up massive API bills by calling the same tool repeatedly.

To build reliable enterprise systems, model your pipelines as **deterministic state machines**. By structuring workflows as directed graphs where nodes represent execution steps and edges define explicit transitions, you gain complete control over agent behavior. This guide details how to build stateful multi-agent systems using LangGraph and TypeScript.

---

## State management mechanics

The core mechanism of a stateful agent is **single-source-of-truth state tracking**. Instead of passing raw message arrays back and forth, the entire graph shares a centralized, schema-enforced state.

![LangGraph Agent State Transition Machine](/images/references/stateful-agent-orchestration-langgraph-1.webp)

When a node runs:

1. **State Injection:** The node receives the current global state as a read-only parameter.
2. **Deterministic Execution:** The node performs its task, such as invoking an LLM, querying a database, or checking permissions.
3. **State Reduction:** The node returns an update payload. LangGraph applies **reducer functions** to merge this update into the global state. A `messages` array reducer appends new messages rather than overwriting the entire history.
4. **Edge Evaluation:** The graph engine evaluates conditional edges based on the updated state to choose the next node.

This design makes agents testable. Because state transitions run in isolation, developers can mock the state at any node to verify downstream execution paths.

---

## Agent topologies: Chains vs. ReAct vs. State Machines

Before writing code, compare the structural differences among LLM orchestration options:

![LLM Orchestration Topologies Comparison](/images/references/stateful-agent-orchestration-langgraph-2.webp)

- **Linear Chains:** Predictable and simple, but unable to handle conditional routing or error recovery.
- **Autonomous ReAct Loops:** Flexible, but prone to infinite loops, execution drift, and untestable paths.
- **State-Machine Graphs:** The ideal balance. They provide explicit nodes and conditional routing, allowing flexibility while enforcing strict execution boundaries.

---

## Implementing a stateful supervisor graph in TypeScript

Let's build a production-grade multi-agent coordinator that manages a customer support workflow. The graph consists of a **Supervisor node** that acts as the router, and two specialized workers: a **Catalog Searcher** and a **Refund Processor**.

### Step 1: Define the Graph State

We define our state using a typed schema. This state tracks the user's request, the active agent, message history, and a validation flag.

```typescript
import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";

// Define the shape of our global agent state
export const AgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (x, y) => x.concat(y),
    default: () => [],
  }),
  nextAgent: Annotation<string>({
    reducer: (x, y) => y ?? x,
    default: () => "supervisor",
  }),
  refundAuthorized: Annotation<boolean>({
    reducer: (x, y) => y ?? x,
    default: () => false,
  }),
  attemptsCount: Annotation<number>({
    reducer: (x, y) => x + y,
    default: () => 0,
  }),
});

export type AgentStateType = typeof AgentState.State;
```

### Step 2: Write the Graph Nodes

Each node is an asynchronous function that takes the current state and returns a state update.

```typescript
import { AIMessage, HumanMessage } from "@langchain/core/messages";

// 1. Supervisor / Router Node
export async function supervisorNode(state: AgentStateType) {
  const lastMessage = state.messages[state.messages.length - 1];
  const query = lastMessage.content.toString().toLowerCase();

  // Simple, deterministic routing logic.
  // In a full implementation, this would call a fast classification LLM.
  let next = "supervisor";
  if (query.includes("refund") || query.includes("charge")) {
    next = "refund_processor";
  } else if (query.includes("search") || query.includes("product")) {
    next = "catalog_searcher";
  } else {
    next = "__end__"; // Finalize the conversation
  }

  return {
    nextAgent: next,
    attemptsCount: 1, // Increment attempts count to prevent infinite runs
  };
}

// 2. Catalog Searcher Worker Node
export async function catalogSearcherNode(state: AgentStateType) {
  // Simulate catalog retrieval logic
  const mockSearchResult =
    "Found 2 matching items: 1. Nykaa Matte Lipstick (Rs. 450), 2. Nykaa Gel Eyeliner (Rs. 320).";

  return {
    messages: [new AIMessage({ content: mockSearchResult })],
    nextAgent: "supervisor", // Route back to supervisor for next steps
  };
}

// 3. Refund Processor Worker Node
export async function refundProcessorNode(state: AgentStateType) {
  // Enforce a strict state transition: only authorize refunds under Rs. 1000 automatically
  const isAuthorized = true; // Business rule validation logic here

  return {
    messages: [
      new AIMessage({
        content: isAuthorized
          ? "Refund has been pre-authorized."
          : "Refund request escalated to manager.",
      }),
    ],
    refundAuthorized: isAuthorized,
    nextAgent: "supervisor",
  };
}
```

### Step 3: Assemble and Compile the Graph

Now we wire the nodes together. We enforce a loop limit inside our routing edge to prevent infinite execution runs.

```typescript
import { StateGraph, START, END } from "@langchain/langgraph";

// Create the builder instance
const workflow = new StateGraph(AgentState)
  // Add nodes to the graph
  .addNode("supervisor", supervisorNode)
  .addNode("catalog_searcher", catalogSearcherNode)
  .addNode("refund_processor", refundProcessorNode);

// Define edges: START always runs the supervisor first
workflow.addEdge(START, "supervisor");

// Define worker returns: workers always hand back control to the supervisor
workflow.addEdge("catalog_searcher", "supervisor");
workflow.addEdge("refund_processor", "supervisor");

// Define conditional edge routing from supervisor
workflow.addConditionalEdges(
  "supervisor",
  (state: AgentStateType) => {
    // Safety guardrail: hard termination after 5 execution cycles
    if (state.attemptsCount >= 5) {
      console.warn("Max loop cycles reached. Guardrail triggered.");
      return END;
    }

    // Dynamically route based on supervisor state modifications
    const target = state.nextAgent;
    if (target === "__end__") return END;
    return target;
  },
  {
    catalog_searcher: "catalog_searcher",
    refund_processor: "refund_processor",
    [END]: END,
  },
);

// Compile the graph with persistent memory checks (in-memory checkpointing)
import { MemorySaver } from "@langchain/langgraph";
const memory = new MemorySaver();

export const supportAgent = workflow.compile({ checkpointer: memory });
```

---

## Defensive engineering: Guardrails and loop breakers

When deploying agentic state machines to production, design for unexpected LLM behaviors.

- **Hard Cycle Limits:** Track execution iterations (`attemptsCount`) in your state. If an agent loops more than a limit (such as 5 cycles), stop the graph and hand control to a human.
- **Strict Schema Assertions:** If a worker node processes transactional actions, do not rely on LLM parameters directly. Use Zod schemas at the node level to check input payloads at runtime.
- **State Checkpoint Persistence:** Use a persistent checkpointer (like PostgreSQL or Redis) in production. Persistent checkpoints enable query tracing, retrying failed runs from the error node, and enforcing human approval before sensitive mutations.

By moving from probabilistic prompt streams to schema-enforced state graphs, you can build autonomous systems that handle complex user workflows with enterprise-grade reliability.
