#38870·n8n

AI Agent tool responses (observation) intermittently return empty even though the tool executed successfully — toolWorkflow / toolCode + Agent (LangChain)

Author: bgramirez01Created Sep 16, 2026Updated Sep 17, 2026
Labelsteam:aistatus:in-linearstatus:team-assigned

Bug Description

n8n version: 2.31.4 (stable) Nodes: @n8n/n8n-nodes-langchain Agent (v3.1), toolWorkflow (v2.2), toolCode (v1.3), lmChatOpenAi (v1.3), memoryPostgresChat (v1.4) Model: gpt-4.1, temperature 0.2

Summary

An AI Agent node with 4 connected tools (3x toolWorkflow calling sub-workflows, 1x toolCode with inline JS) intermittently receives an EMPTY observation from a tool call, even though the tool's own execution completes successfully and returns the correct data. This causes the agent to either hallucinate plausible-but-fake values, or (with the right system prompt) honestly report "the tool didn't return anything" — both are bad outcomes for a production agent.

Evidence

With returnIntermediateSteps: true on the Agent node, a failing tool call looks like this in intermediateSteps:

"action": { "tool": "get_pricing_info", "toolInput": {...} },
"observation": "\"\""

Literally the 2-character string "" — not an error, not null, just an empty JSON string — while the SAME sub-workflow's own execution record (visible separately in n8n's execution log for that nested workflow) shows it computed and returned the correct object via its terminal node, e.g.:

{ "output": "{\"precio_a_mostrar_usd\":100, ...}" }

So the tool itself never fails. The corruption happens strictly in the bridge between the tool node's output and what the LangChain Agent's tool-calling loop reads as the observation.

What we ruled out (controlled reproduction, ~25 runs across two nearly-identical workflows)

We built a minimal replica workflow (same 4 tools, same sub-workflows, same LLM/credential) alongside the real one, and isolated variables one at a time. None of these reproduced the bug in the replica, even though the SAME exact payload reliably reproduced it on the real workflow:

  • Tool type (toolCode vs. simple toolWorkflow vs. complex toolWorkflow with a 25-node sub-workflow)
  • Calling 1 tool vs. calling all 4 sequentially in a single turn
  • Chat memory: fresh/clean session vs. reused session with prior turns
  • System prompt length/content (short generic prompt vs. the full ~4000-character production prompt)
  • Session key strategy: static vs. dynamic (per-user) session key on the Postgres chat memory node
  • Tool description field length/detail (short placeholder vs. full production description with examples)
  • An extra $fromAI-independent input field resolving to undefined on one of the toolWorkflow nodes

A possible clue: latency changes the reproduction rate

As a workaround, we added an await this.helpers.httpRequest(...) call (logging the real result to an external endpoint) right before each tool's return statement — adding roughly 100ms-1s of extra latency to each tool call before it returns.

Before this change: the bug reproduced in ~1 out of 7 test runs. After adding this latency: the bug reproduced in 5 out of 5 runs.

This is not proof, but it's a strong hint that this may be a race condition between the tool's execution finishing and the Agent's tool-calling loop reading the result — something that a few hundred extra milliseconds of latency inside the tool makes much more likely to lose.

Question

Has anyone seen this? Is there a known issue with the Agent v3 / toolWorkflow v2.2 combination around reading tool outputs, especially under any timing/latency pressure? We're currently mitigating in production by having each tool also log its raw result externally and a downstream node cross-check intermediateSteps for empty observations, substituting the real value when found — but obviously we'd rather fix the root cause.

Happy to share the full workflow JSON (sanitized) or run further tests if that would help narrow this down.

To Reproduce

  1. Create an AI Agent node (LangChain) with at least one connected tool of type toolWorkflow (or toolCode) that computes a real result and returns it via its terminal node.
  2. Enable returnIntermediateSteps: true on the Agent node.
  3. Trigger the agent with a prompt that causes it to call that tool.
  4. Repeat the same exact trigger multiple times (5-10+) — the bug is intermittent, not 100% reproducible on every single run in a simplified setup, though it appeared consistently on our production workflow. Adding ~100ms-1s of extra latency inside the tool (e.g. an awaited HTTP call before the tool's return statement) made it reproduce far more reliably (5/5 runs vs. ~1/7 without the added latency).
  5. Inspect the intermediateSteps array in the agent's execution result.

Expected behavior

The observation field for each tool call in intermediateSteps should always contain the tool's real output (e.g. {"precio_a_mostrar_usd":100, ...}).

Instead, it intermittently comes back as the literal empty string "", even though the tool's own sub-workflow execution (visible separately in n8n's execution log) shows it computed and returned the correct data via its terminal node. The corruption happens strictly in the bridge between the tool node's output and what the Agent's tool-calling loop reads as the observation.

Debug Info

Debug info

core

  • n8nVersion: 2.34.5
  • platform: docker (self-hosted)
  • nodeJsVersion: 24.18.0
  • nodeEnv: production
  • database: postgres
  • executionMode: regular
  • concurrency: -1
  • license: enterprise (production)
  • consumerId: bd29d3a5-266a-4729-bc60-9c491f3bb2c7

storage

  • success: all
  • error: all
  • progress: false
  • manual: true
  • binaryMode: filesystem

pruning

  • enabled: true
  • maxAge: 336 hours
  • maxCount: 10000 executions

client

  • userAgent: mozilla/5.0 (windows nt 10.0; win64; x64) applewebkit/537.36 (khtml, like gecko) chrome/153.0.0.0 safari/537.36 edg/153.0.0.0
  • isTouchDevice: false

cluster

  • instanceCount: 1
  • versions: 2.34.5
  • instances:
    • instanceKey: 24b0f278-55bd-4c90-bac8-718cb5ae4479, hostId: main-4422269654be, instanceType: main, instanceRole: leader, version: 2.34.5
  • checks:
    • check: hostid-clash, status: succeeded, warnings: -
    • check: lifecycle, status: succeeded, warnings: -
    • check: split-brain, status: succeeded, warnings: -
    • check: version-mismatch, status: succeeded, warnings: -

Generated at: 2026-09-16T16:37:46.158Z

Operating System

Linux (Docker, self-hosted)

n8n Version

2.34.5

Node.js Version

24.18.0

Database

PostgreSQL

Execution mode

main (default)

Hosting

self hosted