Returning state in nodes won't update it #456

Closed
opened 2026-02-20 17:40:13 -05:00 by yindo · 8 comments
Owner

Originally created by @NathanAP on GitHub (Feb 13, 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

class MyNode:
   
   def action(state: MyState):
      # some cool stuff
      state.my_value = 123
      return state
   
   def should_continue(state: MyState):
      print(state.my_value) # this returns default value (in my case is None)
      # some cool logic to continue or not
      return "END"

Description

Hi, I've been using latest version forever, but since this morning I've being dealing with the fact that returning state in my nodes won't update it. When I rollback to 0.2.71 it worked fine. Did something change since .71?

Sorry if example code is not really helpful.

edit1: my versions were wrong;
edit2: looking better, the problem seems related to Lists, Dicts and BaseModels. It wont't update those for me, here are some print I've made:

agent=None settings=None database=None user=User(id=UUID('89fe6ec9-a318-4c5c-8236-e870f7fa4e6f'), status=True, full_name='nathan', email="nathan@example.com"

System Info

System Information

OS: Linux
OS Version: #1 SMP PREEMPT_DYNAMIC Debian 6.1.106-3 (2024-08-26)
Python Version: 3.12.1 (main, Dec 12 2024, 22:30:56) [GCC 9.4.0]

Package Information

langchain_core: 0.3.33
langchain: 0.3.17
langchain_community: 0.3.16
langsmith: 0.3.4
langchain_openai: 0.3.3
langchain_text_splitters: 0.3.5
langgraph_sdk: 0.1.43

Optional packages not installed

langserve

Other Dependencies

aiohttp: 3.10.10
async-timeout: Installed. No version info available.
dataclasses-json: 0.6.7
httpx: 0.27.2
httpx-sse: 0.4.0
jsonpatch: 1.33
langsmith-pyo3: Installed. No version info available.
numpy: 1.26.4
openai: 1.59.7
orjson: 3.10.11
packaging: 24.1
pydantic: 2.10.6
pydantic-settings: 2.6.1
pytest: Installed. No version info available.
PyYAML: 6.0.2
requests: 2.32.3
requests-toolbelt: 1.0.0
rich: Installed. No version info available.
SQLAlchemy: 2.0.35
tenacity: 9.0.0
tiktoken: 0.8.0
typing-extensions: 4.12.2
zstandard: 0.23.0

Originally created by @NathanAP on GitHub (Feb 13, 2025). ### Checked other resources - [x] This is a bug, not a usage question. For questions, please use GitHub Discussions. - [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 ```python class MyNode: def action(state: MyState): # some cool stuff state.my_value = 123 return state def should_continue(state: MyState): print(state.my_value) # this returns default value (in my case is None) # some cool logic to continue or not return "END" ``` ### Description Hi, I've been using `latest` version forever, but since this morning I've being dealing with the fact that returning `state` in my nodes won't update it. When I rollback to `0.2.71` it worked fine. Did something change since `.71`? Sorry if example code is not really helpful. edit1: my versions were wrong; edit2: looking better, the problem seems related to Lists, Dicts and BaseModels. It wont't update those for me, here are some `print` I've made: ``` agent=None settings=None database=None user=User(id=UUID('89fe6ec9-a318-4c5c-8236-e870f7fa4e6f'), status=True, full_name='nathan', email="nathan@example.com" ``` ### System Info System Information ------------------ > OS: Linux > OS Version: #1 SMP PREEMPT_DYNAMIC Debian 6.1.106-3 (2024-08-26) > Python Version: 3.12.1 (main, Dec 12 2024, 22:30:56) [GCC 9.4.0] Package Information ------------------- > langchain_core: 0.3.33 > langchain: 0.3.17 > langchain_community: 0.3.16 > langsmith: 0.3.4 > langchain_openai: 0.3.3 > langchain_text_splitters: 0.3.5 > langgraph_sdk: 0.1.43 Optional packages not installed ------------------------------- > langserve Other Dependencies ------------------ > aiohttp: 3.10.10 > async-timeout: Installed. No version info available. > dataclasses-json: 0.6.7 > httpx: 0.27.2 > httpx-sse: 0.4.0 > jsonpatch: 1.33 > langsmith-pyo3: Installed. No version info available. > numpy: 1.26.4 > openai: 1.59.7 > orjson: 3.10.11 > packaging: 24.1 > pydantic: 2.10.6 > pydantic-settings: 2.6.1 > pytest: Installed. No version info available. > PyYAML: 6.0.2 > requests: 2.32.3 > requests-toolbelt: 1.0.0 > rich: Installed. No version info available. > SQLAlchemy: 2.0.35 > tenacity: 9.0.0 > tiktoken: 0.8.0 > typing-extensions: 4.12.2 > zstandard: 0.23.0
yindo added the question label 2026-02-20 17:40:13 -05:00
yindo closed this issue 2026-02-20 17:40:13 -05:00
Author
Owner

@vbarda commented on GitHub (Feb 13, 2025):

@NathanAP you should not be mutating state inside your nodes, the updates should be done by returning the dictionaries / pydantic objects from the node functions

@vbarda commented on GitHub (Feb 13, 2025): @NathanAP you should not be mutating state inside your nodes, the updates should be done by returning the dictionaries / pydantic objects from the node functions
Author
Owner

@vbarda commented on GitHub (Feb 13, 2025):

If this is still not working in the latest version, let me know

@vbarda commented on GitHub (Feb 13, 2025): If this is still not working in the latest version, let me know
Author
Owner

@NathanAP commented on GitHub (Feb 13, 2025):

Hmmmm, I think I always did the wrong way then 😅 I always made it like this:

class MyState(BaseModel):
  username: str
  password: str
  auth_token: str | None = None

class LoginAnUserNode:
  def action(self, state: MyState):
    state.auth_token = login_an_user(state.username, state.password)

    return state

then I could be doing something like this, right?

class MyState(BaseModel):
  username: str
  password: str
  auth_token: str | None = None

class LoginAnUserNode:
  def action(self, state: MyState):
    auth_token = login_user(state.username, state.password)

    return state.model_copy(
      update={"auth_token": auth_token}
    )
@NathanAP commented on GitHub (Feb 13, 2025): Hmmmm, I think I always did the wrong way then 😅 I always made it like this: ```python class MyState(BaseModel): username: str password: str auth_token: str | None = None class LoginAnUserNode: def action(self, state: MyState): state.auth_token = login_an_user(state.username, state.password) return state ``` then I could be doing something like this, right? ```python class MyState(BaseModel): username: str password: str auth_token: str | None = None class LoginAnUserNode: def action(self, state: MyState): auth_token = login_user(state.username, state.password) return state.model_copy( update={"auth_token": auth_token} ) ```
Author
Owner

@vbarda commented on GitHub (Feb 13, 2025):

you don't even need a copy, you can just do

def action(self, state: MyState):
    auth_token = login_user(state.username, state.password)
    return {"auth_token": auth_token}

we will automatically coerce to pydantic model under the hood. let me know if this works

@vbarda commented on GitHub (Feb 13, 2025): you don't even need a copy, you can just do ```python def action(self, state: MyState): auth_token = login_user(state.username, state.password) return {"auth_token": auth_token} ``` we will automatically coerce to pydantic model under the hood. let me know if this works
Author
Owner

@NathanAP commented on GitHub (Feb 13, 2025):

It worked, thanks ❤

@NathanAP commented on GitHub (Feb 13, 2025): It worked, thanks ❤
Author
Owner

@EricZheng1024 commented on GitHub (Jul 3, 2025):

I think state is also a dictionary, so modifying and returning it may be fine. For example, (suppose that there are many keys in state)

state["value"] = 123; return state

is equivalent to

return {"value": 123}

That's how I'm doing it now, and it hasn't caused any issues. (I'm new to LangChain; I may have overlooked some details.)

@EricZheng1024 commented on GitHub (Jul 3, 2025): I think `state` is also a dictionary, so modifying and returning it may be fine. For example, (suppose that there are many keys in `state`) ```python state["value"] = 123; return state ``` is equivalent to ```python return {"value": 123} ``` That's how I'm doing it now, and it hasn't caused any issues. (I'm new to LangChain; I may have overlooked some details.)
Author
Owner

