When Using Multiple States, Changing the Input State of a Node can Affect the State Fields Received by the Routing Function #316

Closed
opened 2026-02-20 17:36:20 -05:00 by yindo · 6 comments
Owner

Originally created by @ahmed33033 on GitHub (Nov 21, 2024).

Checked other resources

  • I added a very descriptive title to this issue.
  • I searched the LangGraph/LangChain 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/LangChain rather than my code.
  • I am sure this is better as an issue rather than a GitHub discussion, since this is a LangGraph bug and not a design question.

Example Code

from operator import add
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, START, END

class PersonState(TypedDict):
    name: str
    money: Annotated[int, add]
    
class PurchasesState(TypedDict):
    purchases: Annotated[list[str], add]
    total_cost: Annotated[int, add]

class OverallState(PersonState, PurchasesState):
    pass


# node_1 operation. 
def pay_salary(state: PersonState) -> OverallState:
    print(f"Node 1: {state}")
    return {"money": 1000}

# node_2 operation. 
def subtract_expenses(state: PurchasesState) -> OverallState:
    print(f"Node 2: {state}")
    return {'money': -100, "purchases": ["new iphone"], 'total_cost': 100}

# node_3 operation. 
def add_assistance(state: OverallState) -> OverallState:
    print(f"Node 3: {state}")
    return {"money": 100}

# Routing Function. 
def check_if_poor(state: OverallState):
    print(f"Routing function: {state}")
    # if poor, add asssitance
    if (state['money'] < 100): return "node_3"
    else: return END


graph = StateGraph(OverallState, input = PersonState)
graph.add_edge(START, 'node_1')
graph.add_node('node_1', pay_salary)
graph.add_edge('node_1', 'node_2')

graph.add_node('node_2', subtract_expenses)
graph.add_conditional_edges('node_2', check_if_poor)

graph.add_node('node_3', add_assistance)
graph.add_edge("node_3", END)

workflow = graph.compile()
output_dict = workflow.invoke({"name": "Ahmed"})
print(f"Final output: {output_dict}")

Error Message and Stack Trace (if applicable)

(No exception, but inaccurate console output)

CONSOLE OUTPUT:
Node 1: {'name': 'Ahmed', 'money': 0}
Node 2: {'purchases': [], 'total_cost': 0}
Routing function: {'money': -100, 'purchases': ['new iphone'], 'total_cost': 100}
Node 3: {'name': 'Ahmed', 'money': 900, 'purchases': ['new iphone'], 'total_cost': 100}
Final output: {'name': 'Ahmed', 'money': 1000, 'purchases': ['new iphone'], 'total_cost': 100}

Description

