fix(core): Resolve $().item expressions in partial executions (#27338)

This commit is contained in:
Raúl Gómez Morales
2026-03-26 11:38:06 +01:00
committed by GitHub
parent c114e7ea59
commit d3e45bc126
2 changed files with 155 additions and 0 deletions
@@ -1229,6 +1229,24 @@ export class WorkflowDataProxy {
if (pinnedData) {
return that.returnExecutionData(pinnedData[itemIndex], resolveFullItem);
}
// The active node has not run yet (e.g. partial execution),
// so paired item resolution cannot trace through the chain.
// Fall back to reading directly from the referenced node's
// run data, which is what first()/last()/all() do.
const nodeRunData = that.getNodeExecutionOrPinnedData({
nodeName,
});
if (nodeRunData.length) {
// In the UI preview itemIndex is always 0, but guard against
// out-of-bounds access in case that assumption ever changes.
if (itemIndex >= nodeRunData.length) {
throw createExpressionError(
`"${nodeName}" node has ${nodeRunData.length} item(s) but expression references item ${itemIndex}`,
{ itemIndex },
);
}
return that.returnExecutionData(nodeRunData[itemIndex], resolveFullItem);
}
}
const executionData = that.connectionInputData;
@@ -1907,4 +1907,141 @@ describe('WorkflowDataProxy', () => {
expect(result).not.toHaveProperty('params');
});
});
describe('Partial execution: $() referencing executed node from unexecuted active node', () => {
// Scenario: Reference → Edit → NoOp → Edit Fields
// Only Reference and Edit have been executed (partial execution).
// Edit Fields (active, unexecuted) uses $('Edit').item.json.test
// "Edit" has data in runData, so the expression should resolve.
const workflowData: IWorkflowBase = {
id: '123',
name: 'partial execution test',
nodes: [
{
id: 'node1',
name: 'Reference',
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [256, 16] as [number, number],
parameters: {},
},
{
id: 'node2',
name: 'Edit',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [544, 16] as [number, number],
parameters: {
assignments: {
assignments: [
{
id: 'e8d2af0b-147a-4b0d-a106-5ed5a5b753e4',
name: 'test',
value: '={{ 1111 }}',
type: 'string',
},
],
},
options: {},
},
},
{
id: 'node3',
name: 'NoOp',
type: 'n8n-nodes-base.noOp',
typeVersion: 1,
position: [832, 32] as [number, number],
parameters: {},
},
{
id: 'node4',
name: 'Edit Fields',
type: 'n8n-nodes-base.set',
typeVersion: 3.4,
position: [1136, 0] as [number, number],
parameters: {},
},
],
connections: {
Reference: {
main: [[{ node: 'Edit', type: NodeConnectionTypes.Main, index: 0 }]],
},
Edit: {
main: [[{ node: 'NoOp', type: NodeConnectionTypes.Main, index: 0 }]],
},
NoOp: {
main: [[{ node: 'Edit Fields', type: NodeConnectionTypes.Main, index: 0 }]],
},
},
active: false,
activeVersionId: null,
isArchived: false,
createdAt: new Date(),
updatedAt: new Date(),
};
// Exact run data from a real partial execution (only Reference and Edit ran)
const run: IRun = {
data: createRunExecutionData({
resultData: {
runData: {
Reference: [
{
startTime: 1774006769741,
executionTime: 1,
executionIndex: 0,
executionStatus: 'success',
source: [],
data: {
main: [[{ json: {}, pairedItem: { item: 0 } }]],
},
},
],
Edit: [
{
startTime: 1774006769743,
executionTime: 8,
executionIndex: 1,
executionStatus: 'success',
source: [
{
previousNode: 'Reference',
previousNodeOutput: 0,
previousNodeRun: 0,
},
],
data: {
main: [[{ json: { test: '1111' }, pairedItem: { item: 0 } }]],
},
},
],
// NoOp and Edit Fields have NOT been executed
},
},
}),
mode: 'manual',
startedAt: new Date(),
status: 'success',
storedAt: 'db',
};
test('$("Edit").first() should return data when referenced node was executed', () => {
const proxy = getProxyFromFixture(workflowData, run, 'Edit Fields', 'manual');
expect(proxy.$('Edit').first().json.test).toBe('1111');
});
test('$("Edit").isExecuted should return true', () => {
const proxy = getProxyFromFixture(workflowData, run, 'Edit Fields', 'manual');
expect(proxy.$('Edit').isExecuted).toBe(true);
});
test('$("Edit").item.json.test should resolve when the referenced node was executed', () => {
// .item uses paired item resolution which requires connectionInputData
// (the active node's input). In a partial execution the active node hasn't
// run so connectionInputData is empty, but the referenced node "Edit" has
// data in runData. The fix falls back to reading from runData directly.
const proxy = getProxyFromFixture(workflowData, run, 'Edit Fields', 'manual');
expect(proxy.$('Edit').item.json.test).toBe('1111');
});
});
});