@NathanAP commented on GitHub (Jul 4, 2025):

Yes!
I guess my problem is that I always set everything to Pydantic to guardrail my entire code, but since states are stored as Dict, turns out that working with TypedDict is WAY easier.

Right now I'm treating everything as Pydantic in my code, but when I'm transitioning between nodes/store/checkpoint I always transform it to Dict, then transform it back again to my Pydantic class. It a bit annoying to do it, but it let me control my code way easier too. Much better than work just with dicts.

@NathanAP commented on GitHub (Jul 4, 2025): Yes! I guess my problem is that I always set everything to Pydantic to guardrail my entire code, but since states are stored as Dict, turns out that working with TypedDict is WAY easier. Right now I'm treating everything as Pydantic in my code, but when I'm transitioning between nodes/store/checkpoint I always transform it to Dict, then transform it back again to my Pydantic class. It a bit annoying to do it, but it let me control my code way easier too. Much better than work just with dicts.
Author
Owner

@saireddy-tk commented on GitHub (Jul 16, 2025):

you don't even need a copy, you can just do

def action(self, state: MyState):
auth_token = login_user(state.username, state.password)
return {"auth_token": auth_token}
we will automatically coerce to pydantic model under the hood. let me know if this works

This is an example using Node.js. Previously, I used to update the state in Node, which sometimes caused only some states to update while others did not. However, after following your approach, it worked. Thank you. @vbarda

