langgraph icon indicating copy to clipboard operation
langgraph copied to clipboard

Inconsistency in API - `add_node` accepts objects but `add_edge` requires string literals for the same nodes.

Open emircetinmemis opened this issue 2 months ago • 4 comments

Checked other resources

  • [x] This is a bug, not a usage question. For questions, please use the LangChain Forum (https://forum.langchain.com/).
  • [x] I added a clear and detailed title that summarizes the issue.
  • [x] I read what a minimal reproducible example is (https://stackoverflow.com/help/minimal-reproducible-example).
  • [x] 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 typing import Annotated, List

from IPython.display import Image, display
from langchain_core.messages import AIMessage, BaseMessage
from langgraph.graph import END, START, StateGraph, add_messages
from pydantic import BaseModel, Field


class State(BaseModel):
    messages: Annotated[List[BaseMessage], add_messages] = Field(default_factory=list, description="Conversation history to keep track of.")


class ClassNode:
    def __call__(self, state: State) -> State:
        return {"messages": AIMessage("I'm fine, thank's, what about you?")}

    def __str__(self):
        return self.__class__.__name__


intent_classifier = ClassNode()

workflow = StateGraph(State)

# workflow.add_edge(START, str(intent_classifier)) # This works because already a string is being passed.
workflow.add_edge(START, intent_classifier)  # This does not work because an object is being passed, which is not welcomed.
workflow.add_node(intent_classifier)  # This works because, api can handle objects when adding nodes.

graph = workflow.compile()

display(Image(graph.get_graph(xray=True).draw_mermaid_png()))

for message in graph.invoke({"messages": "Hi there, how's it going?"}).get("messages"):
    message.pretty_print()

Error Message and Stack Trace (if applicable)

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[11], line 29
     26 workflow.add_edge(START, intent_classifier)  # This does not work because an object is being passed, which is not welcomed.
     27 workflow.add_node(intent_classifier)  # This works because, api can handle objects when adding nodes.
---> 29 graph = workflow.compile()
     31 display(Image(graph.get_graph(xray=True).draw_mermaid_png()))
     33 for message in graph.invoke({"messages": "Hi there, how's it going?"}).get("messages"):

File ~/Documents/GitHub/Opinsane/.venv/lib/python3.12/site-packages/langgraph/graph/state.py:836, in StateGraph.compile(self, checkpointer, cache, store, interrupt_before, interrupt_after, debug, name)
    833 interrupt_after = interrupt_after or []
    835 # validate the graph
--> 836 self.validate(
    837     interrupt=(
    838         (interrupt_before if interrupt_before != "*" else []) + interrupt_after
    839         if interrupt_after != "*"
    840         else []
    841     )
    842 )
    844 # prepare output channels
    845 output_channels = (
    846     "__root__"
    847     if len(self.schemas[self.output_schema]) == 1
   (...)    853     ]
    854 )

File ~/Documents/GitHub/Opinsane/.venv/lib/python3.12/site-packages/langgraph/graph/state.py:791, in StateGraph.validate(self, interrupt)
    789 for target in all_targets:
    790     if target not in self.nodes and target != END:
--> 791         raise ValueError(f"Found edge ending at unknown node `{target}`")
    792 # validate interrupts
    793 if interrupt:

ValueError: Found edge ending at unknown node `ClassNode`

Description

Description

There's an inconsistency in LangGraph's API that creates an awkward developer experience. The add_node method accepts objects (callables), but add_edge only accepts string literals for node names, even when referencing the exact same nodes.

The Problem

When working with node objects (which is a common pattern for encapsulation), we can pass the object itself to add_node, but we're forced to use str(node) when referencing the same node in add_edge. This creates unnecessary duplication and breaks the natural flow of the API.

What works:

  • workflow.add_node(node_object) - Accepts the object ✅

What doesn't work:

  • workflow.add_edge(START, node_object) - Throws TypeError: 'ClassNode' object is not iterable
  • workflow.add_edge(node_object, END) - Throws TypeError: 'ClassNode' object is not iterable

Current workaround:

  • Must use workflow.add_edge(START, str(node_object)) 🤷

Why This Matters

  1. Inconsistent API: If add_node can handle objects, add_edge should too
  2. Breaks encapsulation: Forces developers to manually convert objects to strings
  3. Error-prone: Mixing object references and string conversions increases the chance of mismatched node names
  4. Poor DX: Developers expect to use the same reference (the object) throughout the graph definition

Expected Behavior

add_edge should accept any object that implements __str__() and automatically convert it to a string internally, just like how add_node handles objects. This would allow:

workflow.add_node(intent_classifier)  # Works
workflow.add_edge(START, intent_classifier)  # Should work
workflow.add_edge(intent_classifier, END)  # Should work

Benefits of Fixing This

  • Consistent API: Same object can be used everywhere
  • Better encapsulation: Node name is defined once in the class via __str__
  • Cleaner code: No need for str() conversions scattered throughout
  • Type safety: Using object references reduces typos in string literals
  • Backward compatible: str("already_a_string") returns the same string, so existing code won't break

System Info

System Information

OS: Darwin OS Version: Darwin Kernel Version 24.6.0: Mon Jul 14 11:29:54 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8122 Python Version: 3.12.11 (main, Jul 8 2025, 20:41:49) [Clang 20.1.4 ]

Package Information

langchain_core: 1.0.2 langchain: 1.0.3 langchain_community: 0.4.1 langsmith: 0.4.38 langchain_classic: 1.0.0 langchain_openai: 1.0.1 langchain_text_splitters: 1.0.0 langgraph_sdk: 0.2.9

Optional packages not installed

langserve

Other Dependencies

aiohttp: 3.13.2 async-timeout: Installed. No version info available. claude-agent-sdk: Installed. No version info available. dataclasses-json: 0.6.7 httpx: 0.28.1 httpx-sse: 0.4.3 jsonpatch: 1.33 langchain-anthropic: Installed. No version info available. langchain-aws: 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-ollama: Installed. No version info available. langchain-perplexity: Installed. No version info available. langchain-together: Installed. No version info available. langchain-xai: Installed. No version info available. langgraph: 1.0.2 langsmith-pyo3: Installed. No version info available. numpy: 2.3.4 openai: 2.6.1 openai-agents: Installed. No version info available. opentelemetry-api: Installed. No version info available. opentelemetry-exporter-otlp-proto-http: Installed. No version info available. opentelemetry-sdk: Installed. No version info available. orjson: 3.11.4 packaging: 25.0 pydantic: 2.12.3 pydantic-settings: 2.11.0 pytest: Installed. No version info available. pyyaml: 6.0.3 PyYAML: 6.0.3 requests: 2.32.5 requests-toolbelt: 1.0.0 rich: 14.2.0 sqlalchemy: 2.0.44 SQLAlchemy: 2.0.44 tenacity: 9.1.2 tiktoken: 0.12.0 typing-extensions: 4.15.0 vcrpy: Installed. No version info available. zstandard: 0.25.0

emircetinmemis avatar Nov 01 '25 13:11 emircetinmemis

I'd like to work on this issue.

zhangzhefang-github avatar Nov 04 '25 00:11 zhangzhefang-github

I've implemented a fix for this issue and created PR #6391.

Summary:

  • Added _get_node_key() helper function to convert node objects to string keys
  • Updated add_edge() to accept str | list[str] | Any | list[Any]
  • Fully backward compatible - all existing string usage unchanged
  • Comprehensive tests added covering 5 scenarios (strings, objects, mixed usage, lists)

Testing:

  • ✅ All existing tests pass (19/19)
  • ✅ New test test_add_edge_with_objects added
  • ✅ Linting and formatting checks pass

The PR is ready for review!

zhangzhefang-github avatar Nov 04 '25 00:11 zhangzhefang-github

I've implemented a fix for this issue and created PR #6391.

Summary:

  • Added _get_node_key() helper function to convert node objects to string keys
  • Updated add_edge() to accept str | list[str] | Any | list[Any]
  • Fully backward compatible - all existing string usage unchanged
  • Comprehensive tests added covering 5 scenarios (strings, objects, mixed usage, lists)

Testing:

  • ✅ All existing tests pass (19/19)
  • ✅ New test test_add_edge_with_objects added
  • ✅ Linting and formatting checks pass

The PR is ready for review!

That look's solid, perfect.

Shall I close the issue?

emircetinmemis avatar Nov 04 '25 01:11 emircetinmemis

@emircetinmemis Thanks for the feedback!

I think we should wait for the maintainers to review and merge PR #6391 before closing this issue. Since the PR includes "Closes #6376", the issue will automatically close once merged.

This ensures the fix is properly reviewed and works as expected. 👍

zhangzhefang-github avatar Nov 04 '25 01:11 zhangzhefang-github