409e92d6ae
Build and Push Docker Images / Build Docker Image (push) Has been cancelled
Build and Push Docker Images / Build Railway Docker Image (push) Has been cancelled
Build and Publish n8n Docker Image / test-image (push) Has been cancelled
Dependency Compatibility Check / Fresh Install Dependency Check (push) Has been cancelled
Build and Publish n8n Docker Image / build-and-push (push) Has been cancelled
Build and Publish n8n Docker Image / create-release (push) Has been cancelled
Automated Release / Detect Version Change (push) Has been cancelled
Automated Release / Generate Release Notes (push) Has been cancelled
Automated Release / Create GitHub Release (push) Has been cancelled
Automated Release / Package MCPB Bundle (push) Has been cancelled
Automated Release / Build and Verify (push) Has been cancelled
Automated Release / Publish to NPM (push) Has been cancelled
Automated Release / Build and Push Docker Images (push) Has been cancelled
Automated Release / Update Documentation (push) Has been cancelled
Automated Release / Notify Release Completion (push) Has been cancelled
Secret Scan / secretlint (push) Has been cancelled
Test Suite / test (push) Has been cancelled
Test Suite / cjs-runtime (push) Has been cancelled
Test Suite / publish-results (push) Has been cancelled
898 lines
22 KiB
Markdown
898 lines
22 KiB
Markdown
# Data Access Patterns - JavaScript Code Node
|
||
|
||
Comprehensive guide to accessing data in n8n Code nodes using JavaScript.
|
||
|
||
---
|
||
|
||
## Overview
|
||
|
||
In n8n Code nodes, you access data from previous nodes using built-in variables and methods. Understanding which method to use is critical for correct workflow execution.
|
||
|
||
**Data Access Priority** (by common usage):
|
||
1. **`$input.all()`** - Most common - Batch operations, aggregations
|
||
2. **`$input.first()`** - Very common - Single item operations
|
||
3. **`$input.item`** - Common - Each Item mode only
|
||
4. **`$node["NodeName"].json`** - Specific node references
|
||
5. **`$json`** - Direct current item (legacy, use `$input` instead)
|
||
|
||
---
|
||
|
||
## Pattern 1: $input.all() - Process All Items
|
||
|
||
**Usage**: Most common pattern for batch processing
|
||
|
||
**When to use:**
|
||
- Processing multiple records
|
||
- Aggregating data (sum, count, average)
|
||
- Filtering arrays
|
||
- Transforming datasets
|
||
- Comparing items
|
||
- Sorting or ranking
|
||
|
||
### Basic Usage
|
||
|
||
```javascript
|
||
// Get all items from previous node
|
||
const allItems = $input.all();
|
||
|
||
// allItems is an array of objects like:
|
||
// [
|
||
// {json: {id: 1, name: "Alice"}},
|
||
// {json: {id: 2, name: "Bob"}}
|
||
// ]
|
||
|
||
console.log(`Received ${allItems.length} items`);
|
||
|
||
return allItems;
|
||
```
|
||
|
||
### Example 1: Filter Active Items
|
||
|
||
```javascript
|
||
const allItems = $input.all();
|
||
|
||
// Filter only active items
|
||
const activeItems = allItems.filter(item => item.json.status === 'active');
|
||
|
||
return activeItems;
|
||
```
|
||
|
||
### Example 2: Transform All Items
|
||
|
||
```javascript
|
||
const allItems = $input.all();
|
||
|
||
// Map to new structure
|
||
const transformed = allItems.map(item => ({
|
||
json: {
|
||
id: item.json.id,
|
||
fullName: `${item.json.firstName} ${item.json.lastName}`,
|
||
email: item.json.email,
|
||
processedAt: new Date().toISOString()
|
||
}
|
||
}));
|
||
|
||
return transformed;
|
||
```
|
||
|
||
### Example 3: Aggregate Data
|
||
|
||
```javascript
|
||
const allItems = $input.all();
|
||
|
||
// Calculate total
|
||
const total = allItems.reduce((sum, item) => {
|
||
return sum + (item.json.amount || 0);
|
||
}, 0);
|
||
|
||
return [{
|
||
json: {
|
||
total,
|
||
count: allItems.length,
|
||
average: total / allItems.length
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 4: Sort and Limit
|
||
|
||
```javascript
|
||
const allItems = $input.all();
|
||
|
||
// Get top 5 by score
|
||
const topFive = allItems
|
||
.sort((a, b) => (b.json.score || 0) - (a.json.score || 0))
|
||
.slice(0, 5);
|
||
|
||
return topFive.map(item => ({json: item.json}));
|
||
```
|
||
|
||
### Example 5: Group By Category
|
||
|
||
```javascript
|
||
const allItems = $input.all();
|
||
|
||
// Group items by category
|
||
const grouped = {};
|
||
|
||
for (const item of allItems) {
|
||
const category = item.json.category || 'Uncategorized';
|
||
|
||
if (!grouped[category]) {
|
||
grouped[category] = [];
|
||
}
|
||
|
||
grouped[category].push(item.json);
|
||
}
|
||
|
||
// Convert to array format
|
||
return Object.entries(grouped).map(([category, items]) => ({
|
||
json: {
|
||
category,
|
||
items,
|
||
count: items.length
|
||
}
|
||
}));
|
||
```
|
||
|
||
### Example 6: Deduplicate by ID
|
||
|
||
```javascript
|
||
const allItems = $input.all();
|
||
|
||
// Remove duplicates by ID
|
||
const seen = new Set();
|
||
const unique = [];
|
||
|
||
for (const item of allItems) {
|
||
const id = item.json.id;
|
||
|
||
if (!seen.has(id)) {
|
||
seen.add(id);
|
||
unique.push(item);
|
||
}
|
||
}
|
||
|
||
return unique;
|
||
```
|
||
|
||
---
|
||
|
||
## Pattern 2: $input.first() - Get First Item
|
||
|
||
**Usage**: Very common for single-item operations
|
||
|
||
**When to use:**
|
||
- Previous node returns single object
|
||
- Working with API responses
|
||
- Getting initial/first data point
|
||
- Configuration or metadata access
|
||
|
||
### Basic Usage
|
||
|
||
```javascript
|
||
// Get first item from previous node
|
||
const firstItem = $input.first();
|
||
|
||
// Access the JSON data
|
||
const data = firstItem.json;
|
||
|
||
console.log('First item:', data);
|
||
|
||
return [{json: data}];
|
||
```
|
||
|
||
### Example 1: Process Single API Response
|
||
|
||
```javascript
|
||
// Get API response (typically single object)
|
||
const response = $input.first().json;
|
||
|
||
// Extract what you need
|
||
return [{
|
||
json: {
|
||
userId: response.data.user.id,
|
||
userName: response.data.user.name,
|
||
status: response.status,
|
||
fetchedAt: new Date().toISOString()
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 2: Transform Single Object
|
||
|
||
```javascript
|
||
const data = $input.first().json;
|
||
|
||
// Transform structure
|
||
return [{
|
||
json: {
|
||
id: data.id,
|
||
contact: {
|
||
email: data.email,
|
||
phone: data.phone
|
||
},
|
||
address: {
|
||
street: data.street,
|
||
city: data.city,
|
||
zip: data.zip
|
||
}
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 3: Validate Single Item
|
||
|
||
```javascript
|
||
const item = $input.first().json;
|
||
|
||
// Validation logic
|
||
const isValid = item.email && item.email.includes('@');
|
||
|
||
return [{
|
||
json: {
|
||
...item,
|
||
valid: isValid,
|
||
validatedAt: new Date().toISOString()
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 4: Extract Nested Data
|
||
|
||
```javascript
|
||
const response = $input.first().json;
|
||
|
||
// Navigate nested structure
|
||
const users = response.data?.users || [];
|
||
|
||
return users.map(user => ({
|
||
json: {
|
||
id: user.id,
|
||
name: user.profile?.name || 'Unknown',
|
||
email: user.contact?.email || 'no-email'
|
||
}
|
||
}));
|
||
```
|
||
|
||
### Example 5: Combine with Other Methods
|
||
|
||
```javascript
|
||
// Get first item's data
|
||
const firstData = $input.first().json;
|
||
|
||
// Use it to filter all items
|
||
const allItems = $input.all();
|
||
const matching = allItems.filter(item =>
|
||
item.json.category === firstData.targetCategory
|
||
);
|
||
|
||
return matching;
|
||
```
|
||
|
||
---
|
||
|
||
## Pattern 3: $input.item - Current Item (Each Item Mode)
|
||
|
||
**Usage**: Common in "Run Once for Each Item" mode
|
||
|
||
**When to use:**
|
||
- Mode is set to "Run Once for Each Item"
|
||
- Need to process items independently
|
||
- Per-item API calls or validations
|
||
- Item-specific error handling
|
||
|
||
**IMPORTANT**: Only use in "Each Item" mode. Will be undefined in "All Items" mode.
|
||
|
||
### Basic Usage
|
||
|
||
```javascript
|
||
// In "Run Once for Each Item" mode
|
||
const currentItem = $input.item;
|
||
const data = currentItem.json;
|
||
|
||
console.log('Processing item:', data.id);
|
||
|
||
return [{
|
||
json: {
|
||
...data,
|
||
processed: true
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 1: Add Processing Metadata
|
||
|
||
```javascript
|
||
const item = $input.item;
|
||
|
||
return [{
|
||
json: {
|
||
...item.json,
|
||
processed: true,
|
||
processedAt: new Date().toISOString(),
|
||
processingDuration: Math.random() * 1000 // Simulated duration
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 2: Per-Item Validation
|
||
|
||
```javascript
|
||
const item = $input.item;
|
||
const data = item.json;
|
||
|
||
// Validate this specific item
|
||
const errors = [];
|
||
|
||
if (!data.email) errors.push('Email required');
|
||
if (!data.name) errors.push('Name required');
|
||
if (data.age && data.age < 18) errors.push('Must be 18+');
|
||
|
||
return [{
|
||
json: {
|
||
...data,
|
||
valid: errors.length === 0,
|
||
errors: errors.length > 0 ? errors : undefined
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 3: Item-Specific API Call
|
||
|
||
```javascript
|
||
const item = $input.item;
|
||
const userId = item.json.userId;
|
||
|
||
// Make API call specific to this item
|
||
const response = await this.helpers.httpRequest({
|
||
method: 'GET',
|
||
url: `https://api.example.com/users/${userId}/details`
|
||
});
|
||
|
||
return [{
|
||
json: {
|
||
...item.json,
|
||
details: response
|
||
}
|
||
}];
|
||
```
|
||
|
||
> ⚠️ **Use `this.helpers.httpRequest`, not `$helpers`.** In the Code node's task-runner sandbox (default since n8n v2.0) the bare `$helpers` global is undefined — `$helpers.httpRequest()` throws `ReferenceError: $helpers is not defined`. **For authenticated APIs, don't extend this pattern.** `this.helpers.httpRequestWithAuthentication` is blocked in the task-runner sandbox. Use an HTTP Request node with the credential attached, or delegate to a sub-workflow whose HTTP Request node holds the credential. For anything beyond a trivial unauthenticated GET, prefer the HTTP Request node anyway. See ERROR_PATTERNS.md Error #6.
|
||
|
||
### Example 4: Conditional Processing
|
||
|
||
```javascript
|
||
const item = $input.item;
|
||
const data = item.json;
|
||
|
||
// Process based on item type
|
||
if (data.type === 'premium') {
|
||
return [{
|
||
json: {
|
||
...data,
|
||
discount: 0.20,
|
||
tier: 'premium'
|
||
}
|
||
}];
|
||
} else {
|
||
return [{
|
||
json: {
|
||
...data,
|
||
discount: 0.05,
|
||
tier: 'standard'
|
||
}
|
||
}];
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Pattern 4: $node - Reference Other Nodes
|
||
|
||
**Usage**: Less common, but powerful for specific scenarios
|
||
|
||
**When to use:**
|
||
- Need data from specific named node
|
||
- Combining data from multiple nodes
|
||
- Accessing metadata about workflow execution
|
||
|
||
### Basic Usage
|
||
|
||
```javascript
|
||
// Get output from specific node
|
||
const webhookData = $node["Webhook"].json;
|
||
const apiData = $node["HTTP Request"].json;
|
||
|
||
return [{
|
||
json: {
|
||
fromWebhook: webhookData,
|
||
fromAPI: apiData
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 1: Combine Multiple Sources
|
||
|
||
```javascript
|
||
// Reference multiple nodes
|
||
const webhook = $node["Webhook"].json;
|
||
const database = $node["Postgres"].json;
|
||
const api = $node["HTTP Request"].json;
|
||
|
||
return [{
|
||
json: {
|
||
combined: {
|
||
webhook: webhook.body,
|
||
dbRecords: database.length,
|
||
apiResponse: api.status
|
||
},
|
||
processedAt: new Date().toISOString()
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 2: Compare Across Nodes
|
||
|
||
```javascript
|
||
const oldData = $node["Get Old Data"].json;
|
||
const newData = $node["Get New Data"].json;
|
||
|
||
// Compare
|
||
const changes = {
|
||
added: newData.filter(n => !oldData.find(o => o.id === n.id)),
|
||
removed: oldData.filter(o => !newData.find(n => n.id === o.id)),
|
||
modified: newData.filter(n => {
|
||
const old = oldData.find(o => o.id === n.id);
|
||
return old && JSON.stringify(old) !== JSON.stringify(n);
|
||
})
|
||
};
|
||
|
||
return [{
|
||
json: {
|
||
changes,
|
||
summary: {
|
||
added: changes.added.length,
|
||
removed: changes.removed.length,
|
||
modified: changes.modified.length
|
||
}
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Example 3: Access Node Metadata
|
||
|
||
```javascript
|
||
// Get data from specific execution path
|
||
const ifTrueBranch = $node["IF True"].json;
|
||
const ifFalseBranch = $node["IF False"].json;
|
||
|
||
// Use whichever branch executed
|
||
const result = ifTrueBranch || ifFalseBranch || {};
|
||
|
||
return [{json: result}];
|
||
```
|
||
|
||
---
|
||
|
||
## Critical: Webhook Data Structure
|
||
|
||
**MOST COMMON MISTAKE**: Forgetting webhook data is nested under `.body`
|
||
|
||
### The Problem
|
||
|
||
Webhook node wraps all incoming data under a `body` property. This catches many developers by surprise.
|
||
|
||
### Structure
|
||
|
||
```javascript
|
||
// Webhook node output structure:
|
||
{
|
||
"headers": {
|
||
"content-type": "application/json",
|
||
"user-agent": "...",
|
||
// ... other headers
|
||
},
|
||
"params": {},
|
||
"query": {},
|
||
"body": {
|
||
// ← YOUR DATA IS HERE
|
||
"name": "Alice",
|
||
"email": "alice@example.com",
|
||
"message": "Hello!"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Wrong vs Right
|
||
|
||
```javascript
|
||
// ❌ WRONG: Trying to access directly
|
||
const name = $json.name; // undefined
|
||
const email = $json.email; // undefined
|
||
|
||
// ✅ CORRECT: Access via .body
|
||
const name = $json.body.name; // "Alice"
|
||
const email = $json.body.email; // "alice@example.com"
|
||
|
||
// ✅ CORRECT: Extract body first
|
||
const webhookData = $json.body;
|
||
const name = webhookData.name; // "Alice"
|
||
const email = webhookData.email; // "alice@example.com"
|
||
```
|
||
|
||
### Example: Full Webhook Processing
|
||
|
||
```javascript
|
||
// Get webhook data from previous node
|
||
const webhookOutput = $input.first().json;
|
||
|
||
// Access the actual payload
|
||
const payload = webhookOutput.body;
|
||
|
||
// Access headers if needed
|
||
const contentType = webhookOutput.headers['content-type'];
|
||
|
||
// Access query parameters if needed
|
||
const apiKey = webhookOutput.query.api_key;
|
||
|
||
// Process the actual data
|
||
return [{
|
||
json: {
|
||
// Data from webhook body
|
||
userName: payload.name,
|
||
userEmail: payload.email,
|
||
message: payload.message,
|
||
|
||
// Metadata
|
||
receivedAt: new Date().toISOString(),
|
||
contentType: contentType,
|
||
authenticated: !!apiKey
|
||
}
|
||
}];
|
||
```
|
||
|
||
### POST Data, Query Params, and Headers
|
||
|
||
```javascript
|
||
const webhook = $input.first().json;
|
||
|
||
return [{
|
||
json: {
|
||
// POST body data
|
||
formData: webhook.body,
|
||
|
||
// Query parameters (?key=value)
|
||
queryParams: webhook.query,
|
||
|
||
// HTTP headers
|
||
userAgent: webhook.headers['user-agent'],
|
||
contentType: webhook.headers['content-type'],
|
||
|
||
// Request metadata
|
||
method: webhook.method, // POST, GET, etc.
|
||
url: webhook.url
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Common Webhook Scenarios
|
||
|
||
```javascript
|
||
// Scenario 1: Form submission
|
||
const formData = $json.body;
|
||
const name = formData.name;
|
||
const email = formData.email;
|
||
|
||
// Scenario 2: JSON API webhook
|
||
const apiPayload = $json.body;
|
||
const eventType = apiPayload.event;
|
||
const data = apiPayload.data;
|
||
|
||
// Scenario 3: Query parameters
|
||
const apiKey = $json.query.api_key;
|
||
const userId = $json.query.user_id;
|
||
|
||
// Scenario 4: Headers
|
||
const authorization = $json.headers['authorization'];
|
||
const signature = $json.headers['x-signature'];
|
||
```
|
||
|
||
---
|
||
|
||
## Choosing the Right Pattern
|
||
|
||
### Decision Tree
|
||
|
||
```
|
||
Do you need ALL items from previous node?
|
||
├─ YES → Use $input.all()
|
||
│
|
||
└─ NO → Do you need just the FIRST item?
|
||
├─ YES → Use $input.first()
|
||
│
|
||
└─ NO → Are you in "Each Item" mode?
|
||
├─ YES → Use $input.item
|
||
│
|
||
└─ NO → Do you need specific node data?
|
||
├─ YES → Use $node["NodeName"]
|
||
└─ NO → Use $input.first() (default)
|
||
```
|
||
|
||
### Quick Reference Table
|
||
|
||
| Scenario | Use This | Example |
|
||
|----------|----------|---------|
|
||
| Sum all amounts | `$input.all()` | `allItems.reduce((sum, i) => sum + i.json.amount, 0)` |
|
||
| Get API response | `$input.first()` | `$input.first().json.data` |
|
||
| Process each independently | `$input.item` | `$input.item.json` (Each Item mode) |
|
||
| Combine two nodes | `$node["Name"]` | `$node["API"].json` |
|
||
| Filter array | `$input.all()` | `allItems.filter(i => i.json.active)` |
|
||
| Transform single object | `$input.first()` | `{...input.first().json, new: true}` |
|
||
| Webhook data | `$input.first()` | `$input.first().json.body` |
|
||
|
||
---
|
||
|
||
## Common Mistakes
|
||
|
||
### Mistake 1: Using $json Without Context
|
||
|
||
```javascript
|
||
// ❌ WRONG: $json is ambiguous
|
||
const value = $json.field;
|
||
|
||
// ✅ CORRECT: Be explicit
|
||
const value = $input.first().json.field;
|
||
```
|
||
|
||
### Mistake 2: Forgetting .json Property
|
||
|
||
```javascript
|
||
// ❌ WRONG: Trying to access fields on item object
|
||
const items = $input.all();
|
||
const names = items.map(item => item.name); // undefined
|
||
|
||
// ✅ CORRECT: Access via .json
|
||
const names = items.map(item => item.json.name);
|
||
```
|
||
|
||
### Mistake 3: Using $input.item in All Items Mode
|
||
|
||
```javascript
|
||
// ❌ WRONG: $input.item is undefined in "All Items" mode
|
||
const data = $input.item.json; // Error!
|
||
|
||
// ✅ CORRECT: Use appropriate method
|
||
const data = $input.first().json; // Or $input.all()
|
||
```
|
||
|
||
### Mistake 4: Not Handling Empty Arrays
|
||
|
||
```javascript
|
||
// ❌ WRONG: Crashes if no items
|
||
const first = $input.all()[0].json;
|
||
|
||
// ✅ CORRECT: Check length first
|
||
const items = $input.all();
|
||
if (items.length === 0) {
|
||
return [];
|
||
}
|
||
const first = items[0].json;
|
||
|
||
// ✅ ALSO CORRECT: Use $input.first()
|
||
const first = $input.first().json; // Built-in safety
|
||
```
|
||
|
||
### Mistake 5: Modifying Original Data
|
||
|
||
```javascript
|
||
// ❌ RISKY: Mutating original
|
||
const items = $input.all();
|
||
items[0].json.modified = true; // Modifies original
|
||
return items;
|
||
|
||
// ✅ SAFE: Create new objects
|
||
const items = $input.all();
|
||
return items.map(item => ({
|
||
json: {
|
||
...item.json,
|
||
modified: true
|
||
}
|
||
}));
|
||
```
|
||
|
||
---
|
||
|
||
## Advanced Patterns
|
||
|
||
### Pattern: Pagination Handling
|
||
|
||
```javascript
|
||
const currentPage = $input.all();
|
||
const pageNumber = $node["Set Page"].json.page || 1;
|
||
|
||
// Combine with previous pages
|
||
const allPreviousPages = $node["Accumulator"]?.json.accumulated || [];
|
||
|
||
return [{
|
||
json: {
|
||
accumulated: [...allPreviousPages, ...currentPage],
|
||
currentPage: pageNumber,
|
||
totalItems: allPreviousPages.length + currentPage.length
|
||
}
|
||
}];
|
||
```
|
||
|
||
### Pattern: Conditional Node Reference
|
||
|
||
```javascript
|
||
// Access different nodes based on condition
|
||
const condition = $input.first().json.type;
|
||
|
||
let data;
|
||
if (condition === 'api') {
|
||
data = $node["API Response"].json;
|
||
} else if (condition === 'database') {
|
||
data = $node["Database"].json;
|
||
} else {
|
||
data = $node["Default"].json;
|
||
}
|
||
|
||
return [{json: data}];
|
||
```
|
||
|
||
### Pattern: Multi-Node Aggregation
|
||
|
||
```javascript
|
||
// Collect data from multiple named nodes
|
||
const sources = ['Source1', 'Source2', 'Source3'];
|
||
const allData = [];
|
||
|
||
for (const source of sources) {
|
||
const nodeData = $node[source]?.json;
|
||
if (nodeData) {
|
||
allData.push({
|
||
source,
|
||
data: nodeData
|
||
});
|
||
}
|
||
}
|
||
|
||
return allData.map(item => ({json: item}));
|
||
```
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
**Most Common Patterns**:
|
||
1. `$input.all()` - Process multiple items, batch operations
|
||
2. `$input.first()` - Single item, API responses
|
||
3. `$input.item` - Each Item mode processing
|
||
|
||
**Critical Rule**:
|
||
- Webhook data is under `.body` property
|
||
|
||
**Best Practice**:
|
||
- Be explicit: Use `$input.first().json.field` instead of `$json.field`
|
||
- Always check for null/undefined
|
||
- Use appropriate method for your mode (All Items vs Each Item)
|
||
|
||
**See Also**:
|
||
- [SKILL.md](SKILL.md) - Overview and quick start
|
||
- [COMMON_PATTERNS.md](COMMON_PATTERNS.md) - Production patterns
|
||
- [ERROR_PATTERNS.md](ERROR_PATTERNS.md) - Avoid common mistakes
|
||
|
||
---
|
||
|
||
## Mode Performance: Why "All Items" Is Faster
|
||
|
||
Mode choice is the single biggest performance lever in a Code node, and the reason generalizes to the rest of your workflow. Every time n8n hands items to a *per-item* execution context it pays a setup cost. Measured on an n8n 2.x instance (small records, ~10k items):
|
||
|
||
| What runs per item | Approx. cost | Why |
|
||
|---|---|---|
|
||
| Code **All Items** (one run for the whole set) | ~0.02 ms/item | one context setup, then plain JS — the loop is free |
|
||
| Expression in any node (IF / Set / etc.) | ~0.2 ms/item | a light eval context per item |
|
||
| Code **Each Item** | ~0.6 ms/item | a full code sandbox per item — ~3× an expression, ~25–30× All Items |
|
||
|
||
So `Run Once for Each Item` over 10k items is ~6 s of pure overhead vs ~0.2 s for the same logic in `Run Once for All Items`. Use Each Item only when an item genuinely needs isolating (independent error handling, or a per-item API call you can't batch); otherwise loop *inside* one All Items node.
|
||
|
||
Two corollaries you will hit constantly:
|
||
|
||
- **Expression complexity is essentially free.** An elaborate `{{ }}` measures the same as a trivial one — ~90% of the cost is n8n building the per-item context, not running your code. Don't simplify expressions for speed; reduce the *number* of per-item boundaries instead.
|
||
- **Every node→node hop re-copies all items** (~0.05 ms/item per hop). Six chained All Items Code nodes cost ~7× a single node doing the same six steps, so consolidate a hot transform chain into one All Items node — and never build a chain of *Each Item* Code nodes, where the per-item tax multiplies by node count (a 6-node Each-Item chain over 2k items ≈ 7 s).
|
||
|
||
**Scale check:** below a few hundred items this is all sub-100 ms and not worth a thought, and most real workflows are dominated by I/O (HTTP / DB / Sheets round-trips) that dwarfs node overhead. Reach for these rules on the hot path — large item counts with little I/O — not everywhere.
|
||
|
||
---
|
||
|
||
## Production Gotchas
|
||
|
||
Hard-won lessons from real-world n8n workflow deployments.
|
||
|
||
### SplitInBatches Loop Semantics
|
||
|
||
The SplitInBatches node has two outputs — and the naming is counterintuitive:
|
||
- `main[0]` = **done** — fires ONCE after all batches are processed
|
||
- `main[1]` = **each batch** — fires for every batch (this is the loop body)
|
||
|
||
Always add a **Limit 1** node after the done output before downstream processing, as a safety against edge cases where done fires with extra items.
|
||
|
||
### SplitInBatches: Iteration Count Is the Cost
|
||
|
||
Each loop iteration re-executes the entire loop body through the workflow engine — ~0.8 ms per iteration of pure overhead, on top of whatever the body does. Total ≈ `⌈items / batchSize⌉ × (~0.8 ms + body cost)`:
|
||
|
||
- `batchSize: 1` over N items pays that N times — it's the loop equivalent of *Run Once for Each Item* (and if the body has several nodes, each iteration re-pays all of them).
|
||
- Raising `batchSize` cuts iterations proportionally; the body still sees every item. Use the **largest batch your real constraint allows** (API rate limit, page size, memory). If you don't need batching at all, don't loop — process the whole set in one All Items node.
|
||
|
||
### Cross-Iteration Data Accumulation (CRITICAL)
|
||
|
||
After a SplitInBatches loop, `$('Node Inside Loop').all()` returns **ONLY the last iteration's items**, not cumulative results. This silently drops data from all but the final batch.
|
||
|
||
**Fix**: Use workflow static data to accumulate across iterations:
|
||
|
||
```javascript
|
||
// BEFORE the loop (reset accumulator):
|
||
const staticData = $getWorkflowStaticData('global');
|
||
staticData.results = [];
|
||
return $input.all();
|
||
|
||
// INSIDE the loop body (accumulate):
|
||
const staticData = $getWorkflowStaticData('global');
|
||
const results = [];
|
||
for (const item of $input.all()) {
|
||
const processed = { /* ... */ };
|
||
results.push({ json: processed });
|
||
staticData.results.push(processed);
|
||
}
|
||
return results;
|
||
|
||
// AFTER the loop (read accumulated data):
|
||
const staticData = $getWorkflowStaticData('global');
|
||
const allResults = staticData.results || [];
|
||
// Now aggregate across ALL iterations
|
||
```
|
||
|
||
### pairedItem for New Output Items
|
||
|
||
When creating new items that don't map 1:1 to input items, include `pairedItem` — otherwise downstream Set nodes fail with `paired_item_no_info`:
|
||
|
||
```javascript
|
||
const results = [];
|
||
for (let i = 0; i < $input.all().length; i++) {
|
||
const item = $input.all()[i];
|
||
results.push({
|
||
json: { /* new data */ },
|
||
pairedItem: { item: i }
|
||
});
|
||
}
|
||
return results;
|
||
```
|
||
|
||
### Correct Node Reference Syntax
|
||
|
||
```javascript
|
||
// ❌ WRONG - .json directly on node reference
|
||
const data = $('HTTP Request').json;
|
||
|
||
// ✅ CORRECT - call .first() then access .json
|
||
const data = $('HTTP Request').first().json;
|
||
|
||
// ✅ Also correct - get all items
|
||
const allData = $('HTTP Request').all();
|
||
```
|
||
|
||
### Float Precision for Price/Currency Comparison
|
||
|
||
When comparing prices or currency values, floating point noise can cause false positives. Round to cents:
|
||
|
||
```javascript
|
||
// ❌ Unreliable - float comparison
|
||
if (newPrice !== oldPrice) { /* triggers on noise */ }
|
||
|
||
// ✅ Reliable - compare at cent level
|
||
if (Math.round(newPrice * 100) !== Math.round(oldPrice * 100)) {
|
||
// Real price change detected
|
||
}
|
||
```
|