From 803ab421648171d8b7909694597e560244e1cefa Mon Sep 17 00:00:00 2001 From: mfsiega <93014743+mfsiega@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:20:33 +0100 Subject: [PATCH] fix(core): During partial execution don't include loop as start node if the loop isn't closed (#22555) Co-authored-by: Danny Martini --- .../__tests__/find-start-nodes.test.ts | 41 +++++++++++++++++++ .../find-start-nodes.ts | 10 ++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-start-nodes.test.ts b/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-start-nodes.test.ts index 30da6e563be..788cc7e6c8e 100644 --- a/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-start-nodes.test.ts +++ b/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-start-nodes.test.ts @@ -624,5 +624,46 @@ describe('findStartNodes', () => { expect(startNodes.size).toBe(1); expect(startNodes).toContainEqual(afterLoop); }); + + // done (empty) + // ┌────► + // ┌─────────┐1 ┌────┐1 │ + // │ trigger ┼──►loop┼─┬┘ ►► + // └─────────┘ └────┘ │ ┌────────┐ + // └─►│in loop │ + // └────────┘ + test('if a loop node does not actually form a loop in the graph, it uses loop output instead of done output', () => { + // ARRANGE + const trigger = createNodeData({ name: 'trigger' }); + const loop = createNodeData({ name: 'loop', type: 'n8n-nodes-base.splitInBatches' }); + const inLoop = createNodeData({ name: 'inLoop' }); + const graph = new DirectedGraph().addNodes(trigger, loop, inLoop).addConnections( + { from: trigger, to: loop }, + // Note: loop connects to inLoop via output 1, but there's no connection + // back to loop, so it's not actually a loop + { from: loop, outputIndex: 1, to: inLoop }, + ); + const runData: IRunData = { + [trigger.name]: [toITaskData([{ data: { name: 'trigger' } }])], + // The loop node has data on output 1 (the first output), but not on output 0 (done) + [loop.name]: [toITaskData([{ outputIndex: 1, data: { name: 'loop' } }])], + }; + + // ACT + const startNodes = findStartNodes({ + graph, + trigger, + destination: inLoop, + runData, + pinData: {}, + }); + + // ASSERT + // Because the loop node doesn't form an actual loop, it should check output 1 + // for run data (not output 0). Since output 1 has data, the loop node should + // not be a start node, and we should continue to inLoop. + expect(startNodes.size).toBe(1); + expect(startNodes).toContainEqual(inLoop); + }); }); }); diff --git a/packages/core/src/execution-engine/partial-execution-utils/find-start-nodes.ts b/packages/core/src/execution-engine/partial-execution-utils/find-start-nodes.ts index 7b9a5f424ff..cb4e1b11083 100644 --- a/packages/core/src/execution-engine/partial-execution-utils/find-start-nodes.ts +++ b/packages/core/src/execution-engine/partial-execution-utils/find-start-nodes.ts @@ -83,7 +83,11 @@ function findStartNodesRecursive( // last run -1, NodeConnectionTypes.Main, - 0, + // Although this is a Loop node, the graph may not actually have a loop here e.g., + // while the workflow is under development. If there's not a loop, we treat the loop + // node as a normal node and take the data from the first output at index 1. + // If there *is* a loop, we take the data from the `done` output at index 0. + isALoop(graph, current) ? 0 : 1, ); if (nodeRunData === null || nodeRunData.length === 0) { @@ -130,6 +134,10 @@ function findStartNodesRecursive( return startNodes; } +function isALoop(graph: DirectedGraph, node: INode): boolean { + return graph.getChildren(node).has(node); +} + /** * The start node is the node from which a partial execution starts. The start * node will be executed or re-executed.