Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Branch Conditions Limited to Preceding Node's Input Schema Instead of Full Graph State #3657

Open
4 tasks done
AI091 opened this issue Mar 3, 2025 · 6 comments
Open
4 tasks done
Labels
bug Something isn't working

Comments

@AI091
Copy link

AI091 commented Mar 3, 2025

Checked other resources

  • This is a bug, not a usage question. For questions, please use GitHub Discussions.
  • I added a clear and detailed title that summarizes the issue.
  • I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • I included a self-contained, minimal example that demonstrates the issue INCLUDING all the relevant imports. The code run AS IS to reproduce the issue.

Example Code

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, input=InputState)
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"
})

Error Message and Stack Trace (if applicable)

Description

Description

When using a branch condition in LangGraph, the branch condition function only has access to fields defined in the source node's input schema, not the full graph schema. This breaks the expected behavior where branch conditions should have access to the complete state.

Reproduction Example

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, input=InputState)
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"
}) 

Current Behavior

The router function only receives properties that are defined in the source node's input schema, plus any updates from that node. In this example, property_not_in_input is lost when the router is called, despite the router function expecting the complete OverallState.

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'}

Expected Behavior

The router function should receive the complete graph state (all properties in OverallState), regardless of what fields the source node uses or updates.

System Info

System Information

OS: Linux
OS Version: #26~22.04.1-Ubuntu SMP Thu Jul 11 22:33:04 UTC 2024
Python Version: 3.12.1 (main, Dec 12 2024, 22:30:56) [GCC 9.4.0]

Package Information

langchain_core: 0.3.40
langchain: 0.3.19
langsmith: 0.1.147
langchain_google_vertexai: 2.0.10
langchain_openai: 0.2.2
langchain_pinecone: 0.2.0
langchain_text_splitters: 0.3.6
langgraph_sdk: 0.1.53

Optional packages not installed

langserve

Other Dependencies

aiohttp: 3.9.5
aiohttp<4.0.0,>=3.8.3: Installed. No version info available.
anthropic[vertexai]: Installed. No version info available.
async-timeout<5.0.0,>=4.0.0;: Installed. No version info available.
google-cloud-aiplatform: 1.81.0
google-cloud-storage: 2.19.0
httpx: 0.27.2
httpx-sse: 0.4.0
jsonpatch<2.0,>=1.33: Installed. No version info available.
langchain-anthropic;: Installed. No version info available.
langchain-aws;: Installed. No version info available.
langchain-cohere;: Installed. No version info available.
langchain-community;: Installed. No version info available.
langchain-core<1.0.0,>=0.3.34: Installed. No version info available.
langchain-core<1.0.0,>=0.3.35: Installed. No version info available.
langchain-deepseek;: Installed. No version info available.
langchain-fireworks;: Installed. No version info available.
langchain-google-genai;: Installed. No version info available.
langchain-google-vertexai;: Installed. No version info available.
langchain-groq;: Installed. No version info available.
langchain-huggingface;: Installed. No version info available.
langchain-mistralai: Installed. No version info available.
langchain-mistralai;: Installed. No version info available.
langchain-ollama;: Installed. No version info available.
langchain-openai;: Installed. No version info available.
langchain-text-splitters<1.0.0,>=0.3.6: Installed. No version info available.
langchain-together;: Installed. No version info available.
langchain-xai;: Installed. No version info available.
langsmith-pyo3: Installed. No version info available.
langsmith<0.4,>=0.1.125: Installed. No version info available.
langsmith<0.4,>=0.1.17: Installed. No version info available.
numpy: 1.26.4
numpy<2,>=1.26.4;: Installed. No version info available.
numpy<3,>=1.26.2;: Installed. No version info available.
openai: 1.64.0
orjson: 3.10.15
packaging<25,>=23.2: Installed. No version info available.
pinecone-client: 5.0.1
pydantic: 2.10.6
pydantic<3.0.0,>=2.5.2;: Installed. No version info available.
pydantic<3.0.0,>=2.7.4: Installed. No version info available.
pydantic<3.0.0,>=2.7.4;: Installed. No version info available.
PyYAML>=5.3: Installed. No version info available.
requests: 2.32.3
requests-toolbelt: 1.0.0
requests<3,>=2: Installed. No version info available.
SQLAlchemy<3,>=1.4: Installed. No version info available.
tenacity!=8.4.0,<10,>=8.1.0: Installed. No version info available.
tenacity!=8.4.0,<10.0.0,>=8.1.0: Installed. No version info available.
tiktoken: 0.9.0
typing-extensions>=4.7: Installed. No version info available.

