Unable to add Nodes and Edges dynamically before runtime #152

Closed
opened 2026-02-15 17:16:24 -05:00 by yindo · 18 comments
Owner

Originally created by @guidorietbroek on GitHub (Dec 25, 2024).

Description:
When adding nodes to a StateGraph, the type inference only works correctly when using method chaining syntax. Adding nodes dynamically using separate stateGraph.addNode() calls breaks the type accumulation in the N type parameter.

Example of working code (with correct type inference):

const stateGraph = new StateGraph(StateAnnotation)
    .addNode("supervisor", supervisor)
    .addNode("retrievalAgent", retrievalAgent)

Example of code that breaks type inference:

const stateGraph = new StateGraph(StateAnnotation);
stateGraph.addNode("supervisor", supervisor);
stateGraph.addNode("retrievalAgent", retrievalAgent);

This makes it difficult to dynamically add nodes to the graph, as we're forced to use method chaining. This seems unintuitive and limits the flexibility of the StateGraph implementation.

Expected behavior:
Both approaches should maintain correct type inference, allowing for more flexible ways to add nodes to the graph, especially when dealing with dynamic configurations.

Would it be possible to adjust the type definitions to maintain type accumulation in the N type parameter regardless of whether method chaining is used?

Originally created by @guidorietbroek on GitHub (Dec 25, 2024). **Description:** When adding nodes to a StateGraph, the type inference only works correctly when using method chaining syntax. Adding nodes **dynamically** using separate `stateGraph.addNode()` calls breaks the type accumulation in the `N` type parameter. Example of working code (with correct type inference): ```typescript const stateGraph = new StateGraph(StateAnnotation) .addNode("supervisor", supervisor) .addNode("retrievalAgent", retrievalAgent) ``` Example of code that breaks type inference: ```typescript const stateGraph = new StateGraph(StateAnnotation); stateGraph.addNode("supervisor", supervisor); stateGraph.addNode("retrievalAgent", retrievalAgent); ``` This makes it difficult to dynamically add nodes to the graph, as we're forced to use method chaining. This seems unintuitive and limits the flexibility of the StateGraph implementation. **Expected behavior:** Both approaches should maintain correct type inference, allowing for more flexible ways to add nodes to the graph, especially when dealing with dynamic configurations. _Would it be possible to adjust the type definitions to maintain type accumulation in the N type parameter regardless of whether method chaining is used?_
yindo closed this issue 2026-02-15 17:16:24 -05:00
Author
Owner

@guidorietbroek commented on GitHub (Dec 25, 2024):

After further investigation, I think I have found the root cause of this issue. It stems from how type casting is implemented in the addNode method:

return this as StateGraph<SD, S, U, N | K, I, O, C>;

This superficial type casting works for method chaining because each call receives a new cast, but fails for separate calls because the instance retains its original type. Interestingly, this is why addEdge works without chaining (it returns this without type casting) while addNode doesn't.

This suggests the issue requires a deeper solution and it is not possible to solve with an easy fix.

Any thoughts?

Maybe it should be a new feature "adding nodes dynamically to a graph"

@guidorietbroek commented on GitHub (Dec 25, 2024): After further investigation, I think I have found the root cause of this issue. It stems from how type casting is implemented in the `addNode` method: ```typescript return this as StateGraph<SD, S, U, N | K, I, O, C>; ``` This superficial type casting works for method chaining because each call receives a new cast, but fails for separate calls because the instance retains its original type. Interestingly, this is why addEdge works without chaining (it returns this without type casting) while addNode doesn't. This suggests the issue requires a deeper solution and it is not possible to solve with an easy fix. Any thoughts? Maybe it should be a new feature **"adding nodes dynamically to a graph"**
Author
Owner

@guidorietbroek commented on GitHub (Dec 29, 2024):

@jacoblee93 @bracesproul Having worked with LangGraph since its early days, we're now trying to make our well-functioning graphs more generic and reusable. The current method chaining requirement is becoming a real bottleneck for this.