class Prompt extends BaseNode {
  constructor(config) {
    super(config)
    this.validateConfig()
  }

  validateConfig() {
    if (!this.config.prompt) throw new ValidationError(`Prompt node ${this.id} must have a prompt`)
    if (!this.config.input?.validations) throw new ValidationError(`Prompt node ${this.id} must have input validations`)
    if (!this.config.output?.key) throw new ValidationError(`Prompt node ${this.id} must have output key`)
  }

  /**
   * Execute the prompt
   * @param {Object} state - The state object
   * @returns {Object} The state object
   */
  async execute(state) {
    logger.info(`Executing prompt: ${this.id}`)
    try {
      const inputs = Object.keys(this.config.input.validations || {}).reduce((acc, key) => ({
        ...acc,
        [key]: this.getValue(key, state),
      }), {})

      this.validateInputs(inputs)

      const formattedPrompt = this.formatPrompt(this.config.prompt, inputs)
      const output = {}
      output[this.config.output.key] = formattedPrompt
      return output
    } catch (error) {
      logger.error(`Error executing prompt: ${this.id}`, error)
      errorMessages = [...(state.errorMessages || []), { error: error.message, prompt: this.id }]
      throw new WorkflowError(`Prompt execution failed: ${error.message}`, this.id, error)
    }
  }
}
@saireddy-tk commented on GitHub (Jul 16, 2025): > you don't even need a copy, you can just do > > def action(self, state: MyState): > auth_token = login_user(state.username, state.password) > return {"auth_token": auth_token} > we will automatically coerce to pydantic model under the hood. let me know if this works This is an example using Node.js. Previously, I used to update the state in Node, which sometimes caused only some states to update while others did not. However, after following your approach, it worked. Thank you. @vbarda ``` class Prompt extends BaseNode { constructor(config) { super(config) this.validateConfig() } validateConfig() { if (!this.config.prompt) throw new ValidationError(`Prompt node ${this.id} must have a prompt`) if (!this.config.input?.validations) throw new ValidationError(`Prompt node ${this.id} must have input validations`) if (!this.config.output?.key) throw new ValidationError(`Prompt node ${this.id} must have output key`) } /** * Execute the prompt * @param {Object} state - The state object * @returns {Object} The state object */ async execute(state) { logger.info(`Executing prompt: ${this.id}`) try { const inputs = Object.keys(this.config.input.validations || {}).reduce((acc, key) => ({ ...acc, [key]: this.getValue(key, state), }), {}) this.validateInputs(inputs) const formattedPrompt = this.formatPrompt(this.config.prompt, inputs) const output = {} output[this.config.output.key] = formattedPrompt return output } catch (error) { logger.error(`Error executing prompt: ${this.id}`, error) errorMessages = [...(state.errorMessages || []), { error: error.message, prompt: this.id }] throw new WorkflowError(`Prompt execution failed: ${error.message}`, this.id, error) } } } ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: langchain-ai/langgraph#456