N8n Code Nodes Official
n8nOfficialPublished by n8n
- Skill
- Guidelines
- Automation and integrations
- Apache-2.0
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The Code node is a powerful tool in n8n, but it is frequently misused for tasks better handled by native nodes or expressions. This guide establishes a decision hierarchy to ensure you only use Code when necessary. By prioritizing expressions and Edit Fields with arrow functions, you can significantly improve workflow performance, readability, and debuggability. Code nodes should be reserved for complex multi-source aggregation, external library usage, or stateful constructs that cannot be achieved through standard n8n components.
What it does
- Provides a decision tree to determine if a task requires a Code node or a simpler alternative.
- Explains how to use Edit Fields with arrow functions for multi-line logic without the overhead of a Code node.
- Identifies common anti-patterns where Code nodes are used unnecessarily for simple transformations.
- Outlines legitimate use cases for Code nodes, such as multi-source aggregation and complex statistical operations.
- Clarifies when to use native nodes like Crypto or XML instead of writing custom JavaScript.
- Offers performance insights comparing the overhead of the sandboxed Code node versus in-process expressions.
When to use it
- When you need to perform multi-source aggregation across multiple upstream nodes.
- When you require complex statistical operations that depend on the entire dataset.
- When you need to utilize external libraries not available in native nodes.
- When you have complex, multi-step logic that cannot be cleanly expressed in an Edit Fields node.
- When you have existing, complex logic that is impractical to refactor into native nodes.
Advantages
- Reduces workflow complexity by favoring native nodes and expressions.
- Improves execution performance by avoiding unnecessary sandbox overhead.
- Increases maintainability by keeping logic visual and modular.
- Ensures better debugging by using native tools that provide clearer error reporting.
- Prevents common pitfalls like manual binary handling or redundant XML parsing.
Usage examples
- “How do I transform a single field value in n8n?”
- The guide directs you to use an expression in an Edit Fields node (e.g., {{ $json.name.toUpperCase() }}) rather than a Code node.
- “I need to aggregate data from three different nodes, should I use a Code node?”
- Yes, this is a valid use case for a Code node as it involves multi-source aggregation across the whole dataset.
- “How do I hash a string in n8n?”
- The guide instructs you to use the native Crypto node instead of writing custom JavaScript with require('crypto').
- “Can I write multi-line logic without a Code node?”
- Yes, you can use an immediately-invoked arrow function inside an Edit Fields node to handle complex logic while remaining in-process.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/n8n-io/n8n-code-nodes-official/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip n8n-code-nodes-official.zip -d .agents/skills
Documentation by n8n
n8n Code Nodes
The Code node is powerful and often the wrong tool. The n8n equivalent of dropping into raw SQL when an ORM would do: real cases exist, but the moment a Code node handles logic an expression could, the workflow is harder to read, debug, and maintain. There's also a real perf gap: Code runs in a sandboxed JS runtime, expressions and Edit Fields run in-process, and the per-invocation overhead can be hundreds of times higher in Code (anecdotally ~2ms vs ~600ms for equivalent logic). For hot paths and large item counts, that compounds.
Strong defaults
Code node is a last resort. Decision order: expression (
{{...}}) → arrow function inside Edit Fields → Code node. The first two paths cover most "transform this data" tasks. Code earns its place for multi-source aggregation, external libraries, and a few specific patterns documented below.Default to JavaScript. Write JS unless the user explicitly asked for Python ("use Python here," "I'm a Python shop," pasted Python code). Everywhere else in n8n (expressions, Edit Fields) is JS, JS has a curated library allowlist (
lodash,crypto,luxon).
Decision tree
Need custom logic?
├── Is it a transformation of one or two fields?
│ └── Expression: {{ $json.foo.toUpperCase() }} or {{ $json.items.map(item => item.name).join(', ') }}
│ Most "just transform this" cases land here.
│
├── Is it multi-line, but pure data shaping (map, filter, reduce, conditional)?
│ └── Edit Fields with arrow function expression. See references/ARROW_FUNCTIONS_IN_EDIT_FIELDS.md
│
├── Does it need full statements, multiple data sources, or external libs?
│ ├── Are you SURE the above two don't work?
│ │ └── Re-read the parent. The bar is high.
│ └── Yes, genuinely needs it
│ └── Code node. See references/JAVASCRIPT_PATTERNS.md
│
└── Is it actually two separate transformations stitched together?
└── Use two nodes (Edit Fields → Edit Fields, or Edit Fields → IF). Composability beats one big Code block.For the full decision logic with examples for each branch, see references/DECISION_TREE.md.
What expressions can do that people forget
Common reaches-for-Code-node that should be expressions:
// ❌ Code node
return { name: $input.first().json.name.toUpperCase() }
// ✅ Expression in Edit Fields, "name" field
{{ $json.name.toUpperCase() }}// ❌ Code node
const items = $input.first().json.items
return { tags: items.map(item => item.tag).filter(tag => tag).join(', ') }
// ✅ Expression
{{ $json.items.map(item => item.tag).filter(tag => tag).join(', ') }}// ❌ Code node
const date = new Date($input.first().json.created_at)
return { formatted: date.toISOString().slice(0, 10) }
// ✅ Expression with n8n's date extension
{{ $json.created_at.toDateTime().format('yyyy-MM-dd') }}For more on what expressions can express, see the n8n-expressions-official skill.
What arrow-functions-in-Edit-Fields can do
Edit Fields assigns field values via expression. Inline arrow functions get you most multi-line logic without the Code node:
// In Edit Fields, "summary" field:
{{ (() => {
const items = $json.items
const total = items.reduce((sum, item) => sum + item.price, 0)
const tax = total * 0.08
return `Total: $${(total + tax).toFixed(2)}`
})() }}Right tool for "logic slightly too gnarly for a one-liner." See references/ARROW_FUNCTIONS_IN_EDIT_FIELDS.md for patterns and formatting.
When the Code node IS the right answer
Real uses exist. Bar is high, not "never." The cases below are legitimate. Build with code without apologizing.
Multi-source aggregation across the whole dataset
When a node needs to:
Read from multiple upstream nodes simultaneously (e.g.,
$('Source A').all(),$('Source B').all(),$('Source C').first().json).Compute statistics or transformations across all items at once (not per-item).
Apply multi-step logic with intermediate data structures (lookup maps, accumulators, ratios).
Most common valid case. Examples:
Analytics rollups: per-group averages, percentile scoring, normalization across categories.
Building lookup tables from one source and joining against rows from another.
Computing offsets/ratios across two reference datasets, then applying to a third.
// Real-world shape: aggregate test results from one source,
// model metadata from another, category mapping from a third,
// and produce per-model-per-category averages.
const testResults = $('Get Test Results').all().map(item => item.json)
const models = $('Get Models').all().map(item => item.json)
const categoryMap = $('Get Category Map').first().json.testCategoryMap
const categoryByTestId = Object.fromEntries(
categoryMap.map(mapping => [mapping.testId, mapping.category])
)
const result = models.map(model => {
const modelTests = testResults.filter(test => test.modelId === model.id)
const stats = modelTests.reduce((acc, test) => {
const cat = categoryByTestId[test.testId]
if (!cat) return acc
acc[cat] ??= { scored: 0, available: 0, count: 0 }
acc[cat].scored += test.pointsScored ?? 0
acc[cat].available += test.pointsAvailable ?? 0
acc[cat].count += 1
return acc
}, {})
const averages = Object.fromEntries(
Object.entries(stats).map(([category, stat]) => [
category,
{ avg: stat.available > 0 ? stat.scored / stat.available : 0, n: stat.count }
])
)
return { modelId: model.id, modelName: model.modelName, ...averages }
})
return result.map(json => ({ json }))Technically expressions can reach across items via $('Node Name').all() and reduce inline, but for anything with this much shape (joins, group-bys, nested aggregation) the result is a one-line megaexpression that's hard to read and impossible to debug. Use Code.
External libraries
JS Code can require from a curated allowlist (lodash, etc.), but expressions can't.
Always check for a native node first. n8n has more native nodes than people realize. Dropping into Code for something with a native node is a recurring mistake. The next two subsections cover the specific traps.
Cryptographic operations: use the Crypto node, not Code
HMAC, signing, hashing, encryption: n8n has a native Crypto node (n8n-nodes-base.crypto). Use it. It handles SHA256, MD5, HMAC, encrypt/decrypt, and random generation, all without writing JavaScript.
// WRONG (recurring AI slip):
const crypto = require('crypto')
const hash = crypto.createHash('sha256').update(buf).digest('hex').slice(0, 12)
// RIGHT: configure the Crypto node with operation: 'hash', type: 'SHA256'
// then read $('Hash').item.json.<output> downstream.The Code-with-require('crypto') pattern is one of the most common false positives for "this needs a Code node." It doesn't. The Crypto node covers it.
Hashing binary, not strings
Don't reach for Code just because you need to hash binary (a PDF, an image, a file buffer). The Crypto node has a binaryPropertyName parameter. Point it at the binary slot key and it hashes the buffer directly. You don't need this.helpers.getBinaryDataBuffer(...) in user code.
// WRONG (recurring AI slip with binary):
const crypto = require('crypto')
const buf = await this.helpers.getBinaryDataBuffer($itemIndex, 'data')
const hash = crypto.createHash('sha256').update(buf).digest('hex')
// RIGHT: Crypto node configured to hash binaryPropertyName='data', SHA256.
// Then $('Crypto').item.json.<output> has the hash; chain a Set node for any field shaping.The remaining valid Code-for-crypto case: a non-standard signing scheme that the Crypto node doesn't expose (e.g., a custom AWS-style signature), AND httpCustomAuth credential doesn't fit either. Rare. Justify explicitly.
XML / SOAP / RSS parsing: use the XML node, not Code
n8n has a native XML node (n8n-nodes-base.xml) with parse and stringify operations. It already converts XML to JSON. Once it has, the result is plain JSON and Edit Fields with arrow function expressions handles all the field extraction, array normalization (Array.isArray(...) ? ... : ...), and link-finding (.find()) you'd reach for Code to do.
// WRONG (recurring AI slip):
// XML node already parsed → another Code node to extract a few fields:
const entry = $('Parse XML').item.json.feed.entry
const firstEntry = Array.isArray(entry) ? entry[0] : entry
return { json: { title: firstEntry.title, url: firstEntry.link.find(link => link.type === 'pdf').href } }
// RIGHT: Edit Fields with arrow function expressions:
// title: ={{ (() => { const entry = $('Parse XML').item.json.feed.entry; return Array.isArray(entry) ? entry[0].title : entry.title; })() }}
// pdfUrl: ={{ $('Parse XML').item.json.feed.entry.link.find(link => link.type === 'pdf')?.href }}If the field-extraction logic is genuinely too gnarly for inline expressions even with multi-line arrow functions, the next stop is Edit Fields with a single multi-line arrow function, NOT a Code node. See references/ARROW_FUNCTIONS_IN_EDIT_FIELDS.md.
What these have in common
Valid cases are about scope: whole-dataset, multiple sources, or stateful constructs single-item tools can't reach. Invalid cases: Code doing what an expression could, OR what a native node already does.
Quick tests:
"Could I describe this Code node's job as 'take this one item and...'?" If yes, wrong tool.
"Is there a native node for this?" Search via
search_nodesfirst. Crypto, XML, JSON parsing, date math (Luxon), HTTP calls, file I/O, regex matching: all have native nodes or expression-level support.
JavaScript Code node specifics
Two modes:
Run Once for All Items (default, use this): runs once with
$input.all(). If you need per-item logic, justfor (const item of $input.all())inside. This is the standard shape and almost always what you want.Run Once for Each Item: runs once per item with
$input.first()(or$input.item).
Common shape:
// Run Once for All Items
const items = $input.all()
const totals = items.map(item => ({
...item.json,
total: item.json.qty * item.json.price,
}))
return totals.map(json => ({ json }))The return must be an array of { json: ... } objects (or { json: ..., binary: ... }), not raw JSON.
For binary handling, error patterns, and Code-node-only bugs, see references/JAVASCRIPT_PATTERNS.md.
Reference files
| File | Read when |
|---|---|
references/DECISION_TREE.md | You're tempted to use a Code node and want to verify the simpler paths really don't work |
references/ARROW_FUNCTIONS_IN_EDIT_FIELDS.md | The transformation is multi-line but pure data shaping |
references/JAVASCRIPT_PATTERNS.md | Code node is genuinely needed and JS is the language |
Anti-patterns
| Anti-pattern | What goes wrong | Fix |
|---|---|---|
Code node doing return { x: $input.first().json.x.toUpperCase() } | Whole node for one expression | Replace with an Edit Fields expression |
| Code node building HTML strings for an email body | The Email node's body field accepts expressions | Inline the expression into the email node |
Code node using new Date() for date formatting | Loses to Luxon's clarity | Use Luxon in expression. See n8n-expressions-official |
| Set node + Code node combo (Set builds inputs, Code transforms) | Two nodes for what should be one Edit Fields | Collapse into one Edit Fields with arrow function |
| Pasting credentials/tokens into Code node text | Same leak as text fields | Use credentials, not Code node |
Files
- ATTRIBUTION.mdAdded by AutoGineer852 B
- LICENSELicence file added by AutoGineer11.0 KiB
- SKILL.md12.0 KiB
- references/ARROW_FUNCTIONS_IN_EDIT_FIELDS.md8.3 KiB
- references/DECISION_TREE.md5.8 KiB
- references/JAVASCRIPT_PATTERNS.md6.0 KiB
references/ARROW_FUNCTIONS_IN_EDIT_FIELDS.md
Arrow functions inside Edit Fields
This pattern handles "logic too gnarly for one line, but just data shaping" without a Code node. An immediately-invoked arrow function inside an Edit Fields expression.
The workhorse pattern for n8n custom logic.
The shape
In an Edit Fields node, set a field's value to:
{{ (() => {
// multi-line logic here
const x = $json.someField
const y = doSomething(x)
return y
})() }}Breakdown:
{{ ... }}: n8n expression delimiter.(() => { ... })(): arrow function defined and immediately invoked.Inside: any JS function body.
return: the field's value.
What you can do inside
Pretty much anything in plain JS:
Multiple
const/let.if/else,switch.for/while(use sparingly, sincemap/filter/reduceis usually cleaner).try/catch.Inline regex.
Native JS stdlib:
Math,JSON,String,Array,Object,Number,Map,Set. Prefer Luxon overDate.Luxon (
DateTime,Interval,Duration).
What you can't do:
require()external libs: Code-node-only.await: expressions are synchronous.
Edit Fields can call $input.all() and $('Other Node').all() though, and that's where this gets powerful: pair it with the Execute Once toggle on the node and you get one-shot cross-item aggregation without a Code node. See "Cross-item aggregation with Execute Once" below.
When to use
Use arrow functions in Edit Fields when the logic is too long for one line, you need intermediate variables, you want comments, or you'd otherwise reach for Code. Works for both per-item logic (default mode) and cross-item aggregation (with Execute Once toggled on, see below).
Code is the right answer when the aggregation logic is too gnarly for an inline arrow function, you need external libs, or you need async.
Performance: this is the other reason to prefer arrow functions
Code nodes run in a sandboxed JS runtime, separate from n8n's main process. Each invocation pays sandbox setup, value marshaling in and out, and isolation overhead. Edit Fields arrow functions run in n8n's expression engine, in-process, with none of that.
The gap is large in practice. Anecdotally, the same logic has been measured at ~2ms in an Edit Fields arrow function vs ~600ms in a Code node. The exact ratio varies with item count and code shape, but the order of magnitude is real.
For most workflows this doesn't matter. The Code node's overhead is a rounding error next to one HTTP call. But when it matters:
Hot paths. High-frequency triggers (webhook per request, scheduled-every-minute, chat messages on a busy channel).
Large item counts. Per-item Code execution at 600ms × 10000 items adds up fast, even when each call is "instant" by human standards.
Latency-sensitive surfaces. Synchronous chat replies, request-response webhooks where total response time is user-visible.
In those cases, cramming what would naturally be a Code node into an Edit Fields arrow function can be worth the readability hit. It's ugly: a 50-line IIFE inside {{...}} is harder to read than a clean Code node. But if profiling shows the Code node is the bottleneck, the win is real and measurable. This is a deliberate trade, not a default.
Examples
Conditional field assembly
{{ (() => {
const status = $json.status
const dueDate = $json.due_date
? DateTime.fromISO($json.due_date)
: null
const isOverdue = dueDate
&& dueDate < DateTime.now()
&& status !== 'completed'
if (isOverdue) {
return `⚠️ OVERDUE: ${$json.title}`
}
if (status === 'in_progress') {
return `🔄 ${$json.title}`
}
if (status === 'completed') {
return `✅ ${$json.title}`
}
return $json.title
})() }}Branching logic is awkward in a single-line ternary chain and overkill for Code.
Building structured data for an email
{{ (() => {
const items = $json.line_items
const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0)
const tax = subtotal * 0.08
const total = subtotal + tax
const rows = items
.map(item => `<tr><td>${item.name}</td><td>${item.qty}</td><td>$${item.price.toFixed(2)}</td></tr>`)
.join('\n')
return `
<h1>Invoice #${$json.invoice_id}</h1>
<table>
<thead><tr><th>Item</th><th>Qty</th><th>Price</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<p>Subtotal: $${subtotal.toFixed(2)}</p>
<p>Tax: $${tax.toFixed(2)}</p>
<p><strong>Total: $${total.toFixed(2)}</strong></p>
`
})() }}Historically gets written in a Code node and read into an email node. The arrow-function-in-Edit-Fields version keeps it in one place. The email body is in the Edit Fields parameter, not in some upstream Code output.
Parsing freeform user input
{{ (() => {
const text = $json.user_message.toLowerCase()
// Detect intent from common phrasings
const wantsHelp = /\b(help|stuck|how do i)\b/.test(text)
const wantsCancel = /\b(cancel|unsubscribe|stop)\b/.test(text)
const wantsContact = /\b(contact|reach|talk to)\b/.test(text)
if (wantsCancel) return 'cancel'
if (wantsHelp) return 'help'
if (wantsContact) return 'contact'
return 'unknown'
})() }}Sanitizing input
{{ (() => {
let title = $json.title
title = title.trim()
title = title.replace(/\s+/g, ' ') // collapse whitespace
title = title.replace(/[^\w\s\-.]/g, '') // strip non-alphanumeric (keep dash and dot)
title = title.slice(0, 100) // cap length
return title
})() }}Formatting rules
Past ~5 lines, formatting matters. Indent generously, comment intent.
{{ (() => {
// Compute stripe-style amount in cents from dollar amount
const dollars = parseFloat($json.amount)
if (isNaN(dollars) || dollars < 0) {
return null
}
const cents = Math.round(dollars * 100)
// Cap at Stripe's max (per their docs)
return Math.min(cents, 99999999)
})() }}Aim for 4-space indents inside the function. Comments above non-obvious blocks. People read this six months from now.
For broader rules, see the n8n-expressions-official skill.
Common mistakes
Forgetting to invoke the function
// DON'T: defines but doesn't call
{{ () => {
return $json.foo.toUpperCase()
} }}The result of this expression is the function itself, not its return value. Always close with () to invoke:
{{ (() => {
return $json.foo.toUpperCase()
})() }}Returning nothing
{{ (() => {
const x = $json.foo
x.toUpperCase() // ← no return
})() }}The expression evaluates to undefined. Always end with an explicit return.
Cross-item aggregation with Execute Once
By default Edit Fields runs once per input item, so calling $input.all() inside an arrow function re-evaluates the same array N times. Toggle Execute Once on the node and the expression runs once total, producing one output item. That combination is the lightweight alternative to a Code node for aggregations:
// Execute Once enabled on the Edit Fields node
{{ (() => {
const items = $input.all().map(item => item.json)
const total = items.reduce((sum, item) => sum + item.amount, 0)
return `Processed ${items.length} items, total $${total.toFixed(2)}`
})() }}The same shape works for $('Other Node').all() when you want to aggregate across an upstream node specifically. Reach for a Code node only when the aggregation logic is too gnarly for an inline arrow function (joins, group-bys, multiple intermediate structures).
Without Execute Once, the expression still resolves, you just pay the cost N times and produce N identical output items, which is almost never what you want.
Awaiting
// DON'T
{{ (async () => {
const data = await fetch(...) // expressions are synchronous
})() }}If you need async work, that's a separate node (HTTP Request, etc.), not an arrow function.
When this gets too big
Past ~30 lines, ask:
Could this be split into multiple Edit Fields steps?
Is this genuinely a Code case? (See
DECISION_TREE.md.)Is it domain-specific enough to be a sub-workflow? (See
n8n-subworkflows-official.)
Great for "a bit too gnarly for one line." Not great for "I'm writing a small program."
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- n8n
- Publisher
- n8n (Official)
- Source
- n8n-io/skills · skills/n8n-code-nodes-official · commit 27b6b9f
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by n8n or n8n.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download n8n-code-nodes-official.zip