@YassinNouh21
Copy link
Contributor

@AI091

The Problem

In your code, the intermediate node uses InputState as its input schema, which only contains property_in_input. When the router function is called after the intermediate node, it only receives:

  1. The properties defined in InputState (property_in_input)
  2. Any updates made by the intermediate node (property_control)

But it's missing property_not_in_input which is part of the full OverallState.

Solution

you can work around it by ensuring your router function only depends on properties that are either:

  1. Included in the source node's input schema, or
  2. Updated by the source node

Here's a modified version of your code that works around the issue:

from libs.langgraph.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 
    property_not_in_input: str  # Added this to ensure it's available in the router

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, input=InputState)
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"
})

The key change is adding property_not_in_input to the InputState TypedDict to ensure it's available in the router function.

@AI091
Copy link
Author

AI091 commented Mar 3, 2025

Hi, @YassinNouh21 I understand and this quick fix is what I am currently doing but this bug could always result in unexpected behavior and hurts observability in langsmith.

@YassinNouh21
Copy link
Contributor

YassinNouh21 commented Mar 3, 2025

Hi, @YassinNouh21 I understand and this quick fix is what I am currently doing but this bug could always result in unexpected behavior and hurts observability in langsmith.

Yes it is more about workaround. But in my opinion, I think that u miss use the Input state as it supposes to be at the start node not in the intermediate nodes

@AI091
Copy link
Author

AI091 commented Mar 3, 2025

Yes it is more about workaround. But in my opinion, I think that u miss use the Input state as it supposes to be at the start node not in the intermediate nodes

Thanks for your thoughts!

Hmm, Input Nodes would act as a filter to what each node sees, drawing a line of separation on inputs required for each node. I find this idea helpful in long workflows beginning or amid the workflow and is essential for me in Langsmith instead of seeing key states from overall states I can see the relevant input to each node instead, so I am not sure why you think that.

But still applying it only at the start node and then having a router function after it that should use the full schema will have the same problem, so I don't think that's a relevant discussion.

@YassinNouh21
Copy link
Contributor

Yes it is more about workaround. But in my opinion, I think that u miss use the Input state as it supposes to be at the start node not in the intermediate nodes

Thanks for your thoughts!

Hmm, Input Nodes would act as a filter to what each node sees, drawing a line of separation on inputs required for each node. I find this idea helpful in long workflows beginning or amid the workflow and is essential for me in Langsmith instead of seeing key states from overall states I can see the relevant input to each node instead, so I am not sure why you think that.

But still applying it only at the start node and then having a router function after it that should use the full schema will have the same problem, so I don't think that's a relevant discussion.

I believe the issue is that InputState should only apply at the start node. Using it mid-workflow filters out important properties (like property_not_in_input), leaving the router with an incomplete state. This behavior not only breaks the expected full-state propagation but also hurts observability in LangSmith. Wouldn't it be better if we kept the full OverallState for intermediate nodes and router functions?

as you can see in this docs. it is explicity stated that

When distinct schemas are specified, an internal schema will still be used for communication between nodes. The input schema ensures that the provided input matches the expected structure, while the output schema filters the internal data to return only the relevant information according to the defined output schema.

@AI091
Copy link
Author

AI091 commented Mar 3, 2025

I believe the issue is that InputState should only apply at the start node. Using it mid-workflow filters out important properties (like property_not_in_input), leaving the router with an incomplete state. This behavior not only breaks the expected full-state propagation but also hurts observability in LangSmith.

Using input states doesn't inherently lose information, it's only when using router functions as stated in my issue, even in subsequent nodes the nodes still can access overallstate normally.
Trying out the same example by using normal edges would also keep information normal, so again a behavior specific to this issue and shouldn't be the expected behavior.

Wouldn't it be better if we kept the full OverallState for intermediate nodes and router functions?
For long workflows this isn't ideal for my use cases nor do I think its expected behavior.

as you can see in this docs. it is explicitly stated that
When distinct schemas are specified, an internal schema will still be used for communication between nodes. The input schema ensures that the provided input matches the expected structure, while the output schema filters the internal data to return only the relevant information according to the defined output schema.

This doesn't say that I shouldn't define private schemas amid the graph. Refer to the docs 1 , 2 here of using internal schemas in midst of graph, here called "private" states. this doesn't break propagation.

It is possible to have nodes write to private state channels inside the graph for internal node communication. We can simply define a private schema, PrivateState. See this notebook for more detail.

This causes problems for other people aswell, like here.
I added a separate issue for more clarity on the exact problem

@vbarda vbarda added the bug Something isn't working label Mar 3, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants