DEV Community

Pirate Prentice
Pirate Prentice

Posted on

n8n Code Node: Advanced Patterns — Multi-Language, Loops, and Error Handling (Free Workflow JSON)

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() or n8n.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}))
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" }]] }
  }
}
Enter fullscreen mode Exit fullscreen mode

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");
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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" };
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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" };
}
Enter fullscreen mode Exit fullscreen mode

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;"
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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:

  1. Pick one language — JavaScript unless you have a specific Python need
  2. Filter before the loop — don't process items you'll discard
  3. Wrap external calls in try-catch — never let unexpected errors crash silently
  4. Use runOnceForAllItems when aggregating — clearer than per-item loops
  5. 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)

Collapse
 
pirateprentice profile image
Pirate Prentice

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 — native fetch() 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?