fix(core): During partial execution don't include loop as start node if the loop isn't closed (#22555)

Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
mfsiega
2025-12-04 18:20:33 +01:00
committed by GitHub
co-authored by Danny Martini
parent 3026a813b0
commit 803ab42164
2 changed files with 50 additions and 1 deletions
@@ -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);
});
});
});
@@ -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.