diff --git a/packages/workflow/src/workflow-data-proxy.ts b/packages/workflow/src/workflow-data-proxy.ts index 1e70d6d935f..2e4538e1647 100644 --- a/packages/workflow/src/workflow-data-proxy.ts +++ b/packages/workflow/src/workflow-data-proxy.ts @@ -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; diff --git a/packages/workflow/test/workflow-data-proxy.test.ts b/packages/workflow/test/workflow-data-proxy.test.ts index 4cac88b2c4f..23cb5d7561e 100644 --- a/packages/workflow/test/workflow-data-proxy.test.ts +++ b/packages/workflow/test/workflow-data-proxy.test.ts @@ -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'); + }); + }); });