langgraph-state

star 3

Managing state in LangGraph: schemas, reducers, channels, and message passing for coordinating agent execution

christian-bromann By christian-bromann schedule Updated 2/11/2026

name: langgraph-state description: Managing state in LangGraph: schemas, reducers, channels, and message passing for coordinating agent execution language: js

langgraph-state (JavaScript/TypeScript)


name: langgraph-state description: Managing state in LangGraph - schemas, reducers, channels, and message passing for coordinating agent execution

Overview

State is the central data structure in LangGraph that persists throughout graph execution. Proper state management is crucial for building reliable agents.

Key Concepts:

  • StateSchema: Defines the structure and types of your state
  • Reducers: Control how state updates are applied (ReducedValue)
  • Channels: Low-level state management primitives
  • Message Passing: How nodes communicate via state updates

Decision Table: State Update Strategies

Need Solution Use Case
Overwrite value Plain Zod schema Simple fields like strings
Append to list ReducedValue with concat Logs, accumulating data
Custom logic Custom reducer function Complex merging, validation
Messages MessagesValue Chat applications

Code Examples

Basic State Management

import { StateGraph, StateSchema, START, END } from "@langchain/langgraph";
import { z } from "zod";

const State = new StateSchema({
  input: z.string(),
  processed: z.string(),
  count: z.number(),
});

const process = async (state: typeof State.State) => {
  return {
    processed: state.input.toUpperCase(),
    count: state.count + 1,
  };
};

const graph = new StateGraph(State)
  .addNode("process", process)
  .addEdge(START, "process")
  .addEdge("process", END)
  .compile();

const result = await graph.invoke({ input: "hello", count: 0 });
console.log(result);  // { input: 'hello', processed: 'HELLO', count: 1 }

Messages with Reducer

import { StateSchema, MessagesValue, StateGraph, START, END } from "@langchain/langgraph";
import { HumanMessage, AIMessage } from "@langchain/core/messages";

const MessagesState = new StateSchema({
  messages: MessagesValue,
});

const addResponse = async (state: typeof MessagesState.State) => {
  const lastMessage = state.messages.at(-1);
  const userMsg = lastMessage?.content || "";
  return {
    messages: [new AIMessage({ content: `Response to: ${userMsg}` })],
  };
};

const graph = new StateGraph(MessagesState)
  .addNode("respond", addResponse)
  .addEdge(START, "respond")
  .addEdge("respond", END)
  .compile();

const result = await graph.invoke({
  messages: [new HumanMessage({ content: "Hello!" })],
});
console.log(result.messages.length);  // 2 (original + response)

Custom Reducer with ReducedValue

import { StateSchema, ReducedValue, START, END, StateGraph } from "@langchain/langgraph";
import { z } from "zod";

const State = new StateSchema({
  metadata: new ReducedValue(
    z.record(z.string(), z.any()).default(() => ({})),
    {
      inputSchema: z.record(z.string(), z.any()),
      reducer: (current, update) => ({ ...current, ...update }),
    }
  ),
  data: z.string(),
});

const updateMetadata = async (state: typeof State.State) => {
  return { metadata: { timestamp: "2024-01-01" } };
};

const graph = new StateGraph(State)
  .addNode("update", updateMetadata)
  .addEdge(START, "update")
  .addEdge("update", END)
  .compile();

const result = await graph.invoke({
  metadata: { user: "alice" },
  data: "test",
});
// metadata is merged: { user: "alice", timestamp: "2024-01-01" }

List Accumulation with ReducedValue

import { StateSchema, ReducedValue } from "@langchain/langgraph";
import { z } from "zod";

const State = new StateSchema({
  items: new ReducedValue(
    z.array(z.string()).default(() => []),
    {
      inputSchema: z.array(z.string()),
      reducer: (current, update) => current.concat(update),
    }
  ),
});

const addItems = async (state: typeof State.State) => {
  return { items: ["new_item"] };
};

