Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Write JavaScript code in n8n Code nodes. Use when writing JavaScript in n8n, using $input/$json/$node syntax, making HTTP requests with $helpers, working with dates using DateTime, troubleshooting Code node errors, choosing between Code node modes, or doing any custom data transformation in n8n. Always use this skill when a workflow needs a Code node — whether for data aggregation, filtering, API calls, format conversion, batch processing logic, or any custom JavaScript. Covers SplitInBatches lo
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 433% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 132% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 409% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-21 | ✗→✓ | ▲ Improved | 143% | 0% |
Expert guidance for writing JavaScript code in n8n Code nodes.
javascript// Basic template for Code nodes const items = $input.all(); // Process data const processed = items.map(item => ({ json: { ...item.json, processed: true, timestamp: new Date().toISOString() } })); return processed;
$input.all(), $input.first(), or $input.item[{json: {...}}] format$json.body (not $json directly)this.helpers.httpRequest() (no auth — the bare $helpers global is undefined in the task-runner sandbox, so $helpers.httpRequest() throws ReferenceError: $helpers is not defined; ignore the validator if it suggests $helpers), DateTime (Luxon), $jmespath(). Not available: this.helpers.httpRequestWithAuthentication (deny-listed), $env (when N8N_BLOCK_ENV_ACCESS_IN_NODE=true), require() (unless allowlisted). For anything beyond a trivial unauthenticated GET (auth, pagination, retries), prefer the HTTP Request node and keep Code nodes for pure logic.N8N_RUNNERS_ALLOWED_BUILT_IN_MODULES and N8N_RUNNERS_ALLOWED_EXTERNAL_MODULES (legacy: NODE_FUNCTION_ALLOW_BUILTIN / NODE_FUNCTION_ALLOW_EXTERNAL). If the user says their instance allows specific modules (e.g. axios, lodash, crypto), use them via require() — don't refuse. If unsure, ask or default to built-ins only.@n8n/n8n-nodes-langchain.toolCode), stop — that node has a different contract (input via query, must return a string, no $input/$helpers). Use the n8n-code-tool skill.The Code node offers two execution modes. Choose based on your use case:
Use this mode for: 95% of use cases
$input.all() or items arrayjavascript// Example: Calculate total from all items const allItems = $input.all(); const total = allItems.reduce((sum, item) => sum + (item.json.amount || 0), 0); return [{ json: { total, count: allItems.length, average: total / allItems.length } }];
When to use:
Use this mode for: Specialized cases only
$input.item or $itemjavascript// Example: Add processing timestamp to each item const item = $input.item; return [{ json: { ...item.json, processed: true, processedAt: new Date().toISOString() } }];
When to use:
Decision Shortcut:
Mode choice is the single biggest performance lever in a Code node. Each per-item execution context costs a setup tax (measured on n8n 2.x, small records):
| What runs per item | Approx. cost | |---|---| | Code All Items (one run for the whole set) | ~0.02 ms/item | | Expression in any node (IF / Set / etc.) | ~0.2 ms/item | | Code Each Item (a full sandbox per item) | ~0.6 ms/item — ~25–30× All Items |
So Run Once for Each Item over 10k items is ~6 s of pure overhead vs ~0.2 s 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. Expression complexity itself is essentially free (~90% of the cost is the per-item context, not your code) and every node→node hop re-copies all items — so reduce the number of per-item boundaries, don't micro-optimize each one. Below a few hundred items none of this matters; reach for it on the hot path (large item counts, little I/O).
See: DATA_ACCESS.md → "Mode Performance" for the corollaries, hop costs, and scale check.
Four ways to pull data from upstream nodes. Note $node["Name"] and $('Name') need .first().json or .all() — never .json directly.
javascriptconst allItems = $input.all(); // 1. All items — batch ops, aggregation (most common) const data = $input.first().json; // 2. First item — single objects, API responses const item = $input.item; // 3. Current item — "Each Item" mode ONLY (undefined otherwise) const other = $node["Webhook"].json; // 4. Named node — combine data across nodes
Always access fields via .json (e.g. item.json.name, not item.name), and prefer the explicit $input.first().json.field over a bare $json.field.
See: DATA_ACCESS.md for the full guide — every pattern with examples, a decision tree, and the common mistakes (mutating originals, missing length checks, $input.item in the wrong mode).
MOST COMMON MISTAKE: Webhook data is nested under .body
javascript// ❌ WRONG - Will return undefined const name = $json.name; const email = $json.email; // ✅ CORRECT - Webhook data is under .body const name = $json.body.name; const email = $json.body.email; // Or with $input const webhookData = $input.first().json.body; const name = webhookData.name;
Why: Webhook node wraps all request data under body property. This includes POST data, query parameters, and JSON payloads.
See: DATA_ACCESS.md for full webhook structure details
CRITICAL RULE: Always return array of objects with json property
javascript// ✅ Single result return [{ json: { field1: value1, field2: value2 } }]; // ✅ Multiple results return [ {json: {id: 1, data: 'first'}}, {json: {id: 2, data: 'second'}} ]; // ✅ Transformed array const transformed = $input.all() .filter(item => item.json.valid) .map(item => ({ json: { id: item.json.id, processed: true } })); return transformed; // ✅ Empty result (when no data to return) return []; // ✅ Conditional return if (shouldProcess) { return [{json: processedData}]; } else { return []; }
javascript// ❌ WRONG: Object without array wrapper return { json: {field: value} }; // ❌ WRONG: Array without json wrapper return [{field: value}]; // ❌ WRONG: Plain string return "processed"; // ❌ WRONG: Raw data without mapping return $input.all(); // Missing .map() // ❌ WRONG: Incomplete structure return [{data: value}]; // Should be {json: value}
Why it matters: Next nodes expect array format. Incorrect format causes workflow execution to fail.
See: ERROR_PATTERNS.md #3 for detailed error solutions
The most useful Code node shapes from production workflows. One quick example — sum/aggregate across all items:
javascriptconst items = $input.all(); const total = items.reduce((sum, item) => sum + (item.json.amount || 0), 0); return [{ json: { total, count: items.length, average: total / items.length } }];
The full library covers 10 patterns: multi-source aggregation, regex filtering, markdown/structured-text parsing, JSON comparison, CRM/form transformation, release processing, array transformation with computed fields, Slack Block Kit formatting, top-N ranking, and string-aggregation reporting — each with variations.
See: COMMON_PATTERNS.md for the 10 detailed production patterns (and the Best Practices section: validate input, try-catch, filter-early, array methods over loops, console.log debugging).
The recurring Code node failures, in rough frequency order:
return [...], and make sure every branch returns.{{ }}. Use JavaScript: ${$json.field} or $input.first().json.field.return {json:{...}} fails; must be return [{json:{...}}].item.json?.user?.email || 'fallback'.$json.email is undefined; use $json.body.email.httpRequestWithAuthentication) and $env blocked — route secrets through credentials/HTTP Request node, not the Code node sandbox.See: ERROR_PATTERNS.md for the comprehensive guide — each error with wrong/right code, escaping rules, the sandbox restrictions (Errors #6–#7), a prevention checklist, and a quick error-message lookup table.
javascript// HTTP requests (no auth — see sandbox note below) const res = await this.helpers.httpRequest({ method: 'GET', url: 'https://api.example.com/data' }); // DateTime (Luxon): now, formatting, arithmetic const now = DateTime.now(); const formatted = now.toFormat('yyyy-MM-dd'); const tomorrow = now.plus({ days: 1 }); // $jmespath() — query JSON structures const adults = $jmespath($input.first().json, 'users[?age >= `18`]'); // $getWorkflowStaticData() — data that persists across executions
Sandbox (since n8n v2.0, JsTaskRunnerSandbox): the accessor is this.helpers.httpRequest() — the bare $helpers global is undefined here ($helpers.httpRequest() throws ReferenceError). Inside a nested async function where this is lost, call it as await fn.call(this, ...). this.helpers.httpRequestWithAuthentication and this.helpers.requestWithAuthenticationPaginated are deny-listed (→ UnsupportedFunctionError); for authenticated calls use an HTTP Request node with the credential (preferred), a sub-workflow, or a manual Authorization: Bearer ${token} header on this.helpers.httpRequest() only when the token already flows through the workflow as data. $env is blocked when N8N_BLOCK_ENV_ACCESS_IN_NODE=true; require() works only for allowlisted modules. Buffer, URL, and standard JS globals (Math, JSON, Object, Array) always work.
See: BUILTIN_FUNCTIONS.md for the complete reference — full httpRequest options, all DateTime/Luxon operations, JMESPath patterns, static-data use cases, and the sandbox-restriction details.
.json before processing.filter/map/reduce) over manual loops.console.log() for debugging (output goes to the browser console).See: COMMON_PATTERNS.md → "Best Practices" for code examples of each.
Hard-won lessons from real deployments — summarized here, with code in DATA_ACCESS.md → "Production Gotchas":
main[0] = done (fires once, after all batches), main[1] = each batch (the loop body). Add a Limit 1 node after the done output as a safety.batchSize: 1 is the loop equivalent of Each Item — use the largest batch your real constraint (rate limit, page size, memory) allows, or don't loop at all.$('Node Inside Loop').all() returns ONLY the last iteration's items. Accumulate via $getWorkflowStaticData('global') (reset before, push inside, read after).pairedItem: { item: i } or downstream Set nodes fail with paired_item_no_info.$('Node').first().json or $('Node').all() — never .json directly on the reference.Math.round(a*100) !== Math.round(b*100) — to avoid false positives from float noise.> Before reaching for a Code node, walk the transform gatekeeper in the n8n Expression Syntax skill: expression → arrow-function IIFE inside an Edit Fields field → Code node, in that order. The first two paths cover most "transform this data" tasks at ~1–10ms each, versus the Code node's sandboxed ~500–1000ms — a ~100x gap on pure single-item shaping, with no functional difference. The Code node earns its place only for whole-dataset aggregation ($input.all()), allowlisted libraries, or async work. And before writing code for crypto (HMAC, hashing, signing) or XML/SOAP/RSS parsing, check for a native node — n8n has a Crypto node (nodes-base.crypto) and an XML node (nodes-base.xml) that cover those without any JavaScript. Dropping into Code for something a native node already does is one of the most common false positives.
Use Code node when:
Consider other nodes when:
Code node excels at: Complex logic that would require chaining many simple nodes
n8n Expression Syntax:
{{ }} syntax in other nodes{{ }})n8n MCP Tools Expert:
search_nodes({query: "code"})get_node({nodeType: "nodes-base.code"})validate_node({nodeType: "nodes-base.code", config: {...}})n8n Node Configuration:
n8n Workflow Patterns:
n8n Validation Expert:
Before deploying Code nodes, verify:
{json: {...}}$input.all(), $input.first(), or $input.item ${value} .body if from webhookReady to write JavaScript in n8n Code nodes! Start with simple transformations, use the error patterns guide to avoid common mistakes, and reference the pattern library for production-ready examples.
Other measured skills in the registry, with their headline benchmark lift.