(Derived from Discussion Post #2197 , please see discussion post for detailed description)

The Console Output line that is inaccurate:

Routing function: {'money': -100, 'purchases': ['new iphone'], 'total_cost': 100}

How does money have a value of -100? In the method pay_salary, the money attribute is set to 1000. Then, in the subtract_expenses method, I subtract 100 from it. So, the money attribute should have a value of 900, not -100.

The line that is causing the weird output (after debugging)

def subtract_expenses(state: PurchasesState) -> OverallState:

How is it causing the issue?

When I change the inputted state from PurchasesState to OverallState, it works as expected, i.e. the output printed in the routing function check_if_poor is:

Routing function: {'money': 900, 'purchases': ['new iphone'], 'total_cost': 100, 'name': 'Ahmed'}

What's my question?

Why does changing the type of the inputted state in the function subtract_expenses lead me to receive different values for the money attribute in the routing function check_if_poor?

Thanks in advance!

System Info

System Information

OS: Windows
OS Version: 10.0.19045
Python Version: 3.12.6 (tags/v3.12.6:a4a2d2b, Sep 6 2024, 20:11:23) [MSC v.1940 64 bit (AMD64)]

Package Information

langchain_core: 0.3.19
langchain: 0.3.7
langchain_community: 0.3.7
langsmith: 0.1.144
langchain_openai: 0.2.9
langchain_text_splitters: 0.3.2
langgraph: 0.2.53

Optional packages not installed

langserve

Other Dependencies

aiohttp: 3.11.7
async-timeout: Installed. No version info available.
dataclasses-json: 0.6.7
httpx: 0.27.2
httpx-sse: 0.4.0
jsonpatch: 1.33
langgraph-checkpoint: 2.0.5
langgraph-sdk: 0.1.36
numpy: 1.26.4
openai: 1.55.0
orjson: 3.10.11
packaging: 24.2
pydantic: 2.10.0
pydantic-settings: 2.6.1
PyYAML: 6.0.2
requests: 2.32.3
requests-toolbelt: 1.0.0
SQLAlchemy: 2.0.35
tenacity: 9.0.0
tiktoken: 0.8.0
typing-extensions: 4.12.2

Originally created by @ahmed33033 on GitHub (Nov 21, 2024). ### Checked other resources - [X] I added a very descriptive title to this issue. - [X] I searched the [LangGraph](https://langchain-ai.github.io/langgraph/)/LangChain 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/LangChain rather than my code. - [X] I am sure this is better as an issue [rather than a GitHub discussion](https://github.com/langchain-ai/langgraph/discussions/new/choose), since this is a LangGraph bug and not a design question. ### Example Code ```python from operator import add from typing import TypedDict, Annotated from langgraph.graph import StateGraph, START, END class PersonState(TypedDict): name: str money: Annotated[int, add] class PurchasesState(TypedDict): purchases: Annotated[list[str], add] total_cost: Annotated[int, add] class OverallState(PersonState, PurchasesState): pass # node_1 operation. def pay_salary(state: PersonState) -> OverallState: print(f"Node 1: {state}") return {"money": 1000} # node_2 operation. def subtract_expenses(state: PurchasesState) -> OverallState: print(f"Node 2: {state}") return {'money': -100, "purchases": ["new iphone"], 'total_cost': 100} # node_3 operation. def add_assistance(state: OverallState) -> OverallState: print(f"Node 3: {state}") return {"money": 100} # Routing Function. def check_if_poor(state: OverallState): print(f"Routing function: {state}") # if poor, add asssitance if (state['money'] < 100): return "node_3" else: return END graph = StateGraph(OverallState, input = PersonState) graph.add_edge(START, 'node_1') graph.add_node('node_1', pay_salary) graph.add_edge('node_1', 'node_2') graph.add_node('node_2', subtract_expenses) graph.add_conditional_edges('node_2', check_if_poor) graph.add_node('node_3', add_assistance) graph.add_edge("node_3", END) workflow = graph.compile() output_dict = workflow.invoke({"name": "Ahmed"}) print(f"Final output: {output_dict}") ``` ### Error Message and Stack Trace (if applicable) ```shell (No exception, but inaccurate console output) CONSOLE OUTPUT: Node 1: {'name': 'Ahmed', 'money': 0} Node 2: {'purchases': [], 'total_cost': 0} Routing function: {'money': -100, 'purchases': ['new iphone'], 'total_cost': 100} Node 3: {'name': 'Ahmed', 'money': 900, 'purchases': ['new iphone'], 'total_cost': 100} Final output: {'name': 'Ahmed', 'money': 1000, 'purchases': ['new iphone'], 'total_cost': 100} ``` ### Description (Derived from Discussion Post #2197 , please see discussion post for detailed description) ## The Console Output line that is inaccurate: ``` Routing function: {'money': -100, 'purchases': ['new iphone'], 'total_cost': 100} ``` How does `money` have a value of -100? In the method `pay_salary`, the `money` attribute is set to 1000. Then, in the `subtract_expenses` method, I subtract 100 from it. So, the `money` attribute should have a value of 900, not -100. ## The line that is causing the weird output (after debugging) ```python def subtract_expenses(state: PurchasesState) -> OverallState: ``` ## How is it causing the issue? When I change the inputted state from `PurchasesState` to `OverallState`, it works as expected, i.e. the output printed in the routing function `check_if_poor` is: ``` Routing function: {'money': 900, 'purchases': ['new iphone'], 'total_cost': 100, 'name': 'Ahmed'} ``` ## What's my question? Why does changing the type of the inputted state in the function `subtract_expenses` lead me to receive different values for the `money` attribute in the routing function `check_if_poor`? Thanks in advance! ### System Info System Information ------------------ > OS: Windows > OS Version: 10.0.19045 > Python Version: 3.12.6 (tags/v3.12.6:a4a2d2b, Sep 6 2024, 20:11:23) [MSC v.1940 64 bit (AMD64)] Package Information ------------------- > langchain_core: 0.3.19 > langchain: 0.3.7 > langchain_community: 0.3.7 > langsmith: 0.1.144 > langchain_openai: 0.2.9 > langchain_text_splitters: 0.3.2 > langgraph: 0.2.53 Optional packages not installed ------------------------------- > langserve Other Dependencies ------------------ > aiohttp: 3.11.7 > async-timeout: Installed. No version info available. > dataclasses-json: 0.6.7 > httpx: 0.27.2 > httpx-sse: 0.4.0 > jsonpatch: 1.33 > langgraph-checkpoint: 2.0.5 > langgraph-sdk: 0.1.36 > numpy: 1.26.4 > openai: 1.55.0 > orjson: 3.10.11 > packaging: 24.2 > pydantic: 2.10.0 > pydantic-settings: 2.6.1 > PyYAML: 6.0.2 > requests: 2.32.3 > requests-toolbelt: 1.0.0 > SQLAlchemy: 2.0.35 > tenacity: 9.0.0 > tiktoken: 0.8.0 > typing-extensions: 4.12.2
yindo added the bug label 2026-02-20 17:36:20 -05:00
yindo closed this issue 2026-02-20 17:36:20 -05:00
Author
Owner

@vbarda commented on GitHub (Nov 22, 2024):

@ahmed33033 great question -- that's because the state type annotation (input schema) in the node function acts as a "filter" -- currently you use PurchasesState in subtract_expenses, so the node ignores the money value (since PurchasesState doesn't have that key). You need to change it to PersonState, as that is what you're outputting from the previous node (pay_salary). that's also why OverallState works too, as it also includes money key.