We'd love to build our graphs from configuration, allowing us to:

  • Reuse successful graph patterns across different projects
  • Configure node/edge structure through external config
  • Create graph "templates" that can be customized
  • Create graphs based on user rights (include/exclude nodes depending on the user who is signed in)

Would you consider supporting an alternative to method chaining that would enable these patterns? This would make LangGraph even more powerful as a framework for building flexible, production-ready AI applications. Happy to share our experiences and specific use cases if helpful.

@guidorietbroek commented on GitHub (Dec 29, 2024): @jacoblee93 @bracesproul Having worked with LangGraph since its early days, we're now trying to make our well-functioning graphs more generic and reusable. The current method chaining requirement is becoming a real bottleneck for this. We'd love to build our graphs from configuration, allowing us to: - Reuse successful graph patterns across different projects - Configure node/edge structure through external config - Create graph "templates" that can be customized - Create graphs based on user rights (include/exclude nodes depending on the user who is signed in) Would you consider supporting an alternative to method chaining that would enable these patterns? This would make LangGraph even more powerful as a framework for building flexible, production-ready AI applications. Happy to share our experiences and specific use cases if helpful.
Author
Owner

@jacoblee93 commented on GitHub (Dec 30, 2024):

Yeah would love to see what you're trying to do at a low level and how this blocks you - I think best we could do is have a disableTyping param or some constructor which would not really be that different from just ts-ignore anyway

@jacoblee93 commented on GitHub (Dec 30, 2024): Yeah would love to see what you're trying to do at a low level and how this blocks you - I think best we could do is have a `disableTyping` param or some constructor which would not really be that different from just ts-ignore anyway
Author
Owner

@guidorietbroek commented on GitHub (Dec 30, 2024):

Thanks for the response! To clarify: this isn't just about typing. In Python LangGraph, we can use stateGraph.addNode without issues. What we're trying to achieve is building configurable graphs from configuration:

// We want to build the graph based on client configuration
const graph = new StateGraph(StateAnnotation);
clientConfig.enabledNodes.forEach(nodeName => {
  graph.addNode(nodeName, availableNodes[nodeName]);
  // Configure edges based on which nodes are enabled
});

The current method chaining requirement forces us to either:

  • Build separate static graphs for each possible configuration
  • Use complex workarounds that make the code much harder to maintain

Using @ts-ignore or disableTyping wouldn't solve this - it's not just about bypassing TypeScript errors, but about enabling proper graph construction from configuration before runtime.

We're curious about LangGraph's vision regarding configurable graphs. The Python implementation allows this flexibility - is this an intentional design choice? Having this ability in TypeScript would align well with LangGraph's goal of building flexible, production-ready AI applications. Would love to hear your thoughts on this.

@guidorietbroek commented on GitHub (Dec 30, 2024): Thanks for the response! To clarify: this isn't just about typing. In Python LangGraph, we can use stateGraph.addNode without issues. What we're trying to achieve is building configurable graphs from configuration: ```typescript // We want to build the graph based on client configuration const graph = new StateGraph(StateAnnotation); clientConfig.enabledNodes.forEach(nodeName => { graph.addNode(nodeName, availableNodes[nodeName]); // Configure edges based on which nodes are enabled }); ``` The current method chaining requirement forces us to either: - Build separate static graphs for each possible configuration - Use complex workarounds that make the code much harder to maintain Using @ts-ignore or disableTyping wouldn't solve this - it's not just about bypassing TypeScript errors, but about enabling proper graph construction from configuration before runtime. We're curious about LangGraph's vision regarding configurable graphs. The Python implementation allows this flexibility - is this an intentional design choice? Having this ability in TypeScript would align well with LangGraph's goal of building flexible, production-ready AI applications. Would love to hear your thoughts on this.
Author
Owner

@guidorietbroek commented on GitHub (Dec 30, 2024):

I am pretty sure that in the early days of LangGraph this was possible. The first tutorials worked with graph.addNode. and after a while it went to chaining.

