Bug Description
When an Execute Workflow node returns items from a sub-workflow, the sub-workflow's internal pairedItem metadata can pass through to the parent unchanged. If the sub-workflow's final node was a multi-input node (e.g. Merge) and the item came through input index ≥ 1, the parent receives an item with pairedItem: { item: 0, input: 1 } — but the Execute Workflow node only has one input.
Any downstream expression that uses paired-item resolution across that node (e.g. $('Webhook').item.json.field) then silently resolves to null. No expression error, no failed node, no log line — the node runs "successfully" with an empty value, and the failure surfaces much later (in our case as a confusing 400 from an external API that received a parameter with no value).
The behavior is intermittent in real workflows because it depends on which input of the sub-workflow's final Merge produced the winning item on that particular run — which makes it very hard to debug from the outside.
Three separate issues stack up to produce the silent null (all verified on current master, 5c20436):
-
Sub-workflow pairedItem passthrough — packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts (~L462–474): when getExecutionDataById returns no run data (e.g. sub-workflow has "save successful executions" disabled) or findPairedItemThroughWorkflowData returns undefined, the item keeps its sub-internal pairedItem verbatim, exporting an input index that is out of range in the parent context.
-
Paired-item resolver throws a raw TypeError instead of an ExpressionError — packages/workflow/src/workflow-data-proxy.ts, getPairedItem: candidates whose input exceeds sourceArray.length are dropped via return [] inside the flatMap, leaving results empty. Then:
if (results.every((result) => !result.ok)) {
throw results[0].error; // results is [] → results[0] is undefined
}
[].every(...) is vacuously true, so this line throws TypeError: Cannot read properties of undefined (reading 'error') instead of one of the intended typed errors (verified by wrapping the expression in a try/catch inside the expression itself).
-
The expression engine swallows unknown errors — packages/workflow/src/expression.ts, renderExpression: the catch block rethrows only ExpressionError/ExpressionExtensionError/SyntaxError (plus a frontend-only TypeError case). A backend TypeError falls through and the function does return null, so the expression "succeeds" with null and the node keeps executing with an empty parameter value.
Any one of these being fixed would turn the silent wrong value into a visible, diagnosable error.
To Reproduce
Minimal setup — two workflows.
Sub-workflow (note: last node is a Merge fed only via input 2 / index 1; "save successful executions" set to Do not save so the pairedItem remap in Execute Workflow has no run data to work with):
{
"name": "repro-sub",
"nodes": [
{ "id": "n1", "name": "Start", "type": "n8n-nodes-base.executeWorkflowTrigger", "typeVersion": 1.1, "position": [0, 0], "parameters": { "inputSource": "passthrough" } },
{ "id": "n2", "name": "Merge", "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [220, 0], "parameters": {} }
],
"connections": { "Start": { "main": [[ { "node": "Merge", "type": "main", "index": 1 } ]] } },
"settings": { "executionOrder": "v1", "saveDataSuccessExecution": "none" }
}
Parent workflow (a Set node named Webhook stands in for a real webhook; the consumer node uses the paired-item expression):
{
"name": "repro-parent",
"nodes": [
{ "id": "p1", "name": "When clicking Test", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [0, 0], "parameters": {} },
{ "id": "p2", "name": "Webhook", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [200, 0], "parameters": { "assignments": { "assignments": [ { "id": "a1", "name": "webhookUrl", "type": "string", "value": "https://example.com/webhook/abc" } ] }, "options": {} } },
{ "id": "p3", "name": "CallSub", "type": "n8n-nodes-base.executeWorkflow", "typeVersion": 1.2, "position": [420, 0], "parameters": { "source": "database", "workflowId": { "__rl": true, "value": "<repro-sub id>", "mode": "id" }, "options": {} } },
{ "id": "p4", "name": "Consumer", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [640, 0], "parameters": { "assignments": { "assignments": [ { "id": "b1", "name": "host", "type": "string", "value": "={{ $('Webhook').item.json.webhookUrl.includes('staging') ? 'https://staging.example.com' : 'https://prod.example.com' }}" } ] }, "options": {} } }
],
"connections": {
"When clicking Test": { "main": [[ { "node": "Webhook", "type": "main", "index": 0 } ]] },
"Webhook": { "main": [[ { "node": "CallSub", "type": "main", "index": 0 } ]] },
"CallSub": { "main": [[ { "node": "Consumer", "type": "main", "index": 0 } ]] }
},
"settings": { "executionOrder": "v1" }
}
- Import both workflows, point
CallSub at the sub-workflow.
- Execute the parent.
- Inspect the output:
CallSub output item: pairedItem: { "item": 0, "input": 1 }
Consumer output: { "host": null } — execution status: success, no error anywhere.
Control: change the sub-workflow's single connection from Merge input 1 to input 0 and run again — Consumer now outputs { "host": "https://prod.example.com" }. The one-line change toggles the bug.
Expected behavior
Either the paired-item chain resolves correctly (ideally Execute Workflow remaps or strips sub-internal pairedItem so parent-side metadata is always valid), or — at minimum — the expression fails loudly with a proper ExpressionError ("no path back to node …"), failing the node so the wrong value never propagates. An expression should never silently produce null because of internal metadata corruption.
Environment
- n8n Version: reproduced on
master @ 5c20436 (2.30.0 dev); first observed on a self-hosted 1.x deployment in queue mode
- Node.js Version: 24.7.0
- Database: SQLite (repro), PostgreSQL (original occurrence)
- Execution mode: regular (repro), queue (original occurrence)
- Operating System: macOS 15 (repro), Linux/Kubernetes (original occurrence)
Bug Description
When an Execute Workflow node returns items from a sub-workflow, the sub-workflow's internal
pairedItemmetadata can pass through to the parent unchanged. If the sub-workflow's final node was a multi-input node (e.g. Merge) and the item came through input index ≥ 1, the parent receives an item withpairedItem: { item: 0, input: 1 }— but the Execute Workflow node only has one input.Any downstream expression that uses paired-item resolution across that node (e.g.
$('Webhook').item.json.field) then silently resolves tonull. No expression error, no failed node, no log line — the node runs "successfully" with an empty value, and the failure surfaces much later (in our case as a confusing 400 from an external API that received a parameter with no value).The behavior is intermittent in real workflows because it depends on which input of the sub-workflow's final Merge produced the winning item on that particular run — which makes it very hard to debug from the outside.
Three separate issues stack up to produce the silent null (all verified on current
master, 5c20436):Sub-workflow
pairedItempassthrough —packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow/ExecuteWorkflow.node.ts(~L462–474): whengetExecutionDataByIdreturns no run data (e.g. sub-workflow has "save successful executions" disabled) orfindPairedItemThroughWorkflowDatareturnsundefined, the item keeps its sub-internalpairedItemverbatim, exporting an input index that is out of range in the parent context.Paired-item resolver throws a raw
TypeErrorinstead of anExpressionError—packages/workflow/src/workflow-data-proxy.ts,getPairedItem: candidates whoseinputexceedssourceArray.lengthare dropped viareturn []inside theflatMap, leavingresultsempty. Then:[].every(...)is vacuouslytrue, so this line throwsTypeError: Cannot read properties of undefined (reading 'error')instead of one of the intended typed errors (verified by wrapping the expression in a try/catch inside the expression itself).The expression engine swallows unknown errors —
packages/workflow/src/expression.ts,renderExpression: the catch block rethrows onlyExpressionError/ExpressionExtensionError/SyntaxError(plus a frontend-onlyTypeErrorcase). A backendTypeErrorfalls through and the function doesreturn null, so the expression "succeeds" withnulland the node keeps executing with an empty parameter value.Any one of these being fixed would turn the silent wrong value into a visible, diagnosable error.
To Reproduce
Minimal setup — two workflows.
Sub-workflow (note: last node is a Merge fed only via input 2 / index 1; "save successful executions" set to Do not save so the pairedItem remap in Execute Workflow has no run data to work with):
{ "name": "repro-sub", "nodes": [ { "id": "n1", "name": "Start", "type": "n8n-nodes-base.executeWorkflowTrigger", "typeVersion": 1.1, "position": [0, 0], "parameters": { "inputSource": "passthrough" } }, { "id": "n2", "name": "Merge", "type": "n8n-nodes-base.merge", "typeVersion": 3.2, "position": [220, 0], "parameters": {} } ], "connections": { "Start": { "main": [[ { "node": "Merge", "type": "main", "index": 1 } ]] } }, "settings": { "executionOrder": "v1", "saveDataSuccessExecution": "none" } }Parent workflow (a Set node named
Webhookstands in for a real webhook; the consumer node uses the paired-item expression):{ "name": "repro-parent", "nodes": [ { "id": "p1", "name": "When clicking Test", "type": "n8n-nodes-base.manualTrigger", "typeVersion": 1, "position": [0, 0], "parameters": {} }, { "id": "p2", "name": "Webhook", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [200, 0], "parameters": { "assignments": { "assignments": [ { "id": "a1", "name": "webhookUrl", "type": "string", "value": "https://example.com/webhook/abc" } ] }, "options": {} } }, { "id": "p3", "name": "CallSub", "type": "n8n-nodes-base.executeWorkflow", "typeVersion": 1.2, "position": [420, 0], "parameters": { "source": "database", "workflowId": { "__rl": true, "value": "<repro-sub id>", "mode": "id" }, "options": {} } }, { "id": "p4", "name": "Consumer", "type": "n8n-nodes-base.set", "typeVersion": 3.4, "position": [640, 0], "parameters": { "assignments": { "assignments": [ { "id": "b1", "name": "host", "type": "string", "value": "={{ $('Webhook').item.json.webhookUrl.includes('staging') ? 'https://staging.example.com' : 'https://prod.example.com' }}" } ] }, "options": {} } } ], "connections": { "When clicking Test": { "main": [[ { "node": "Webhook", "type": "main", "index": 0 } ]] }, "Webhook": { "main": [[ { "node": "CallSub", "type": "main", "index": 0 } ]] }, "CallSub": { "main": [[ { "node": "Consumer", "type": "main", "index": 0 } ]] } }, "settings": { "executionOrder": "v1" } }CallSubat the sub-workflow.CallSuboutput item:pairedItem: { "item": 0, "input": 1 }Consumeroutput:{ "host": null }— execution status: success, no error anywhere.Control: change the sub-workflow's single connection from Merge input
1to input0and run again —Consumernow outputs{ "host": "https://prod.example.com" }. The one-line change toggles the bug.Expected behavior
Either the paired-item chain resolves correctly (ideally Execute Workflow remaps or strips sub-internal
pairedItemso parent-side metadata is always valid), or — at minimum — the expression fails loudly with a properExpressionError("no path back to node …"), failing the node so the wrong value never propagates. An expression should never silently producenullbecause of internal metadata corruption.Environment
master@ 5c20436 (2.30.0 dev); first observed on a self-hosted 1.x deployment in queue mode