zod graph state: default value silently fails when reducer declaration missing #332

Closed
opened 2026-02-15 18:15:58 -05:00 by yindo · 1 comment
Owner

Originally created by @fauxbytes on GitHub (Aug 6, 2025).

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangGraph.js documentation with the integrated search.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangGraph.js rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package).

Example Code

default works with Annotation

import { Annotation, END, START, StateGraph } from '@langchain/langgraph';

const StateA = Annotation.Root({
  foo: Annotation<string[]>({default: () => [], reducer: (a, _) => a})
});

const a = (state:typeof StateA.State) => state

const graph = new StateGraph(StateA)
  .addNode(a.name, a)
  .addEdge(START, a.name)
  .addEdge(a.name, END)
  .compile();

const main = async () => {
  console.log(await graph.invoke({}));
};

main().catch(console.error);

/**
 * output:
 * { foo: [] }
 */

default doesn't work using zod w/o reducer

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


const StateZ = z.object({
  foo: z.array(z.string()).default([])
})

const a = (state:z.infer<typeof StateZ>) => state

const graph = new StateGraph(StateZ)
  .addNode(a.name, a)
  .addEdge(START, a.name)
  .addEdge(a.name, END)
  .compile();

const main = async () => {
  console.log(await graph.invoke({}));
};

main().catch(console.error);

/**
 * output:
 * undefined
 */

Error Message and Stack Trace (if applicable)

No response

Description

The default declaration fails when using zod schemas for declaring graph state. While Annotation seems to force a reducer, and flags a missing reducer key as an error, zod doesn't. So the zod sample above happily compiles & executes, resulting in an undefined channel.
Adding a (deprecated) .langgraph.reducer produces the expected default. Didn't quite understand what the deprecation notice means by "using the exported langgraph meta registry".

System Info

$ cat package.json |jq '{dependencies, devDependencies}'
{
"dependencies": {
"@langchain/anthropic": "^0.3.25",
"@langchain/core": "^0.3.66",
"@langchain/langgraph": "^0.3.12",
"@langchain/openai": "^0.6.3",
"@langchain/tavily": "^0.1.4",
"dotenv": "^17.2.0",
"langchain": "^0.3.30",
"sqlite": "^5.1.1",
"typeorm": "^0.3.25",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/node": "^24.0.14",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
}
}
$ pnpm -v
10.11.0

$ node -v
v20.18.3

$ wmic os get caption
Caption
Microsoft Windows 11 Pro

Originally created by @fauxbytes on GitHub (Aug 6, 2025). ### Checked other resources - [x] I added a very descriptive title to this issue. - [x] I searched the LangGraph.js documentation with the integrated search. - [x] I used the GitHub search to find a similar question and didn't find it. - [x] I am sure that this is a bug in LangGraph.js rather than my code. - [x] The bug is not resolved by updating to the latest stable version of LangGraph (or the specific integration package). ### Example Code default works with Annotation ```typescript import { Annotation, END, START, StateGraph } from '@langchain/langgraph'; const StateA = Annotation.Root({ foo: Annotation<string[]>({default: () => [], reducer: (a, _) => a}) }); const a = (state:typeof StateA.State) => state const graph = new StateGraph(StateA) .addNode(a.name, a) .addEdge(START, a.name) .addEdge(a.name, END) .compile(); const main = async () => { console.log(await graph.invoke({})); }; main().catch(console.error); /** * output: * { foo: [] } */ ``` default doesn't work using zod w/o reducer ```typescript import { END, START, StateGraph } from '@langchain/langgraph'; import { z } from 'zod'; const StateZ = z.object({ foo: z.array(z.string()).default([]) }) const a = (state:z.infer<typeof StateZ>) => state const graph = new StateGraph(StateZ) .addNode(a.name, a) .addEdge(START, a.name) .addEdge(a.name, END) .compile(); const main = async () => { console.log(await graph.invoke({})); }; main().catch(console.error); /** * output: * undefined */ ``` ### Error Message and Stack Trace (if applicable) _No response_ ### Description The default declaration fails when using zod schemas for declaring graph state. While `Annotation` seems to force a reducer, and flags a missing `reducer` key as an error, zod doesn't. So the zod sample above happily compiles & executes, resulting in an undefined channel. Adding a (deprecated) `.langgraph.reducer` produces the expected default. Didn't quite understand what the deprecation notice means by "_using the exported langgraph meta registry_". ### System Info $ cat package.json |jq '{dependencies, devDependencies}' { "dependencies": { "@langchain/anthropic": "^0.3.25", "@langchain/core": "^0.3.66", "@langchain/langgraph": "^0.3.12", "@langchain/openai": "^0.6.3", "@langchain/tavily": "^0.1.4", "dotenv": "^17.2.0", "langchain": "^0.3.30", "sqlite": "^5.1.1", "typeorm": "^0.3.25", "zod": "^3.25.76" }, "devDependencies": { "@types/node": "^24.0.14", "ts-node": "^10.9.2", "typescript": "^5.8.3" } } $ pnpm -v 10.11.0 $ node -v v20.18.3 $ wmic os get caption Caption Microsoft Windows 11 Pro
yindo closed this issue 2026-02-15 18:15:58 -05:00
Author
Owner

@dqbd commented on GitHub (Sep 11, 2025):

Hello! The above code sample seems to be an issue only for zod/v3. Feel free to use .langgraph.default or withLangGraph to provide default values.

Zod 4 does work by default

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

const StateZ = z.object({
  foo: z.array(z.string()).default([]),
});

const a = (state: z.infer<typeof StateZ>) => {
  console.log("a", state);
};

const graph = new StateGraph(StateZ)
  .addNode(a.name, a)
  .addEdge(START, a.name)
  .addEdge(a.name, END)
  .compile();

const main = async () => {
  console.log(await graph.invoke({}));
};

main();
@dqbd commented on GitHub (Sep 11, 2025): Hello! The above code sample seems to be an issue only for `zod/v3`. Feel free to use `.langgraph.default` or `withLangGraph` to provide default values. Zod 4 does work by default ```typescript import { END, START, StateGraph } from "@langchain/langgraph"; import { z } from "zod/v4"; const StateZ = z.object({ foo: z.array(z.string()).default([]), }); const a = (state: z.infer<typeof StateZ>) => { console.log("a", state); }; const graph = new StateGraph(StateZ) .addNode(a.name, a) .addEdge(START, a.name) .addEdge(a.name, END) .compile(); const main = async () => { console.log(await graph.invoke({})); }; main(); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#332