def subtract_expenses(state: PersonState) -> OverallState:
    ...

hope this helps!

@vbarda commented on GitHub (Nov 22, 2024): @ahmed33033 great question -- that's because the state type annotation (input schema) in the node function acts as a "filter" -- currently you use `PurchasesState` in `subtract_expenses`, so the node ignores the `money` value (since `PurchasesState` doesn't have that key). You need to change it to `PersonState`, as that is what you're outputting from the previous node (`pay_salary`). that's also why `OverallState` works too, as it also includes `money` key. ```python def subtract_expenses(state: PersonState) -> OverallState: ... ``` hope this helps!
Author
Owner

@ahmed33033 commented on GitHub (Nov 22, 2024):

@ahmed33033 great question -- that's because the state type annotation (input schema) in the node function acts as a "filter" -- currently you use PurchasesState in subtract_expenses, so the node ignores the money value (since PurchasesState doesn't have that key). You need to change it to PersonState, as that is what you're outputting from the previous node (pay_salary). that's also why OverallState works too, as it also includes money key.

def subtract_expenses(state: PersonState) -> OverallState:
    ...

hope this helps!

Thank you so much for the quick reply!

Sorry, I think I didn't highlight the issue clearly.

You're absolutely right about the input state acting as a filer. But, the issue is mainly centered around the routing function check_if_poor, and not subtract_expenses. The issue is that the input schema of subtract_expenses affects the state fields that the routing function check_if_poor receives. Essentially, the graph flow should be as follows:

1 - Node pay_salary updates money to 1000.
2 - Node subtract_expenses issues another update to money of -100. This means that after subtract_expenses executes, money should have a value of 900.
3 - Routing function check_if_poor checks the value of money. It's supposed to be 900. But, as you can see from the first console output, the routing function check_if_poor somehow receives a money value of -100. This is the inaccuracy.

The issue is somehow "fixed" by changing the input state of subtract_expenses. But, the input state of subtract_expenses should not affect the state values that the routing function check_if_poor later receives. This is because langgraph nodes can "write to any state channel in the graph state" (langgraph glossary).

@ahmed33033 commented on GitHub (Nov 22, 2024): > @ahmed33033 great question -- that's because the state type annotation (input schema) in the node function acts as a "filter" -- currently you use `PurchasesState` in `subtract_expenses`, so the node ignores the `money` value (since `PurchasesState` doesn't have that key). You need to change it to `PersonState`, as that is what you're outputting from the previous node (`pay_salary`). that's also why `OverallState` works too, as it also includes `money` key. > > ```python > def subtract_expenses(state: PersonState) -> OverallState: > ... > ``` > > hope this helps! Thank you so much for the quick reply! Sorry, I think I didn't highlight the issue clearly. You're absolutely right about the input state acting as a filer. But, the issue is mainly centered around the routing function `check_if_poor`, and not `subtract_expenses`. The issue is that the input schema of `subtract_expenses` affects the state fields that the routing function `check_if_poor` receives. Essentially, the graph flow should be as follows: 1 - Node `pay_salary` updates `money` to 1000. 2 - Node `subtract_expenses` issues another update to `money` of -100. This means that after `subtract_expenses` executes, `money` should have a value of 900. 3 - Routing function `check_if_poor` checks the value of `money`. It's supposed to be *900*. But, as you can see from the first console output, the routing function `check_if_poor` somehow receives a `money` value of *-100*. **This is the inaccuracy.** The issue is somehow "fixed" by changing the input state of `subtract_expenses`. But, the input state of `subtract_expenses` should not affect the state values that the routing function `check_if_poor` later receives. This is because langgraph nodes can "write to any state channel in the graph state" ([langgraph glossary](https://langchain-ai.github.io/langgraph/concepts/low_level/#multiple-schemas)).
Author
Owner