For a more practical application. We have a chatbot with prebuilt tools (as agents) and these are configurable. But we would like to make this even more configurable by toggling these tools in a portal. We are using a supervisor that we build dynamically adding these available subagents based on the config. But right now we have to load all agents as node and create all edges from these node to END with some dynamic conditional edges in between.

Hope this gives more insight in the practical solution. I am pretty sure LangGraph Cloud/Studio will benefit of this as well.

@guidorietbroek commented on GitHub (Dec 30, 2024): I am pretty sure that in the early days of LangGraph this was possible. The first tutorials worked with `graph.addNode`. and after a while it went to chaining. For a more practical application. We have a chatbot with prebuilt tools (as agents) and these are configurable. But we would like to make this even more configurable by toggling these tools in a portal. We are using a supervisor that we build dynamically adding these available subagents based on the config. But right now we have to load all agents as node and create all edges from these node to END with some dynamic conditional edges in between. Hope this gives more insight in the practical solution. I am pretty sure LangGraph Cloud/Studio will benefit of this as well.
Author
Owner

@jacoblee93 commented on GitHub (Dec 30, 2024):

I see - interesting.

It sounds like something like disableValidation parameter would unblock you?

@jacoblee93 commented on GitHub (Dec 30, 2024): I see - interesting. It sounds like something like `disableValidation` parameter would unblock you?
Author
Owner

@guidorietbroek commented on GitHub (Dec 31, 2024):

Not exactly - disabling validation would still require us to work around the method chaining pattern. The core issue is that we want to build graphs programmatically from configuration, similar to how it works in Python LangGraph.

Method chaining forces a static, linear definition of the graph. What we're looking for is the ability to construct graphs more dynamically, where nodes and edges can be added based on configuration - without having to build separate graph definitions for each possible combination.

Is supporting this kind of configuration-based graph construction something that aligns with LangGraph's vision? Or is there perhaps a different pattern you'd recommend for achieving this?

@guidorietbroek commented on GitHub (Dec 31, 2024): Not exactly - disabling validation would still require us to work around the method chaining pattern. The core issue is that we want to build graphs programmatically from configuration, similar to how it works in Python LangGraph. Method chaining forces a static, linear definition of the graph. What we're looking for is the ability to construct graphs more dynamically, where nodes and edges can be added based on configuration - without having to build separate graph definitions for each possible combination. Is supporting this kind of configuration-based graph construction something that aligns with LangGraph's vision? Or is there perhaps a different pattern you'd recommend for achieving this?
Author
Owner

@jacoblee93 commented on GitHub (Jan 3, 2025):

I think this sounds reasonable - but what error do you actually see with this?

What about:

// We want to build the graph based on client configuration
let graph = new StateGraph(StateAnnotation);
clientConfig.enabledNodes.forEach(nodeName => {
  graph = graph.addNode(nodeName, availableNodes[nodeName]);
  // Configure edges based on which nodes are enabled
});
@jacoblee93 commented on GitHub (Jan 3, 2025): I think this sounds reasonable - but what error do you actually see with this? What about: ```ts // We want to build the graph based on client configuration let graph = new StateGraph(StateAnnotation); clientConfig.enabledNodes.forEach(nodeName => { graph = graph.addNode(nodeName, availableNodes[nodeName]); // Configure edges based on which nodes are enabled }); ```
Author
Owner

@guidorietbroek commented on GitHub (Jan 6, 2025):

Thanks for taking the time to have a look! Interesting solution you provide.

I tried the reassignment approach, but it still forces us into a fixed, predefined graph structure. Here's what I tried:

let stateGraph = new StateGraph(StateAnnotation);
stateGraph = stateGraph.addNode("nodeName", nodeFunction);

This results in:

Type 'StateGraph<...>' is not assignable to type 'StateGraph<...>'.
Types of property 'waitingEdges' are incompatible.
Type 'Set<[("__start__" | "nodeName")[], "__start__" | "nodeName"]>' is not assignable to type 'Set<["__start__"[], "__start__"]>'.