const graph = new StateGraph(State)
  .addNode("add", addItems)
  .addEdge(START, "add")
  .addEdge("add", END)
  .compile();

const result = await graph.invoke({ items: ["old1", "old2"] });
console.log(result.items);  // ['old1', 'old2', 'new_item']

Using Channels API

import { StateGraph, LastValue, BinaryOperatorAggregate } from "@langchain/langgraph";

interface State {
  counter: number;
  logs: string[];
}

const graph = new StateGraph<State>({
  channels: {
    counter: new BinaryOperatorAggregate<number>(
      (x, y) => x + y,
      () => 0
    ),
    logs: new BinaryOperatorAggregate<string[]>(
      (x, y) => x.concat(y),
      () => []
    ),
  },
});

Partial State Updates

import { StateSchema, StateGraph, START, END } from "@langchain/langgraph";
import { z } from "zod";

const State = new StateSchema({
  field1: z.string(),
  field2: z.string(),
  field3: z.string(),
});

const updateField1 = async (state: typeof State.State) => {
  // Only update field1, others unchanged
  return { field1: "updated" };
};

const updateField2 = async (state: typeof State.State) => {
  // Only update field2
  return { field2: "also updated" };
};

const graph = new StateGraph(State)
  .addNode("node1", updateField1)
  .addNode("node2", updateField2)
  .addEdge(START, "node1")
  .addEdge("node1", "node2")
  .addEdge("node2", END)
  .compile();

const result = await graph.invoke({
  field1: "original1",
  field2: "original2",
  field3: "original3",
});
// field1: "updated", field2: "also updated", field3: "original3"

Boundaries

What You CAN Configure

✅ Define custom state schemas with Zod ✅ Add reducers via ReducedValue ✅ Create custom reducer functions ✅ Use built-in channels ✅ Use MessagesValue for chat ✅ Partial state updates ✅ Nested state structures

What You CANNOT Configure

❌ Change state schema after compilation ❌ Access state outside node functions ❌ Modify state directly (must return updates) ❌ Share state between separate graphs

Gotchas

1. Forgot Reducer for Arrays

// ❌ WRONG - Array will be overwritten
const State = new StateSchema({
  items: z.array(z.string()),  // No reducer!
});

// Node 1 returns: { items: ["A"] }
// Node 2 returns: { items: ["B"] }
// Final state: { items: ["B"] }  // A is lost!

// ✅ CORRECT
import { ReducedValue } from "@langchain/langgraph";

const State = new StateSchema({
  items: new ReducedValue(
    z.array(z.string()).default(() => []),
    { reducer: (current, update) => current.concat(update) }
  ),
});
// Final state: { items: ["A", "B"] }

2. State Updates Must Return Partial

// ❌ WRONG - Returning entire state object
const myNode = async (state: typeof State.State) => {
  state.field = "updated";
  return state;  // Don't do this!
};

// ✅ CORRECT - Return partial updates
const myNode = async (state: typeof State.State) => {
  return { field: "updated" };
};

3. Default Values

// ❌ RISKY - No default handling
const State = new StateSchema({
  count: z.number(),  // What if undefined?
});

const increment = async (state: typeof State.State) => {
  return { count: state.count + 1 };  // May error if count undefined
};

// ✅ BETTER - Use defaults in schema
const State = new StateSchema({
  count: z.number().default(0),
});

4. Always Await Nodes

// ❌ WRONG - Forgetting await
const result = graph.invoke({ input: "test" });
console.log(result.output);  // undefined (Promise!)

// ✅ CORRECT
const result = await graph.invoke({ input: "test" });
console.log(result.output);  // Works!

Links

Install via CLI
npx skills add https://github.com/christian-bromann/langchain-skills --skill langgraph-state
Repository Details
star Stars 3
call_split Forks 1
navigation Branch main
article Path SKILL.md
More from Creator
christian-bromann
christian-bromann Explore all skills →