When you're building workflows with n8n, the Code node is your workhorse for custom logic — but most tutorials stop at the basics. They show you console.log() and $input.item.field, then move on. Real practitioners hit walls faster:
- Multi-language challenges: Should I use JavaScript or Python? Can I mix them in one workflow?
- Loop control: How do I break out of a loop early without processing all items?
- Error handling: What's the right pattern for try-catch in a Code node? When do I throw vs. silently return null?
-
Dynamic field access: How do I use
require()orn8n.evaluateExpression()safely without killing performance? - Item processing: Should I process items one at a time or fetch them all, and what breaks when I get it wrong?
This guide covers all five + real, copy-paste workflows.
Section 1: JavaScript vs Python — Trade-offs and Gotchas
When to choose each language
JavaScript — the default:
- Faster (V8 runtime, no startup overhead)
- Direct access to n8n's built-in helpers:
n8n.$env,n8n.executionId,n8n.getWorkflowMetadata(),DateTime,Math,crypto - Synchronous by default (no async/await issues unless you write them)
- Best for: quick transformations, API responses, date/time math
Python — for broader ecosystems:
- Access to pip packages (numpy, pandas, requests, etc.) if pre-installed in your n8n instance
- Better for data science: Excel parsing, ML scoring, statistical analysis
- Separate sandbox; can't directly share state with JSON/SET nodes (must serialize via JSON strings)
- Startup cost per execution (~100ms+)
- Best for: heavy computation, leveraging existing Python code, tabular data processing
The mixing problem: multi-language workflows
You can use both in one workflow (JavaScript Code node → output → Python Code node → next step), but state doesn't flow directly between them:
// JavaScript Code node — transform API response
return {
users: data.map(u => ({id: u.id, name: u.name}))
};
The output becomes input to a Python Code node as a JSON string. Python sees it as a dictionary:
import json
users = json.loads($input.all()[0]['json']['users'])
# Now you have a Python list of dicts
Best practice: Choose one language per workflow unless you have a specific reason (e.g., pre-existing Python script you must use). Mixing adds complexity and serialization overhead.
Workflow JSON: JavaScript + Python in one flow (reference)
{
"nodes": [
{
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"position": [250, 300],
"parameters": {
"url": "https://jsonplaceholder.typicode.com/users",
"method": "GET"
}
},
{
"name": "JavaScript Transform",
"type": "n8n-nodes-base.code",
"position": [450, 300],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const users = $input.getAll().map(item => ({\n id: item.json.id,\n email: item.json.email,\n company: item.json.company?.name || 'N/A'\n}));\n\nreturn { users_json: JSON.stringify(users) };"
}
},
{
"name": "Python Stats",
"type": "n8n-nodes-base.code",
"position": [650, 300],
"parameters": {
"language": "python",
"mode": "runOnceForAllItems",
"pythonCode": "import json\nusers = json.loads($input.all()[0]['json']['users_json'])\nreturn {'count': len(users), 'first_email': users[0]['email']}"
}
}
],
"connections": {
"HTTP Request": { "main": [[{ "node": "JavaScript Transform", "branch": 0, "type": "main" }]] },
"JavaScript Transform": { "main": [[{ "node": "Python Stats", "branch": 0, "type": "main" }]] }
}
}
Section 2: Loop Control — Breaking Early Without Running Every Item
The problem
n8n's loop-over-items has no native break statement. If your Code node receives 10 items, it processes all 10 — unless you design around it.
Common scenarios where early exit matters:
- Cost limits: Process Stripe customers until cumulative charges > $500, then stop
- Search queries: Scrape URLs until you find the target
- Rate limits: Hit an API until you get a 429
Four patterns
Pattern 1: Filter BEFORE the loop (preferred)
Use a FILTER node to reduce items upfront. Clean, no errors, predictable.
Pattern 2: Cascade throw
let cumulativeCost = 0;
$input.getAll().forEach(item => {
cumulativeCost += item.json.amount;
if (cumulativeCost > 500) throw new Error("Cost limit exceeded");
});
Stops immediately but node fails — use only if you want loud failure.
Pattern 3: Conditional branch
Process all items but mark items for skip, then use downstream IF node to filter. No errors, but still processes all items.
Pattern 4: Slice at query time (most efficient)
// SQL: SELECT * FROM orders LIMIT 10;
// API: /api/records?limit=10&offset=0
Limit items at the source. No wasted compute.
Workflow JSON: Loop with cost threshold
{
"nodes": [
{
"name": "Check Cost",
"type": "n8n-nodes-base.code",
"position": [450, 300],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "let cumulative = 0;\nconst withCumulative = [];\n\nfor (const item of $input.getAll()) {\n cumulative += item.json.amount;\n withCumulative.push({\n ...item.json,\n cumulative_total: cumulative,\n exceed_budget: cumulative > 500\n });\n if (cumulative > 500) break;\n}\n\nreturn withCumulative;"
}
}
]
}
Section 3: Error Handling — try-catch and Resilience Patterns
Basic try-catch
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (error) {
console.error("API fetch failed:", error.message);
return { error: error.message, status: "failed" };
}
Key insight: catch stops exception propagation. To fail the workflow, re-throw: throw error;
Pattern: Retry with exponential backoff
async function fetchWithRetry(url, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url);
if (response.ok) return response.json();
if (response.status === 429) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000));
continue;
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(r => setTimeout(r, 1000));
}
}
}
return await fetchWithRetry("https://api.example.com/data");
Tip: n8n's built-in Retry settings (node properties) are simpler and don't block the worker — prefer them over in-Code retry loops.
Pattern: Graceful degradation
try {
return $node["Premium API"].json;
} catch (error) {
console.warn("Premium API failed, using fallback:", error.message);
return { data: [], source: "fallback" };
}
Workflow JSON: Per-item error handling
{
"nodes": [
{
"name": "Process Items",
"type": "n8n-nodes-base.code",
"position": [450, 300],
"parameters": {
"mode": "runOnceForAllItems",
"jsCode": "const results = [];\n\nfor (const item of $input.getAll()) {\n try {\n if (Math.random() > 0.8) throw new Error('Rate limited');\n results.push({ id: item.json.id, status: 'success' });\n } catch (error) {\n results.push({ id: item.json.id, status: 'error', error: error.message });\n }\n}\n\nreturn results;"
}
}
]
}
Section 4: Advanced Item Processing — getAll(), map(), reduce()
Single vs. multiple item mode
In Code node properties:
-
Run Once For All Items (
runOnceForAllItems) — receive ALL items; use$input.getAll() -
Run Once Per Item (default) — called once per item; use
$input.item
Switch to "Run Once For All Items" when you need to aggregate across items.
Item structure
// Each item in getAll() has this shape:
{
json: { /* your data */ },
binary: { /* file uploads */ },
index: 0,
pairedItem: null
}
// Access: item.json.field — NOT item.field
Aggregation example
const all = $input.getAll();
// Group by user_id and sum amounts
const byUser = {};
for (const item of all) {
const uid = item.json.user_id;
if (!byUser[uid]) byUser[uid] = { user_id: uid, total: 0, count: 0 };
byUser[uid].total += item.json.amount;
byUser[uid].count += 1;
}
return Object.values(byUser);
Section 5: require() and evaluateExpression() — Dynamic Field Access
Why require() doesn't work
n8n Code node runs in a sandboxed V8 context — no file system, no npm imports:
const axios = require('axios'); // ❌ Error: require is not defined
Workaround: Use native fetch() (built-in, no import needed).
n8n.evaluateExpression() for dynamic lookups
// When the field name is in a variable at runtime:
const fieldName = $input.item.json.lookup_key; // e.g., "user_email"
const value = n8n.evaluateExpression(
`{{ $node["Previous Node"]["json"]["${fieldName}"] }}`,
$input.item
);
Gotcha: evaluateExpression() is slow — parsing + execution overhead per call. Use direct field access (item.json.field) for static lookups. Only use this for truly dynamic field names.
Conclusion: Code Node Mastery
Five rules for production-grade Code nodes:
- Pick one language — JavaScript unless you have a specific Python need
- Filter before the loop — don't process items you'll discard
- Wrap external calls in try-catch — never let unexpected errors crash silently
-
Use
runOnceForAllItemswhen aggregating — clearer than per-item loops -
Avoid
evaluateExpression()in loops — it's ~10× slower than direct field access
The real skill is knowing when NOT to use the Code node. A SET node for field selection, a FILTER node for conditions, or an Aggregate node for sums are simpler and faster.
🔗 Want copy-paste workflows for Stripe, Postgres, Redis, S3, and 40+ other integrations? Grab the n8n Workflow Starter Pack — 29 production-ready workflow JSONs for $29.
What's the most complex pattern you've built in the Code node? Looping with early exit? Multi-language workflows? Share your gotchas below.
Top comments (1)
What's the trickiest Code node pattern you've hit in production? I keep running into the
require()sandbox limitation when trying to use npm packages — nativefetch()covers most cases but sometimes you want lodash or moment. Have you found good workarounds? Also curious: do you mix JavaScript and Python in the same workflow, or do you pick one and stick with it?