This doesn't work - not just because of TypeScript, but because the underlying StateGraph seems designed around static, chain-based construction rather than dynamic configuration. Making it impossible to assign nodes dynamically.

As a workaround we now add all nodes hardcoded and create for all nodes a static routing to END. And in the middel we use a dynamic edge which only routes the available nodes inside the graph. For now it's okay, but when we have 100+ nodes we can choose of, it get's a bit messy and dirty :)

As an answer to your first question.

// When using:
const stateGraph = new StateGraph(StateAnnotation);
stateGraph.addNode("nodeName", nodeFunction);
stateGraph.addEdge("nodeName", END);  

This fails with:

Argument of type '"nodeName"' is not assignable to parameter of type '"__start__"'.ts(2345)

Meaning the StateGraph structure doesn't support adding nodes independently from the chain.

@guidorietbroek commented on GitHub (Jan 6, 2025): Thanks for taking the time to have a look! Interesting solution you provide. I tried the reassignment approach, but it still forces us into a fixed, predefined graph structure. Here's what I tried: ```typescript let stateGraph = new StateGraph(StateAnnotation); stateGraph = stateGraph.addNode("nodeName", nodeFunction); ``` This results in: ```typescript Type 'StateGraph<...>' is not assignable to type 'StateGraph<...>'. Types of property 'waitingEdges' are incompatible. Type 'Set<[("__start__" | "nodeName")[], "__start__" | "nodeName"]>' is not assignable to type 'Set<["__start__"[], "__start__"]>'. ``` This doesn't work - not just because of TypeScript, but because the underlying StateGraph seems designed around static, chain-based construction rather than dynamic configuration. Making it impossible to assign nodes dynamically. As a workaround we now add all nodes hardcoded and create for all nodes a static routing to END. And in the middel we use a dynamic edge which only routes the available nodes inside the graph. For now it's okay, but when we have 100+ nodes we can choose of, it get's a bit messy and dirty :) As an answer to your first question. ```typescript // When using: const stateGraph = new StateGraph(StateAnnotation); stateGraph.addNode("nodeName", nodeFunction); stateGraph.addEdge("nodeName", END); ``` This fails with: ```typescript Argument of type '"nodeName"' is not assignable to parameter of type '"__start__"'.ts(2345) ``` Meaning the StateGraph structure doesn't support adding nodes independently from the chain.
Author
Owner

@guidorietbroek commented on GitHub (Jan 13, 2025):

@jacoblee93 sorry to bump you again. But it’s really holding us up right now.

Is it possible to let us now if we can expect to see this functionality in short future? Otherwise we need to change our plans unfortunately.

@guidorietbroek commented on GitHub (Jan 13, 2025): @jacoblee93 sorry to bump you again. But it’s really holding us up right now. Is it possible to let us now if we can expect to see this functionality in short future? Otherwise we need to change our plans unfortunately.
Author
Owner

@jacoblee93 commented on GitHub (Jan 13, 2025):

Hey sorry for the delays - will try to dig in this week!

Or maybe CC @benjamincburns if he has time

@jacoblee93 commented on GitHub (Jan 13, 2025): Hey sorry for the delays - will try to dig in this week! Or maybe CC @benjamincburns if he has time
Author
Owner

@nikita-wayhq commented on GitHub (Jan 29, 2025):

Side note for those who read.

Seems like adding edges is fine, as it returns this:

https://github.com/langchain-ai/langgraphjs/blob/4bee8fdc9c2bc04d49db504fd267ce65cc4856ba/libs/langgraph/src/graph/state.ts#L351-L354

Anyone from LangChain Team can confirm it's future proof to at least add edges conditionally?

@nikita-wayhq commented on GitHub (Jan 29, 2025): Side note for those who read. Seems like adding edges is fine, as it returns `this`: https://github.com/langchain-ai/langgraphjs/blob/4bee8fdc9c2bc04d49db504fd267ce65cc4856ba/libs/langgraph/src/graph/state.ts#L351-L354 Anyone from LangChain Team can confirm it's future proof to at least add edges conditionally?
Author
Owner

