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

  1. Restructure the loop so every outside connection targets the Split In Batches node and nothing else inside the cycle has an incoming external edge.
  2. 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.
  3. Merge or remove the extra external entry edges until exactly one entry remains, and verify that entry is the batch node.
  4. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/bb6170473c93fbaa. Report an issue: GitHub.