n8n Lesson 5: Why Does This Node Run Three Times, Not Once?
š Full written solution: https://interview-kit-fe.vercel.app/n8n-lesson-05-understanding-the-data-structure-items š» Code on GitHub: https://github.com/timepasshub2539-hue/leetcode-python/tree/main/n8n-lesson-05-understanding-the-data-structure-items Chapters: 0:00 One Invisible Thing Runs This Workflow 3 Times 0:08 Hey, I'm Kai 0:23 You Don't Need to Be a Dev 0:34 Meet the Items Array 0:46 What's Inside One Item 0:58 Where To Actually See This 1:08 Three Items, Three Rows 1:20 Follow One Field Through Three Nodes 1:36 Two Tabs, Two Stories 1:51 The Trap: Assuming One Item 2:07 Predict This 2:19 In Short 2:31 Read Any Node Like a Detective 2:47 Same Shape, Every Node You don't need to be a dev to understand how data actually moves through n8n. This lesson breaks down the items array ā the JSON structure every node passes to the next ā and shows why a workflow with 3 items silently runs a step 3 times. You'll follow one field through three connected nodes, compare the Table view and JSON view of the same data, and learn to predict node behavior before you run it. Includes the #1 trap beginners hit: assuming there's only ever one item. #n8n #automation #nocode #JSON #workflowautomation Watch next: - Python Lesson 22: @dataclass Cuts 15 Lines to 2 ā Here's the Trick: https://youtu.be/aVMY_xACXL0 - N8N Lesson 3: Save vs Activate ā The Difference That Breaks Workflows: https://youtu.be/8oLKMWOmHyk - Lesson 12: The 1,000 Items That Disappeared From My Inner Join: https://youtu.be/5nG6Ofq2PTs
Introduction
If you've spent any time building n8n workflows, you've probably hit this moment: you wire up a node, run the workflow, and it fires three, five, or ten times when you expected it to run once. Nothing in the UI screams an explanation at you. The workflow just... loops. Silently.
This isn't a bug, and it isn't random. It's a direct consequence of one data structure that every single node in n8n passes to the next ā the items array. Once you understand this one shape, node looping stops being mysterious and starts being predictable.
This matters even if you've never touched a line of backend code. n8n is built on top of ideas that show up constantly in real software engineering: arrays, objects, iteration, and the difference between "one record" and "a collection of records." Understanding items arrays here is a gentle, practical introduction to concepts that any developer eventually needs ā the same concepts behind API responses, database query results, and list-processing code in Python or JavaScript. Learn it in n8n's visual context, and it transfers directly.
Problem Overview
Every node in n8n receives data from the node before it, does something with that data, and passes the result to the next node. The question is: what exactly gets passed?
The answer is always the same regardless of which node you're using ā HTTP Request, Set, Code, Google Sheets, whatever. Data moves as a list called an items array. Each entry in that list is one "item," and each item is wrapped in a small, consistent structure:
- a
jsonkey holding the actual data for that item - an optional
binarykey holding a file, if one is attached
That's the entire contract. No matter how complex your workflow gets, every node is really just receiving a list of these wrapped objects and handing back another list of them.
The behavior that trips people up: if a node receives an items array with three entries, the next node runs its internal logic once per item ā three separate executions, not one. This isn't a special "loop mode" you opt into. It's just how n8n walks the array, one item at a time, by default.
Example
Imagine a workflow step that outputs three customer records after a database query. The items array looks like this conceptually:
[
{ "json": { "name": "Asha", "email": "asha@example.com" } },
{ "json": { "name": "Devon", "email": "devon@example.com" } },
{ "json": { "name": "Priya", "email": "priya@example.com" } }
]
Three entries in the array. When this list flows into the next node ā say, a node that sends a welcome email ā that node runs three times: once with Asha's data, once with Devon's, once with Priya's. Each run only sees its own item; it doesn't automatically see the other two unless you explicitly aggregate them.
If your workflow had returned a single customer instead, the array would have one entry, and the next node would run exactly once. The count of items is the count of executions. That's the whole rule.
Intuition
Here's how an experienced engineer thinks about this, without needing to memorize n8n's internals: any time you see a list-shaped structure feeding into a processing step, assume the processing step runs once per element unless told otherwise. This is just how iteration works in every language ā a for loop over a list runs its body once per element. n8n's canvas hides the loop, but the loop is still there.
The trap is assuming there's only ever one item, because during testing there usually is. You build the workflow with one sample row, everything looks fine, and the mental model that forms is "this node gets one thing." Then production traffic sends three, five, or a hundred records, and any expression written with the "one item" assumption either grabs the wrong entry or throws an error with no clear explanation.
The fix in your thinking is simple: before writing an expression, ask "how many items could actually be here?" not "how many items am I testing with?" Then verify it directly by clicking the node and reading its output panel, which shows the live items array with a toggle between a readable table view and the raw JSON view.
There's a second, subtler layer: a node's input and output can look completely different. A node might receive one shape of data and hand forward something restructured ā new keys added, values renamed, an extra wrapper layer introduced. Checking both tabs, not just one, is how you catch exactly where and how data changed shape as it moved through your workflow.
Brute Force Solution
There isn't really a "brute force vs optimal" algorithm here in the traditional sense ā this is a conceptual/data-modeling problem, not a computational one. But there is a brute-force habit, and it's worth naming because it's what most beginners default to.
Idea: Write expressions and configure nodes assuming a single item is always present, and only investigate the items array when something breaks.
Algorithm:
- Build the workflow with one sample record.
- Write expressions like you're accessing a single object.
- Run it, see it work, ship it.
- When production sends multiple items and something fails, start debugging from scratch.
Advantages: Fast to build initially. Feels intuitive if you're new to the concept.
Disadvantages: Breaks silently or loudly the moment real data has more than one row. Debugging happens under pressure, after the workflow is already "done." No systematic way to predict node behavior before running it.
Complexity: Not applicable in Big-O terms, but in practical terms: debugging cost scales with how late you discover the assumption was wrong ā which, with this approach, is usually production.
Optimal Solution
The optimal approach is a habit, not a code change: verify the shape of your data before writing logic against it.
Step-by-step:
| Step | Action | Why |
|---|---|---|
| 1 | Click the node you're about to configure | Opens the output panel with live data |
| 2 | Toggle to JSON view | Shows the raw items array, not a flattened table that can hide structure |
| 3 | Count the items | Tells you exactly how many times the next node will execute |
| 4 | Check the json key contents |
This is where your actual fields live |
| 5 | Check for a binary key |
Tells you if a file is attached and needs separate handling |
| 6 | Compare the node's Input tab to its Output tab | Reveals exactly where and how the data was reshaped |
Doing this before writing an expression means you're never guessing. You know the array length, you know the field names, and you know whether the next node will run once or many times ā before you hit "execute" and find out the hard way.
Python Code
n8n itself is visual, but the mental model maps directly onto ordinary Python, which is useful both for understanding it and for talking about it in an interview context.
from typing import Any
def process_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
Mirrors how an n8n node processes an items array:
runs once per item, returns a new items array.
"""
results = []
for item in items:
data = item["json"]
binary = item.get("binary") # optional, may not exist
# Per-item transformation logic goes here
transformed = {
"name": data["name"].upper(),
"email": data["email"],
}
new_item: dict[str, Any] = {"json": transformed}
if binary:
new_item["binary"] = binary
results.append(new_item)
return results
if __name__ == "__main__":
sample_items = [
{"json": {"name": "asha", "email": "asha@example.com"}},
{"json": {"name": "devon", "email": "devon@example.com"}},
{"json": {"name": "priya", "email": "priya@example.com"}},
]
output = process_items(sample_items)
assert len(output) == len(sample_items)
assert output[0]["json"]["name"] == "ASHA"
assert "binary" not in output[0]
print(f"Processed {len(output)} items ā node would run {len(sample_items)} times in n8n.")
Code Walkthrough
items: list[dict[str, Any]]ā the parameter shape mirrors n8n's items array: a list of dicts, each with ajsonkey and optionalbinarykey.for item in itemsā this loop is the "node runs once per item" behavior, made explicit instead of hidden behind a UI.item["json"]ā required key; every item has one. Accessing it directly is safe.item.get("binary")ā optional key; using.get()instead of[]avoids aKeyErrorwhen no file is attached, matching the "optional binary key" rule.new_itemconstruction ā only attachesbinaryif it existed on the input, so items without files don't gain a spurious empty key.- The
assertblock at the bottom is a minimal self-check: it confirms the output count matches the input count (proving one-output-per-item) and confirms the binary key isn't fabricated.
Dry Run
Input: three items ā Asha, Devon, Priya ā each with only a json key, no binary.
- Loop iteration 1:
data = {"name": "asha", "email": "asha@example.com"},binary = None. Transformed name becomes"ASHA".new_itemgets only ajsonkey. - Loop iteration 2: same process for Devon ā
"DEVON". - Loop iteration 3: same process for Priya ā
"PRIYA". resultsnow holds three items, structurally identical to the input but with uppercased names.- Assertions pass:
len(output) == 3, first item's name is"ASHA", nobinarykey present.
This is exactly what you'd see in n8n's output panel after a Set or Code node processes those three records ā three items in, three items out, one transformation applied per item.
Complexity Analysis
Time: O(n), where n is the number of items in the array. Each item is visited exactly once, and the per-item work (dict construction) is O(1). This is correct because there's no nested iteration over other items ā each transformation only depends on its own item's data.
Space: O(n) for the output list, since we build a new item per input item rather than mutating in place. This matches how n8n itself works ā nodes generally produce a fresh items array rather than mutating the array in place, which keeps the input/output tabs meaningfully different and inspectable.
Alternative Solutions
An alternative is to aggregate all items into a single item before processing ā using n8n's "Aggregate" node or equivalent Python code like all_data = [item["json"] for item in items] passed as one item. This trades "runs once per item" for "runs once total, with all data available at once."
When to use aggregation instead: when your logic needs to compare items against each other (e.g., "find the highest value across all records") rather than transform each independently. The per-item default is right for transformations; aggregation is right for cross-item logic.
Edge Cases
- Empty items array: zero items in means the next node runs zero times ā it's simply skipped, not run with empty data. In the Python version,
process_items([])returns[]with no errors. - Single item: works identically to any other count; the "loop" just has one iteration, which is why testing with one row hides the real behavior.
- Duplicate data across items: n8n doesn't deduplicate automatically ā two items with identical
jsoncontent still count as two separate items and trigger two executions. - Missing
binarykey: must be handled with.get()(or the node-level equivalent), not direct indexing, or you'll get aKeyErroron items without attachments. - Very large item counts: thousands of items mean thousands of node executions; this is where performance and rate limits (e.g., calling an external API per item) become a real design concern.
Common Mistakes
- Writing an expression assuming a single item, then being surprised when production sends multiple.
- Never toggling to JSON view ā the table view can visually flatten structure and hide the wrapper shape.
- Assuming input and output are the same at every node, and not checking both tabs when debugging.
- Treating the
binarykey as always present and accessing it without a safe/optional check. - Not noticing that zero items means the downstream node is skipped entirely, and mistaking that for a bug.
- Forgetting that identical-looking items are still counted separately ā no automatic deduplication.
Interview Questions
- How would you explain the difference between processing a list once vs. once per element, and why does that distinction matter for performance?
- What's the tradeoff between per-item processing and aggregating first, in terms of API calls or side effects?
- How would you defensively handle an optional field in a data structure (like the
binarykey here) without raising an exception? - Given a list of objects, how would you detect and remove duplicates before processing them?
- How does this items-array pattern relate to how REST APIs typically return collections of resources?
Similar Problems
- LeetCode "Two Sum" and other array-iteration problems ā same core skill of reasoning about "what happens per element" versus "across all elements."
- LeetCode "Flatten Nested List Iterator" ā directly relevant to understanding wrapped, nested data structures like the
json/binarywrapper. - LeetCode "Group Anagrams" ā good practice for aggregation-style thinking, the alternative approach discussed above.
- Any "process a batch of records" interview question ā the per-item vs. batch distinction shows up constantly in real backend and data engineering work.
Key Takeaways
- Everything that flows through n8n is an items array ā a list where each entry is a
jsonobject with an optionalbinaryattachment. - A node runs once per item it receives; array length equals execution count.
- Input and output tabs can differ significantly ā always check both when debugging.
- Verify data shape via the output panel before writing expressions, not after something breaks.
- This pattern isn't n8n-specific ā it's ordinary list iteration, the same concept behind loops in any programming language.
Watch the Video
For the full walkthrough ā including tracing one field through three connected nodes and seeing the table/JSON toggle live ā watch the video here: {LINK}
About the Series
This lesson is part of Fun with Learning Technology, a series that opens the hood on tools developers and non-developers use every day but rarely get explained from first principles. Each lesson takes one concept ā like the items array here ā and builds a clear, lasting mental model around it, whether you're coming from a coding background or not.
Call To Action
If this cleared up why your n8n workflow keeps running more times than you expected, the video walks through it visually and is worth a watch on YouTube ā a like helps it reach more people learning the same thing. Subscribe on YouTube and subscribe to the Substack for the next lesson, where we'll actually transform these items ourselves. Drop a comment with your guess on the five-item, if-condition question from the video, and share this with anyone stuck debugging a workflow that "runs too many times."
The solution
item = {
json: {
name: "Ana",
email: "ana@x.com"
},
binary: { } // optional, for files
}Ready to try it yourself? Solve N8n problems with instant feedback in the practice sandbox.
Practice now ā


