How to fix n8n nested loops: the inner loop retains completed state bug

The inner Loop Over Items node skips processing after the first outer iteration because it retains its completed state. Here are the three fixes, including the Done-branch chain pattern that avoids the bug entirely.

Title card: How to fix n8n nested loops inner loop retains completed state bug
The inner Loop Over Items node retains completed state across outer iterations - here is why and how to fix it.

TL;DR: n8n's inner Loop Over Items skips from the second outer iteration onward because nodeContext.done persists; fix by updating n8n (PR #23775), enabling Reset on the inner node, or using the Done-branch chain pattern.

This bug hits whenever two Loop Over Items nodes are nested - outer loop over a list of users, inner loop over each user's records, for example. The outer loop's first iteration works fine. From the second iteration onward, the inner loop fires, sees its completed state from before, and exits through Done immediately without processing any items. The Automation Error Index tracks this under the n8n loop category alongside other state-related bugs.

What causes the inner Loop Over Items to skip on subsequent outer iterations?

When a Loop Over Items node finishes processing its last batch, it sets two internal values: nodeContext.done = true and nodeContext.items = []. On the next invocation, the node checks whether to initialize fresh or continue from where it left off. The original condition was:

if (nodeContext.items === undefined || options.reset === true)

On the second outer iteration, nodeContext.items is not undefined - it's an empty array from the previous run. And unless Reset is manually enabled, options.reset is false. So the condition evaluates to false, the node enters its "continue processing" branch, finds an empty array, and routes everything directly to Done. No items are processed.

This was reported as GitHub issue #23670 ("Nested Loop Over Items: Inner loop skips processing on second outer iteration") and patched in PR #23775.

Two-column diagram showing why nested n8n loops fail: first iteration works normally, second iteration finds nodeContext.done=true and skips to Done immediately with zero items processed
On the second outer iteration, the inner loop sees nodeContext.done=true and routes directly to Done - PR #23775 fixed this by adding that check to the reset condition.

How do you fix nested Loop Over Items on an up-to-date n8n instance?

If your n8n instance includes the fix from PR #23775, the inner loop resets automatically when it encounters nodeContext.done = true at the start of a new outer iteration. The patched condition reads:

if (nodeContext.items === undefined || options.reset === true || nodeContext.done === true)

Verify your n8n version includes this fix by checking the changelog for a commit that references issue #23670 or PR #23775. Kept your instance current with npm update -g n8n or by pulling the latest Docker image - the patch has been in the main branch since the 1.x series. If the inner loop still skips on the second outer iteration after updating, fall back to one of the manual workarounds below.

How do you fix nested loops on older n8n versions?

Two workarounds cover instances that predate the fix.

Option 1: Enable Reset on the inner Loop Over Items node. Open the inner loop node's settings and turn on the Reset option. With Reset enabled, the node re-initializes its state every time it receives incoming data - which happens at the start of each outer iteration. The trade-off: Reset fires on every incoming item, not just at the start of a new outer batch. For most nested-loop patterns this is harmless, but test thoroughly if the inner loop receives data from a Merge node whose output timing is unpredictable.

Option 2: Restructure to the Done-branch chain pattern. This is n8n's intended approach for processing multiple datasets sequentially. Instead of placing the inner Loop Over Items node inside the outer loop's Loop branch, move it to the outer loop's Done branch. The outer loop processes dataset A completely; once done, the Done branch triggers the inner loop to process dataset B. This eliminates the state conflict entirely because only one loop is active at any moment.

The community phrasing from the n8n forum captures it well: "never put a loop node inside a loop node - only use another loop node on the Done branch, not on the Loop one." For a detailed walkthrough of how loop state and reset mechanics work, see why the n8n Loop node only processes the first item.

How do you implement the Done-branch chain pattern in n8n?

The pattern works like this for a two-level nested structure:

  1. Source data (outer dataset) connects to the outer Loop Over Items node.
  2. Outer loop's Loop output connects to the processing nodes for the outer dataset item.
  3. Outer processing nodes loop back to the outer loop's input (standard loop-back wire).
  4. Outer loop's Done output connects to the inner Loop Over Items node.
  5. Inner loop's Loop output connects to the inner processing nodes.
  6. Inner processing nodes loop back to the inner loop's input.
  7. Inner loop's Done output connects to the next stage of your workflow.

The outer loop runs to completion first, then the inner loop starts. This is sequential, not parallel - if you need parallel processing across both datasets simultaneously, n8n is not the right fit for that pattern without using sub-workflows.

Flow diagram of the Done-branch chain pattern: Source feeds Outer Loop, Loop output goes to outer processing with a loop-back wire, Done output feeds Inner Loop, Inner Loop's Loop output goes to inner processing with its own loop-back, Inner Loop's Done output goes to Next Step
The Done-branch chain keeps only one loop active at a time, eliminating the state-retention conflict on all n8n versions.

For workflows involving rate-limited APIs like Google Sheets quota rate-limit errors, add a Wait node between processing nodes and the loop-back wire in each loop independently.

FAQ

Why does the n8n inner loop skip on the second outer loop iteration?

The inner Loop Over Items node sets nodeContext.done = true after its first run. On the next outer iteration, the node's reset condition doesn't recognize this state as a signal to reinitialize, so it enters the processing path with an empty item list and routes everything to Done immediately. PR #23775 patched this by adding nodeContext.done === true to the reset condition.

Does enabling Reset on the inner loop fix the nested loop bug?

Yes, for most configurations. With Reset enabled, the inner loop re-initializes its state every time it receives new data. This catches the start of each new outer iteration. The edge case to watch: Reset fires on every incoming item, so if the inner loop receives several items at once from a Merge node, it resets on each one rather than just at the start of the batch.

Does n8n support true nested loops?

n8n supports nested loops with the Done-branch chain pattern, but not concurrent loop execution within the same branch. Placing one Loop Over Items node inside another's Loop branch causes the state conflict described here. The Done-branch approach - outer loop runs to completion, then Done triggers the inner loop - works reliably and is the documented pattern in the n8n community.

How do I know if my n8n version includes the fix for nested loops?

Check your n8n version's changelog for a reference to GitHub issue #23670 or PR #23775 ("Fix nested loop not processing subsequent parent iterations"). If the patch is included, the inner loop resets automatically when it sees a completed state at the start of a new outer iteration. If not, enable Reset on the inner loop or switch to the Done-branch chain pattern.

What is the Done-branch chain pattern in n8n?

Instead of nesting a second Loop Over Items node inside the first loop's Loop branch, you connect the second loop to the first loop's Done branch. The first loop runs to completion, then its Done output triggers the second loop to start. This ensures only one loop controller is active at a time, avoiding the state-retention bug entirely. It also makes the data flow easier to debug in the execution log.