@guidorietbroek commented on GitHub (Feb 16, 2025):

We will move away from LangGraph due to this limitation.

@guidorietbroek commented on GitHub (Feb 16, 2025): We will move away from LangGraph due to this limitation.
Author
Owner

@jacoblee93 commented on GitHub (Feb 16, 2025):

Sorry - let's still leave open

@jacoblee93 commented on GitHub (Feb 16, 2025): Sorry - let's still leave open
Author
Owner

@benjamincburns commented on GitHub (Feb 24, 2025):

@guidorietbroek sorry that I'm late on the draw here, but the workaround in order to enable non-chaining node/graph additions is to specify the type parameters for StateGraph yourself, using string for the N param.

It's a little ugly, but this example works for me with latest @langchain/langgraph. We could probably relax the default type of N on StateGraph to string to enable this w/o having to provide the params yourself. I'd need to think about it a bit more to see if it'd break anything.

Note that the string literals passed to addNode and addEdge are not restricted by the type checker here, however they must still pass graph validation checks (e.g. I can't add an edge from node1 to a non-existent node3).

import {
  Annotation,
  StateGraph,
  StateType,
  UpdateType,
} from "@langchain/langgraph";

const StateAnnotation = Annotation.Root({
  foo: Annotation<string>,
});

const node1 = async (state: typeof StateAnnotation.State) => {
  return { foo: state.foo + " bar" };
};

const node2 = async (state: typeof StateAnnotation.State) => {
  return { foo: state.foo + " baz" };
};

const graph = new StateGraph<
  (typeof StateAnnotation)["spec"],
  StateType<(typeof StateAnnotation)["spec"]>,
  UpdateType<(typeof StateAnnotation)["spec"]>,
  string
>(StateAnnotation);

graph.addNode("node1", node1);
graph.addNode("node2", node2);
graph.addEdge("__start__", "node1");
graph.addEdge("node1", "node2");
const compiledGraph = await graph.compile();

const result = await compiledGraph.invoke({ foo: "foo" });
console.log(result.foo);
// prints: foo bar baz
@benjamincburns commented on GitHub (Feb 24, 2025): @guidorietbroek sorry that I'm late on the draw here, but the workaround in order to enable non-chaining node/graph additions is to specify the type parameters for `StateGraph` yourself, using `string` for the `N` param. It's a little ugly, but this example works for me with latest `@langchain/langgraph`. We could probably relax the default type of `N` on `StateGraph` to `string` to enable this w/o having to provide the params yourself. I'd need to think about it a bit more to see if it'd break anything. Note that the string literals passed to `addNode` and `addEdge` are _not_ restricted by the type checker here, however they must still pass graph validation checks (e.g. I can't add an edge from `node1` to a non-existent `node3`). ```typescript import { Annotation, StateGraph, StateType, UpdateType, } from "@langchain/langgraph"; const StateAnnotation = Annotation.Root({ foo: Annotation<string>, }); const node1 = async (state: typeof StateAnnotation.State) => { return { foo: state.foo + " bar" }; }; const node2 = async (state: typeof StateAnnotation.State) => { return { foo: state.foo + " baz" }; }; const graph = new StateGraph< (typeof StateAnnotation)["spec"], StateType<(typeof StateAnnotation)["spec"]>, UpdateType<(typeof StateAnnotation)["spec"]>, string >(StateAnnotation); graph.addNode("node1", node1); graph.addNode("node2", node2); graph.addEdge("__start__", "node1"); graph.addEdge("node1", "node2"); const compiledGraph = await graph.compile(); const result = await compiledGraph.invoke({ foo: "foo" }); console.log(result.foo); // prints: foo bar baz ```
Author
Owner

@jung-han commented on GitHub (Apr 1, 2025):

I may have misunderstood, but benjamincburns comment above seems to suggest that there's no problem when using annotations together, but there is a problem when specifying input and output.

const InputAnnotation = Annotation.Root({ foo: Annotation<string> });
const OutputAnnotation = Annotation.Root({ response: Annotation<string> });
const OverallAnnotation = Annotation.Root({
    foo: Annotation<string>,
    response: Annotation<string>,
});

const graph = new StateGraph<
  (typeof OverallAnnotation)['spec'],
  StateType<(typeof OverallAnnotation)['spec']>,
  UpdateType<(typeof OverallAnnotation)['spec']>,
  string, // node id
  (typeof InputAnnotation)['spec'], // input
  (typeof OutputAnnotation)['spec'] // output
>({ input: InputAnnotation, output: OutputAnnotation }); // type error!

Has anyone experienced this issue?

@jung-han commented on GitHub (Apr 1, 2025): I may have misunderstood, but benjamincburns comment above seems to suggest that there's no problem when using annotations together, but there is a problem when specifying input and output. ```ts const InputAnnotation = Annotation.Root({ foo: Annotation<string> }); const OutputAnnotation = Annotation.Root({ response: Annotation<string> }); const OverallAnnotation = Annotation.Root({ foo: Annotation<string>, response: Annotation<string>, }); const graph = new StateGraph< (typeof OverallAnnotation)['spec'], StateType<(typeof OverallAnnotation)['spec']>, UpdateType<(typeof OverallAnnotation)['spec']>, string, // node id (typeof InputAnnotation)['spec'], // input (typeof OutputAnnotation)['spec'] // output >({ input: InputAnnotation, output: OutputAnnotation }); // type error! ``` Has anyone experienced this issue?
Author
Owner

@benjamincburns commented on GitHub (Apr 3, 2025):

@jung-han see this example in our docs, and please open a new issue if you're still having problems - this one meant to track something different.

@benjamincburns commented on GitHub (Apr 3, 2025): @jung-han see [this example](https://langchain-ai.github.io/langgraphjs/concepts/low_level/#multiple-schemas) in our docs, and please open a new issue if you're still having problems - this one meant to track something different.
Author
Owner

@dqbd commented on GitHub (Jun 17, 2025):

Hello! Closing this as Won't Fix since widening the default type for Node would result in addEdge not warning on adding edges for non-existent nodes.

The workaround mentioned by @benjamincburns should alleviate the issue for folks who want to dynamically create graphs

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

const StateAnnotation = Annotation.Root({
  foo: Annotation<string>,
});

const builder = new StateGraph<
  (typeof StateAnnotation)["spec"],
  (typeof StateAnnotation)["State"],
  (typeof StateAnnotation)["Update"],
  // specify nodes beforehand & preserve intellisense
  "node1" | "node2" | (string & {})
>(StateAnnotation);

builder.addNode({
  node1: ({ foo }) => ({ foo: `${foo} bar` }),
  node2: ({ foo }) => ({ foo: `${foo} baz` }),
});

builder.addEdge("__start__", "node1");
builder.addEdge("node1", "node2");
const graph = builder.compile();

const result = await graph.invoke({ foo: "test" });
console.log(result.foo);
@dqbd commented on GitHub (Jun 17, 2025): Hello! Closing this as Won't Fix since widening the default type for Node would result in `addEdge` not warning on adding edges for non-existent nodes. The workaround mentioned by @benjamincburns should alleviate the issue for folks who want to dynamically create graphs ```ts import { Annotation, StateGraph } from "@langchain/langgraph"; const StateAnnotation = Annotation.Root({ foo: Annotation<string>, }); const builder = new StateGraph< (typeof StateAnnotation)["spec"], (typeof StateAnnotation)["State"], (typeof StateAnnotation)["Update"], // specify nodes beforehand & preserve intellisense "node1" | "node2" | (string & {}) >(StateAnnotation); builder.addNode({ node1: ({ foo }) => ({ foo: `${foo} bar` }), node2: ({ foo }) => ({ foo: `${foo} baz` }), }); builder.addEdge("__start__", "node1"); builder.addEdge("node1", "node2"); const graph = builder.compile(); const result = await graph.invoke({ foo: "test" }); console.log(result.foo); ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraphjs#152