@hinthornw commented on GitHub (Nov 22, 2024):

Hm ya this does feel surprising. The -100 is overwriting the state wholesale rather than being passed to the reducer (1000 - 900), despite the reducer of add being set in both states (overall state via inheritence) - thanks for reporting, Ahmed! We'll investigate

@hinthornw commented on GitHub (Nov 22, 2024): Hm ya this does feel surprising. The -100 is overwriting the state wholesale rather than being passed to the reducer (1000 - 900), despite the reducer of `add` being set in both states (overall state via inheritence) - thanks for reporting, Ahmed! We'll investigate
Author
Owner

@mcbortolozzo commented on GitHub (Feb 26, 2025):

I was taking a look at how the graph is handling this interaction between the node and the conditional edges, I may have identified some further clues on how to fix this issue, however I'm just only getting familiar with langgraph, so sorry if anything here is not making that much sense.

  1. When building the graph here, the attach_branch method used to include the conditional edge relies on the input schema of the previous node, since it has already been included on the builder nodes (start in self.builder.nodes is True)
    https://github.com/langchain-ai/langgraph/blob/162e96262f8bd3a771eed05a780ac67c283b5b45/libs/langgraph/langgraph/graph/state.py#L830-L838

  2. This generates a state reader that relies on that same schema to coerce the input for the branching function
    https://github.com/langchain-ai/langgraph/blob/162e96262f8bd3a771eed05a780ac67c283b5b45/libs/langgraph/langgraph/graph/state.py#L864-L883

  3. When the branching code actually runs, this coerces the input for it to the previous node's input state, instead of the state being used for the routing function's input
    https://github.com/langchain-ai/langgraph/blob/162e96262f8bd3a771eed05a780ac67c283b5b45/libs/langgraph/langgraph/graph/graph.py#L76-L95

It seems to me that there is no equivalent concept of input on the schema for the branches, so we can't simply update that schema in the attach_branch method do reflect the router's input arg type. I got stuck at this point in my investigation, maybe someone else can help with some further pointers based on the findings above 😄

@mcbortolozzo commented on GitHub (Feb 26, 2025): I was taking a look at how the graph is handling this interaction between the node and the conditional edges, I may have identified some further clues on how to fix this issue, however I'm just only getting familiar with langgraph, so sorry if anything here is not making that much sense. 1. When building the graph here, the `attach_branch` method used to include the conditional edge relies on the input schema of the previous node, since it has already been included on the builder nodes (`start in self.builder.nodes` is `True`) https://github.com/langchain-ai/langgraph/blob/162e96262f8bd3a771eed05a780ac67c283b5b45/libs/langgraph/langgraph/graph/state.py#L830-L838 2. This generates a state reader that relies on that same schema to coerce the input for the branching function https://github.com/langchain-ai/langgraph/blob/162e96262f8bd3a771eed05a780ac67c283b5b45/libs/langgraph/langgraph/graph/state.py#L864-L883 3. When the branching code actually runs, this coerces the input for it to the previous node's input state, instead of the state being used for the routing function's input https://github.com/langchain-ai/langgraph/blob/162e96262f8bd3a771eed05a780ac67c283b5b45/libs/langgraph/langgraph/graph/graph.py#L76-L95 It seems to me that there is no equivalent concept of `input` on the schema for the branches, so we can't simply update that schema in the `attach_branch` method do reflect the router's input arg type. I got stuck at this point in my investigation, maybe someone else can help with some further pointers based on the findings above 😄
Author
Owner

@AI091 commented on GitHub (Feb 27, 2025):

The behavior that @mcbortolozzo describe is correct. See this example for how to reproduce easily. itsnot just an update issue, the routing function only sees as input, what the preceding node gave it.

from langgraph.graph import StateGraph, START, END
from typing import TypedDict, Literal

class OverallState(TypedDict):
    property_in_input: str 
    property_not_in_input: str 
    property_control: str

class InputState(TypedDict):
    property_in_input: str 

def node1(input: OverallState):
    print("node1 overall state" , input)
    return {
        "property_control": "route_to_node2",
    }

def intermediate(input: InputState):
    print("intermediate node with input state" , input)
    return {
        "property_control": "route_to_end",
    }
    
def node2(input: OverallState):
    print("node2 overall state" , input)
    return {
        "property_control": "completed",
    }

def router(state: OverallState) -> Literal["node2", "END"]:
    print("Router function state:", state)
    if state["property_control"] == "route_to_node2":
        return "node2"
    else:
        return END

graph_builder = StateGraph(OverallState)
graph_builder.add_node("node1", node1)
graph_builder.add_node("intermediate", intermediate)
graph_builder.add_node("node2", node2)

graph_builder.add_edge(START, "node1")
graph_builder.add_edge("node1", "intermediate")
graph_builder.add_conditional_edges("intermediate", router, {
    "node2": "node2",
    END: END
})
graph_builder.add_edge("node2", END)

graph = graph_builder.compile()
result = graph.invoke({
    "property_in_input": "value",
    "property_not_in_input": "value",
    "property_control": "initial_value"
}) 

Output

node1 overall state {'property_in_input': 'value', 'property_not_in_input': 'value', 'property_control': 'initial_value'}
intermediate node with input state {'property_in_input': 'value'}
Router function state: {'property_control': 'route_to_end', 'property_in_input': 'value'}

The issue:

The intermediate node only sees property_in_input due to its InputState typing
The router loses access to property_not_in_input even though it's annotated with OverallState.

@AI091 commented on GitHub (Feb 27, 2025): The behavior that @mcbortolozzo describe is correct. See this example for how to reproduce easily. itsnot just an update issue, the routing function only sees as input, what the preceding node gave it. ```python from langgraph.graph import StateGraph, START, END from typing import TypedDict, Literal class OverallState(TypedDict): property_in_input: str property_not_in_input: str property_control: str class InputState(TypedDict): property_in_input: str def node1(input: OverallState): print("node1 overall state" , input) return { "property_control": "route_to_node2", } def intermediate(input: InputState): print("intermediate node with input state" , input) return { "property_control": "route_to_end", } def node2(input: OverallState): print("node2 overall state" , input) return { "property_control": "completed", } def router(state: OverallState) -> Literal["node2", "END"]: print("Router function state:", state) if state["property_control"] == "route_to_node2": return "node2" else: return END graph_builder = StateGraph(OverallState) graph_builder.add_node("node1", node1) graph_builder.add_node("intermediate", intermediate) graph_builder.add_node("node2", node2) graph_builder.add_edge(START, "node1") graph_builder.add_edge("node1", "intermediate") graph_builder.add_conditional_edges("intermediate", router, { "node2": "node2", END: END }) graph_builder.add_edge("node2", END) graph = graph_builder.compile() result = graph.invoke({ "property_in_input": "value", "property_not_in_input": "value", "property_control": "initial_value" }) ``` ### Output ``` node1 overall state {'property_in_input': 'value', 'property_not_in_input': 'value', 'property_control': 'initial_value'} intermediate node with input state {'property_in_input': 'value'} Router function state: {'property_control': 'route_to_end', 'property_in_input': 'value'} ``` ### The issue: The intermediate node only sees property_in_input due to its InputState typing The router loses access to property_not_in_input even though it's annotated with OverallState.
Author
Owner

@vbarda commented on GitHub (Mar 11, 2025):

@ahmed33033 @mcbortolozzo this is fixed in 0.3.6 - thanks for your patience! the input schema annotation in the conditional edge function is now respected and acts as an input "filter": if the input schema annotation is provided, the conditional edge function will read only the keys in the schema regardless of the graph state it receives. if you want it to read full graph state, simply remove schema annotation or use something like dict for annotation

@vbarda commented on GitHub (Mar 11, 2025): @ahmed33033 @mcbortolozzo this is fixed in 0.3.6 - thanks for your patience! the input schema annotation in the conditional edge function is now respected and acts as an input "filter": if the input schema annotation is provided, the conditional edge function will read only the keys in the schema regardless of the graph state it receives. if you want it to read full graph state, simply remove schema annotation or use something like dict for annotation
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#316