fix(editor): Sort loop node outputs by execution order in setup panel (#27418)

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Svetoslav Dekov
2026-03-23 12:11:03 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent e04dddcbcc
commit efa5d02979
3 changed files with 92 additions and 3 deletions
@@ -487,6 +487,73 @@ describe('workflowUtils', () => {
]);
});
it('should process loop-body output before done output for loop nodes', () => {
// Trigger → Loop Over Items → (done) → Wait
// → (loop) → HTTP Request → (back to Loop)
const trigger = makeNode('Trigger', [0, 0], true);
const loop = makeNode('Loop', [100, 0]);
const wait = makeNode('Wait', [200, -100]);
const httpRequest = makeNode('HTTP Request', [200, 100]);
const connections = {
Trigger: { main: [[{ node: 'Loop', type: 'main' as const, index: 0 }]] },
Loop: {
main: [
// output 0 = "done"
[{ node: 'Wait', type: 'main' as const, index: 0 }],
// output 1 = "loop"
[{ node: 'HTTP Request', type: 'main' as const, index: 0 }],
],
},
'HTTP Request': { main: [[{ node: 'Loop', type: 'main' as const, index: 0 }]] },
};
const nodeTypes = { Loop: 'n8n-nodes-base.splitInBatches' };
const result = sortNodesByExecutionOrder(
[wait, httpRequest, loop, trigger],
connections,
{},
nodeTypes,
);
// Loop body (HTTP Request) should come before done branch (Wait)
expect(result.map((n) => n.node.name)).toEqual(['Trigger', 'Loop', 'HTTP Request', 'Wait']);
});
it('should handle loop node with multiple nodes in the loop body', () => {
// Trigger → Loop → (done) → Done Node
// → (loop) → Step1 → Step2 → (back to Loop)
const trigger = makeNode('Trigger', [0, 0], true);
const loop = makeNode('Loop', [100, 0]);
const doneNode = makeNode('Done', [200, -100]);
const step1 = makeNode('Step1', [200, 100]);
const step2 = makeNode('Step2', [300, 100]);
const connections = {
Trigger: { main: [[{ node: 'Loop', type: 'main' as const, index: 0 }]] },
Loop: {
main: [
[{ node: 'Done', type: 'main' as const, index: 0 }],
[{ node: 'Step1', type: 'main' as const, index: 0 }],
],
},
Step1: { main: [[{ node: 'Step2', type: 'main' as const, index: 0 }]] },
Step2: { main: [[{ node: 'Loop', type: 'main' as const, index: 0 }]] },
};
const nodeTypes = { Loop: 'n8n-nodes-base.splitInBatches' };
const result = sortNodesByExecutionOrder(
[doneNode, step2, step1, loop, trigger],
connections,
{},
nodeTypes,
);
expect(result.map((n) => n.node.name)).toEqual(['Trigger', 'Loop', 'Step1', 'Step2', 'Done']);
});
it('should not follow main connections from connectionsByDestinationNode', () => {
const trigger = makeNode('Trigger', [0, 0], true);
const nodeA = makeNode('A', [100, 0]);
@@ -1,6 +1,7 @@
import type { IWorkflowDb, INodeUi } from '@/Interface';
import type { ITag } from '@n8n/rest-api-client/api/tags';
import type { IConnections } from 'n8n-workflow';
import { SPLIT_IN_BATCHES_NODE_TYPE } from '@/app/constants';
/**
* Converts workflow tags from ITag[] (API response format) to string[] (store format)
@@ -66,6 +67,7 @@ export function sortNodesByExecutionOrder<T extends ExecutionOrderItem>(
nodes: T[],
connectionsBySourceNode: IConnections,
connectionsByDestinationNode: IConnections = {},
nodeTypes: Record<string, string> = {},
): T[] {
const triggers = nodes
.filter((item) => item.isTrigger)
@@ -118,9 +120,23 @@ export function sortNodesByExecutionOrder<T extends ExecutionOrderItem>(
// main chain out of order when sub-nodes are shared across multiple agents.
const sourceConns = connectionsBySourceNode[name];
if (sourceConns?.main) {
for (const outputs of sourceConns.main) {
for (const conn of outputs ?? []) {
dfs(conn.node);
const mainOutputs = sourceConns.main;
// Loop Over Items (SplitInBatches) V3: output 0 = "done", output 1 = "loop".
// The execution engine runs the loop body first (repeatedly) and only sends
// data to "done" after all iterations complete. Process output 1 before
// output 0 so the sorted order matches execution order.
if (nodeTypes[name] === SPLIT_IN_BATCHES_NODE_TYPE && mainOutputs.length > 1) {
for (const idx of [1, 0]) {
for (const conn of mainOutputs[idx] ?? []) {
dfs(conn.node);
}
}
} else {
for (const outputs of mainOutputs) {
for (const conn of outputs ?? []) {
dfs(conn.node);
}
}
}
}
@@ -336,10 +336,16 @@ export const useWorkflowSetupState = (
nodeHasTemplateParams(node.name),
);
const allNodeTypes: Record<string, string> = {};
for (const node of sourceNodes.value) {
allNodeTypes[node.name] = node.type;
}
return sortNodesByExecutionOrder(
nodesForSetup,
workflowsStore.connectionsBySourceNode,
workflowsStore.connectionsByDestinationNode,
allNodeTypes,
);
});