n8n-io/n8n · error · UnsupportedLoopEntryError
The loop formed by ${quote(memberNames)} must be entered onl
Error message
The loop formed by ${quote(memberNames)} must be entered only through its Split In Batches node, but its entry points are: ${quote(entryNames)}. What it means
Thrown by n8n's v1-to-v2 workflow converter when a cycle (strongly connected component) in the workflow graph is reached from the outside through zero or more than one node, or through a node that is not a Split In Batches node. The converter can only collapse a loop into a single back-edge when there is exactly one outside entry and that entry is the batch node; any other shape is ambiguous and rejected. It is a structural, design-time validation error raised during conversion, not at execution time.
Source
Thrown at packages/@n8n/node-engine-compatibility/src/v1-workflow-converter.ts:279
batchNodeIds: Set<string>,
namesById: Map<string, string>,
): string {
const memberSet = new Set(members);
const toNames = (ids: string[]) => ids.map((id) => namesById.get(id) ?? id);
const batchMembers = members.filter((id) => batchNodeIds.has(id));
if (batchMembers.length === 0) {
throw new UnsupportedCycleError(toNames(members));
}
const externalEntries = new Set<string>();
for (const edge of edges) {
if (memberSet.has(edge.to) && !memberSet.has(edge.from)) externalEntries.add(edge.to);
}
const entries = externalEntries.size > 0 ? [...externalEntries] : batchMembers;
if (entries.length !== 1 || !batchNodeIds.has(entries[0])) {
throw new UnsupportedLoopEntryError(toNames(members), toNames(entries));
}
return entries[0];
}
/**
* Splits the nodes into groups (strongly connected components): two nodes
* share a group when you can walk from one to the other and back along
* edges, through any number of nodes. A -> B -> C -> A puts all three in
* one group. A node on no such round trip is a group of one, so a group
* with more than one member, or with a self-loop, is a cycle.
* Textbook Tarjan's algorithm, best reviewed against a reference:
* https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
*/
private computeSccs(nodes: GraphNode[], outgoingByNode: Map<string, GraphEdge[]>): string[][] {
const indexById = new Map<string, number>();
const lowlinkById = new Map<string, number>();
const stack: string[] = [];View on GitHub (pinned to 5ac6606e81)
Solutions
- Restructure the loop so every outside connection targets the Split In Batches node and nothing else inside the cycle has an incoming external edge.
- If the loop currently has no batch node, add a Split In Batches node as the sole entry point and route the cycle through it.
- Merge or remove the extra external entry edges until exactly one entry remains, and verify that entry is the batch node.
- Run the conversion/validation in the editor to see the listed entryNames and memberNames, then remove the offending edge(s).
Example fix
// before: outside edges hit both 'Split In Batches' and 'Process' inside the loop // Trigger --> Split In Batches --> Process --> ... --> Split In Batches // Webhook --> Process // after: route Webhook into Split In Batches instead // Trigger --> Split In Batches --> Process --> ... --> Split In Batches // Webhook --> Split In Batches
Defensive patterns
Strategy: validation
Validate before calling
// Before converting, assert each cycle has exactly one external entry and it is the batch node.
function assertLoopEntry(members, edges, batchNodeIds) {
const memberSet = new Set(members);
const entries = new Set();
for (const e of edges) {
if (memberSet.has(e.to) && !memberSet.has(e.from)) entries.add(e.to);
}
const list = entries.size ? [...entries] : members.filter((id) => batchNodeIds.has(id));
return list.length === 1 && batchNodeIds.has(list[0]);
} Type guard
function isSingleBatchEntry(members, edges, batchNodeIds) {
const memberSet = new Set(members);
const external = edges.filter((e) => memberSet.has(e.to) && !memberSet.has(e.from));
if (external.length === 0) return members.filter((id) => batchNodeIds.has(id)).length === 1;
if (external.length !== 1) return false;
return batchNodeIds.has(external[0].to);
} Prevention
- Always route outside connections into the Split In Batches node, never into other nodes inside a loop.
- Validate workflow JSON with the converter in CI before importing.
- Keep at most one external entry per loop when authoring cyclic sub-workflows.
When it happens
Trigger: resolveSingleBatchEntry() computes externalEntries (edges whose `to` is in the cycle but `from` is not). If externalEntries is non-empty it is used as `entries`, otherwise the batch members inside the cycle are used. The error fires when entries.length !== 1 OR the single entry is not in batchNodeIds. Concretely: (a) two outside nodes both feed into the same loop, (b) an outside node feeds a non-batch node inside the loop, (c) the loop contains no Split In Batches node at all but has more than one batch candidate, or (d) the loop has multiple batch members and no external entry.
Common situations: Migrating legacy v1 workflows that used loops built around nodes other than Split In Batches; wiring a second trigger or a manual/conditional branch into an existing loop body; copying a loop sub-workflow that depended on an external edge for re-entry; refactoring a loop and accidentally adding a parallel path into one of its inner nodes.
Related errors
- Cannot decrease maxIterations when resuming a run. Expected
- Checkpoint for runId ${this.runId} has pending tool calls —
- Model ID is required
- Invalid model ID "${rawId}": expected "provider/model-name"
- Unsupported provider: "${provider}". Supported providers: ${
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/bb6170473c93fbaa.
Report an issue: GitHub.