Ep 04: The Heartbeat of Data — Deep Dive into JSON Items & Implicit Loops
Why This Is the Most Important Episode
Over 40% of beginner questions on the n8n community forum relate to data flow:
- "Why did my node execute 5 times?"
- "Why is the Merge node output wrong?"
- "Why does
$input.first()differ from$input.all()in Code nodes?"
The root cause is always: not understanding n8n's Item-Driven execution model.
Item Anatomy
All data in n8n travels between nodes as Items. An Item is a standard JSON object with two top-level fields:
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Complete structure of an n8n Data Item
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const item = {
// 📦 json: Core data carrier
// All business data lives here
"json": {
"id": 42,
"name": "Alice",
"email": "[email protected]",
"tags": ["VIP", "enterprise"], // Arrays supported
"address": { // Nested objects supported
"city": "Shanghai",
"district": "Pudong"
}
},
// 📎 binary (optional): File carrier
// Used when Items carry files (images, PDFs, CSVs)
"binary": {
"attachment": {
"data": "base64_encoded_string", // File content (Base64)
"mimeType": "image/png",
"fileName": "screenshot.png",
"fileSize": 1024000
}
}
};
Items Are Arrays!
Critical: nodes don't pass single Items — they pass Item arrays:
graph LR
subgraph "Node A Output"
I1["Item 0
{name: 'Alice'}"]
I2["Item 1
{name: 'Bob'}"]
I3["Item 2
{name: 'Carol'}"]
end
subgraph "Connection"
PIPE["📦📦📦
3 Items"]
end
subgraph "Node B Input"
R1["Receives Item 0"]
R2["Receives Item 1"]
R3["Receives Item 2"]
end
I1 & I2 & I3 --> PIPE --> R1 & R2 & R3
style PIPE fill:#f59e0b,stroke:#d97706,color:#fffThe Implicit Loop
This is the biggest difference between n8n and traditional programming.
Traditional code requires manual loops:
# ❌ Traditional: manual iteration required
for user in users:
result = process(user)
results.append(result)
In n8n, loops are built-in — you don't even notice them:
sequenceDiagram
participant HTTP as HTTP Request Node
participant Set as Set Node
participant Gmail as Gmail Node
Note over HTTP: 🔄 API returns 3 users
HTTP->>HTTP: Output: [{name:"Alice"}, {name:"Bob"}, {name:"Carol"}]
rect rgb(40, 60, 40)
Note over Set: 🔁 Implicit loop begins (no loop code visible!)
HTTP->>Set: Item 0: {name:"Alice"}
Set->>Set: Add field greeting: "Hello, Alice"
HTTP->>Set: Item 1: {name:"Bob"}
Set->>Set: Add field greeting: "Hello, Bob"
HTTP->>Set: Item 2: {name:"Carol"}
Set->>Set: Add field greeting: "Hello, Carol"
Note over Set: 🔁 Implicit loop ends
end
Set->>Gmail: 3 Items with greetings
Note over Gmail: Automatically sends 3 emails!Golden Rule: Every n8n node executes once per Item by default. If upstream emits N Items, downstream executes N times. You write zero loop code.
Five Data Manipulation Patterns
Pattern 1: Split (1→N)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Input: 1 Item containing an array of 3 orders
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const orders = $input.first().json.orders; // Extract the array
// Convert each array element into an independent Item
return orders.map(order => ({ json: order }));
// Output: 3 independent Items
Pattern 2: Aggregate (N→1)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// Input: 3 Items
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const allItems = $input.all(); // Get all upstream Items
const totalAmount = allItems.reduce(
(sum, item) => sum + item.json.amount,
0 // Initial value
);
// Return a single summary Item
return [{
json: {
totalOrders: allItems.length,
totalAmount: totalAmount,
averageAmount: Math.round(totalAmount / allItems.length)
}
}];
Pattern 3: Map (N→N)
// Each Item processed independently, same count in = same count out
// Use $input.item to access the current Item in the implicit loop
const currentItem = $input.item;
return {
json: {
...currentItem.json, // Spread: keep all existing fields
processedAt: new Date().toISOString(), // Add: processing timestamp
nameLength: currentItem.json.name.length // Add: computed field
}
};
Pattern 4: Filter (N→M where M≤N)
const items = $input.all();
return items.filter(item => {
// Keep only VIP users who spent over 1000
return item.json.isVIP === true && item.json.totalSpent > 1000;
});
// Input: 10 Items → Output: maybe 3 Items
Pattern 5: Merge (Dual-stream join)
graph LR
subgraph "Source 1: Users"
U["[{id:1, name:'Alice'},
{id:2, name:'Bob'}]"]
end
subgraph "Source 2: Orders"
O["[{userId:1, amount:99},
{userId:2, amount:150}]"]
end
U --> M["Merge Node
Mode: Combine by Field
Match: id = userId"]
O --> M
M --> R["[{id:1, name:'Alice', amount:99},
{id:2, name:'Bob', amount:150}]"]
style M fill:#6366f1,stroke:#4f46e5,color:#fffCommon Pitfalls
| Pitfall | Symptom | Cause | Solution |
|---|---|---|---|
| Empty Items | Downstream never executes | Upstream output [] empty array |
Use If node: {{ $input.all().length > 0 }} |
| Over-execution | Gmail sent 100 emails | 100 Items flowed in | Aggregate first, or use Limit node |
| Binary Lost | File disappears after processing | Code node didn't pass binary |
Return { json: {...}, binary: item.binary } |
| Nested JSON | Can't access deep fields | Wrong expression syntax | Use {{ $json.address.city }} dot notation |
Data Flow Lifecycle Summary
graph TB
subgraph "n8n Item Lifecycle"
Birth["🐣 Birth
Trigger produces initial Items"]
Transform["🔄 Transform
Set/Code nodes modify fields"]
Split["✂️ Split
1 Item → N Items"]
Filter["🔍 Filter
N Items → M (M ≤ N)"]
Merge["🔗 Merge
Multi-source Items JOIN"]
Aggregate["📊 Aggregate
N Items → 1 summary"]
Output["🎯 Consume
Gmail / Slack / DB nodes"]
end
Birth --> Transform --> Split --> Filter --> Merge --> Aggregate --> Output
Transform -->|"Direct consume"| Output
Filter -->|"Direct consume"| OutputNext Episode
In Ep 05, we put theory into practice — building a real Webhook + Telegram Bot bidirectional workflow to make data truly "flow".