1. Introduction and Core Fundamentals
n8n is a robust, open-source workflow automation tool and low-code platform built on Node.js, making JavaScript its native language. While standard nodes (such as Set, If, Merge, or Switch) allow for visual, linear task execution, the Code Node and Inline Expressions represent the "engine room" of sophisticated automation. These tools empower users to implement custom logic, complex data manipulations, and performance optimizations that standard nodes cannot achieve.
1.1 The Role of JavaScript and TypeScript
The Code Node is the unrestricted layer of the platform. Unlike Python (which runs via an adapter in standard setups), JavaScript in n8n runs natively, offering the highest possible performance for data manipulation. A single Code node can often replace a "spaghetti" chain of 10 to 15 standard nodes (like Set or If), resulting in cleaner, more maintainable, and significantly more performant workflows.
Why Use JS/TS in n8n?
- Native Speed: No serialization overhead between the host and the interpreter (in Standard mode).
- Complex Logic: Handle nested loops, complex conditionals, regex operations, and advanced control flow.
- Data Shaping:
map,reduce,filter, andflatMapare exponentially faster than visual loops for large arrays. - Async/Await: First-class support for asynchronous operations and Promises.
- TypeScript: With External Task Runners, you can enforce Type Safety, preventing runtime errors in critical financial or data pipelines.
- Batch Processing: Necessary when processing multiple items together or performing cross-item logic.
1.2 Performance Benefits and Benchmarks
The performance benefits of consolidating logic into a Code node are substantial. Processing arrays with thousands of items via multiple node transitions incurs significant overhead due to the instantiation cost of multiple node contexts.
Benchmarks (Processing a dataset of 1,000 items):
- Code Node: ~580ms to ~582ms (Compiled V8 execution; reduced node transition overhead).
- Standard Set/If Chains: ~1,800ms to ~1,820ms+ (Node instantiation overhead).
Verdict: For batch processing (>100 items) or high-volume data, the Code Node is approximately 3x faster and is mandatory for performance and scalability.
2. Architecture: Standard vs. External Task Runner
To use TypeScript or Custom NPM packages, you must understand where your code executes. You must select one of two architectural options.
2.1 Option A: Standard Execution (Default)
- Engine: The main n8n Node.js process (using
vm2sandbox). - Best For: Standard transformations, speed, lightweight logic.
- Startup: Instant (0ms latency).
- Limitations:
- No TypeScript. Pure JS only.
- Shared Resources: Heavy CPU scripts block the main n8n thread (can freeze the UI).
- Restricted Libs: Cannot easily install custom NPM packages (requires restarting main container). Standard
requireis restricted. No direct file system access (fs) or shell commands (os.system) unless explicitly allowed.
2.2 Option B: External Task Runner (The Enterprise Choice)
- Engine: A separate, isolated Docker container (The "Runner").
- Best For: TypeScript, Heavy CPU tasks, Custom NPM libraries (
axios,lodash,zod). - Capabilities: Full isolation. If a script crashes, n8n stays alive.
- TypeScript: Can run TS natively via
tsxor Node's--experimental-strip-types(Node 22+). - Startup: Slight network latency (~10ms) over WebSocket, but unlimited scalability.
2.3 Configuration: Enabling TypeScript & Custom Packages
To run TypeScript or use libraries like lodash or zod, you generally need Option B (External Runner).
Step 1: Build a Custom Runner Image
The default runner image is generic. You must extend it to add TypeScript support and your desired libraries.
Dockerfile (for External Runner):
# Use the official n8n runner image (match your n8n version)
FROM n8nio/runners:latest
USER root
# 1. Install TypeScript and the 'tsx' loader (for fast TS execution)
# 2. Install custom libraries (e.g., lodash, date-fns, zod)
RUN pnpm add typescript tsx lodash date-fns zod axios
# Copy your configuration file
COPY n8n-task-runners.json /etc/n8n-task-runners.json
USER runner
Step 2: Configure the Allow List
You must strictly allow these modules in your n8n-task-runners.json file.
n8n-task-runners.json:
{
"task-runners": {
"js": {
"env-overrides": {
# Allow these packages to be imported in the Code Node
"NODE_FUNCTION_ALLOW_EXTERNAL": "lodash,date-fns,zod,axios,typescript",
# Enable Node's native type stripping (Node 22+) or generic settings
"NODE_OPTIONS": "--experimental-strip-types"
}
}
}
}
3. Supported Languages and Syntax
3.1 JavaScript (ES6+)
This is the native language of n8n and supports modern ECMAScript features:
- Arrow functions, Destructuring assignment, Template literals.
- Async/await patterns, Spread operators.
- Optional chaining (
?.) and nullish coalescing (??).
3.2 Python
Users can utilize Python for scripting, with specific environment nuances:
- Pyodide: WASM-based Python running inside Node.js. Blocks
os.systemandimportlibfor security. - Native Python: Requires Task Runner setup.
- Variable Renaming: Python syntax forbids variables starting with
$. n8n automatically renames them:-
$input→_input -
$json→_json -
$node→_node -
$vars→_vars
-
3.3 The Sandbox Environment (vm2)
n8n utilizes vm2 (NodeVM) to isolate code execution.
- Restrictions: Standard Node.js libraries (like
fsoros) are not accessible by default. - Console Output:
console.logstatements are redirected to the UI execution panel or server logs depending on deployment. - External Modules: Access requires self-hosted instances with the
NODE_FUNCTION_ALLOW_EXTERNALenvironment variable set (e.g.,NODE_FUNCTION_ALLOW_EXTERNAL=lodash,axios).
4. Inline Expressions vs. Code Node
Understanding the distinction between these two layers is critical.
4.1 Comparative Overview
| Aspect | Inline Expressions | Code Node |
|---|---|---|
| Where | Directly in node parameters (toggle “Fixed / Expression”). | A separate, dedicated node. |
| Syntax | Single-line JS wrapped in {{ }}. |
Multi-line JS (ES6+) or Python. |
| Line Limits | Must remain on a single line. | No limits; supports functions, classes, async. |
| Ideal Use Case | Quick transforms (.isEmail()), field mapping. |
Complex logic, loops, batch processing. |
| Data Handling | Returns a value immediately; no intermediate steps. | Multi-step processing with variables. |
| Loops/Async | ❌ No loops, no async. | ✅ for, .map, await supported. |
| Type Safety | ❌ No. | ✅ Interfaces / Enums (with TS Runner). |
4.2 The Tournament Templating Engine
Since n8n v1.9.0, inline expressions use the "Tournament" engine, providing "syntactic sugar" for data transformations.
Critical Warning: These built-in transformation functions (e.g., .isEmail()) are not standard JavaScript methods. They work inside {{ ... }} but will fail if used directly on a variable inside a Code Node script. To use them within a Code Node, you must wrap them in the $evaluateExpression() helper:
const isEmail = $evaluateExpression('{{ $json.email.isEmail() }}', itemIndex);
5. Inline Expressions: Built-in Transformations Reference
These methods are available within {{ }} syntax.
5.1 String Transformations
-
.isEmail(): Checks for valid email format.{{ "john@example.com".isEmail() }}→true -
.extractDomain(): Extracts domain from URL.{{ "https://google.com/path".extractDomain() }}→"google.com" -
.removeTags(): Removes HTML tags. -
.base64Encode()/.base64Decode(): Base64 operations. -
.toSnakeCase():{{ "Hello World".toSnakeCase() }}→"hello_world" -
.toCamelCase():{{ "hello_world".toCamelCase() }}→"helloWorld" -
.extractUrlPath(): Extracts path from URL. -
.trim(),.toUpperCase(),.toLowerCase(): Standard string methods.
5.2 Array Transformations
-
.sum(): Sums numerical elements.{{ [10, 20].sum() }}→30 -
.removeDuplicates(): Returns unique array. -
.merge(array): Merges another array. -
.isEmpty(): Boolean check for empty array. -
.randomItem(),.first(),.last(): Element retrieval.
5.3 Number Transformations
-
.round(decimalPlaces): Rounds number.{{ $json.amount.round(2) }} -
.toBoolean():0→false, others →true. -
.format(locale): Formats per locale (e.g., 'en-US'). -
.isEven(),.isOdd(): Boolean checks.
5.4 Object Transformations
-
.isEmpty(): Checks if object has no keys. -
.removeField(key): Removes specific key. -
.merge(object): Merges another object. -
.toJsonString(): Converts to JSON string.
5.5 Date & Time Transformations (Luxon)
- Global Shortcuts:
{{ $now }}(Current DateTime),{{ $today }}(Start of current day). -
.toDateTime(): Parses string to Luxon object. -
.plus(amount, unit)/.minus(amount, unit): Time arithmetic. -
.format(formatString):{{ $now.format('yyyy-MM-dd') }}. -
.isWeekend(): Boolean check.
5.6 Inline-Only Helpers
-
$if(condition, trueVal, falseVal): Ternary helper.{{ $if($json.age > 18, "Adult", "Minor") }} -
$ifEmpty(value, default): Returns default if value is null/undefined. -
$max(a, b...),$min(a, b...): Returns max/min values.
6. Code Node: Data Access and Variables
6.1 Data Access Cheatsheet
| Feature | JS / TS Syntax (Best Practice) | Python Equivalent | Description |
|---|---|---|---|
| Input (All) | $input.all() |
_input.all() |
Returns Array of all items (Run Once for All). |
| Legacy Input | items |
N/A | Legacy shorthand for $input.all(). |
| Input (One) | $input.item |
_input.item |
Returns current item (Run Once for Each). |
| Input (First) | $input.first() |
_input.first() |
First item in batch. |
| Input (Last) | $input.last() |
_input.last() |
Last item in batch. |
| Input (Index) | $input.item(index) |
_input.item(index) |
Item at specific index. |
| Node Ref | $('NodeName').first().json |
N/A | Access output of previous nodes. |
| Node All | $('NodeName').all() |
N/A | All items from specific node. |
| Env Vars | $env.API_KEY |
_env.API_KEY |
System environment variables. |
| Workflow Vars | $vars.myConfig |
_vars.myConfig |
Global workflow variables. |
| JSON Access | item.json?.myField |
N/A | Safe access (prevents "undefined" error). |
| Context | $input.context.noItemsLeft |
_input.context.noItemsLeft |
Detects end of batch. |
6.2 JS Equivalents to Inline Transformations
Since Tournament helpers don't work natively in Code Nodes, use these JS equivalents:
- Check Email:
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(item.json.email); - Extract Domain:
const domain = new URL(item.json.url).hostname; - Clean Text:
const clean = text.replace(/<[^>]*>?/gm, ''); - Sum:
const sum = [1,2].reduce((a,b) => a+b, 0); - Remove Duplicates:
const unique = [...new Set(item.json.ids)]; -
Date Format:
const DateTime = require('luxon').DateTime; DateTime.now().toFormat('yyyy-MM-dd');
7. Execution Modes & Return Contracts
7.1 Mode: "Run Once for All Items" (Recommended)
- Behavior: The code executes exactly once for the entire batch.
- Input:
$input.all()(Array). - Performance: High. The Sandbox/VM is instantiated only once.
- Use Case: Aggregation, Filtering, Batching, Cross-Item Logic.
7.2 Mode: "Run Once for Each Item"
- Behavior: The code executes separately (N times) for every incoming item.
- Input:
$input.item(Object). - Performance: Lower.
- Standard Mode: High overhead due to Sandbox reset/re-instantiation.
- External Runner: Incurs high network overhead (~10ms latency per item) + overhead of multiple context startups.
- Note: Avoid for large datasets (>1,000 items) unless strict isolation is required.
7.3 The Output Contract
You must strictly return: INodeExecutionData[].
- Always Return an Array: Even for single items.
- Use the
jsonKey: All data properties must be wrapped injson. - No Primitives: Do not return strings/numbers directly.
- No Unknown Top-Level Keys: Only
json,binary, andpairedItemare allowed.
Valid Output Example:
return [
{
json: { id: 1, success: true },
binary: { ... } // Optional
}
];
Item Linking (pairedItem):
To preserve UI debugging lineage and itemMatching logic when filtering, link output back to input index.
return items.map((item, index) => ({
json: { ...item.json, processed: true },
pairedItem: { item: index }
}));
8. Core Programming Patterns
8.1 Defensive Mapping (Transformation)
Use Optional Chaining (?.) and Nullish Coalescing (??) to handle dirty API data safely.
return $input.all().map(item => {
const d = item.json;
return {
json: {
userId: d.id,
// If address is missing, or city is missing, return 'Unknown'
city: d.contact?.address?.city?.trim() ?? 'Unknown',
// Convert tags array to string, or empty string if null
tags: d.tags?.join(', ') || ''
}
};
});
8.2 Filtering
Chain .filter() and .map(). Optimization: Filter early to stop processing invalid items immediately.
const items = $input.all();
return items
.map(item => item.json)
.filter(user => user.isActive && user.email.includes('@company.com'))
.map(user => ({ json: { ...user, valid: true } }));
8.3 Grouping (reduce)
Group orders by Customer ID or Email.
const items = $input.all();
const grouped = items.reduce((acc, item) => {
const customer = item.json.customerName;
if (!acc[customer]) {
acc[customer] = { customer, totalOrders: 0, items: [] };
}
acc[customer].totalOrders++;
acc[customer].items.push(item.json.product);
return acc;
}, {});
// Convert Object back to Array for n8n
return Object.values(grouped).map(g => ({ json: g }));
8.4 Flattening (flatMap)
Explode nested arrays (e.g., 1 Order → 5 Line Items).
const items = $input.all();
return items.flatMap(item =>
item.json.lineItems.map(product => ({
json: {
orderId: item.json.id,
productName: product.name,
price: product.price
}
}))
);
8.5 Recursive Flattening (Deep Objects)
For deeply nested responses (Stripe, Salesforce).
const item = $input.item.json;
function flattenObject(obj, prefix = '', maxDepth = 5, currentDepth = 0) {
if (currentDepth > maxDepth) {
console.warn('Max flattening depth reached');
return {};
}
const flattened = {};
for (const [key, value] of Object.entries(obj)) {
const newKey = prefix ? `${prefix}_${key}` : key;
if (value === null || value === undefined) {
flattened[newKey] = null;
} else if (typeof value === 'object' && !Array.isArray(value) && value.constructor === Object) {
Object.assign(flattened, flattenObject(value, newKey, maxDepth, currentDepth + 1));
} else if (Array.isArray(value)) {
flattened[`${newKey}_count`] = value.length;
flattened[`${newKey}_json`] = JSON.stringify(value);
} else {
flattened[newKey] = value;
}
}
return flattened;
}
const flattened = flattenObject(item);
return { json: flattened };
8.6 Merging Two Datasets (Lookup Map)
Faster than visual Merge node (O(n) vs O(n²)).
// 1. Get Reference Data
const users = $('Get Users').all();
// 2. Create Lookup Map (ID -> User Object)
const userMap = new Map(users.map(u => [u.json.id, u.json]));
// 3. Merge into current stream
return $input.all().map(item => {
const userId = item.json.userId;
return {
json: {
...item.json,
userData: userMap.get(userId) || { error: 'User not found' }
}
};
});
9. Advanced Programming & TypeScript
9.1 TypeScript Implementation (External Runner)
Since the UI only shows "JavaScript", you write TS code in the JS box, and the External Runner handles it.
Pattern: JIT Transpilation & Interfaces
interface Customer {
id: number;
email: string;
isActive?: boolean;
}
const items = $input.all();
return items.map(item => {
const customer = item.json as Customer; // Type Assertion
return {
json: {
id: customer.id,
emailLabel: `Contact: ${customer.email.toUpperCase()}`,
valid: customer.isActive ?? false
}
};
});
Pattern: Validation with Zod
Requires zod in NODE_FUNCTION_ALLOW_EXTERNAL.
const { z } = require('zod');
const UserSchema = z.object({
id: z.number(),
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user')
});
return $input.all().map(item => {
const result = UserSchema.safeParse(item.json);
if (!result.success) {
return { json: { ...item.json, error: result.error.issues } };
}
return { json: { ...result.data, verified: true } };
});
9.2 HTTP Requests with Async/Await
Use this.helpers.httpRequest or httpRequestWithAuthentication.
Concurrent Requests (Batching):
const items = $input.all();
const batchSize = 10;
const results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const promises = batch.map(async (item) => {
try {
const response = await this.helpers.httpRequest({
method: 'POST',
url: 'https://api.example.com/process',
body: item.json,
json: true
});
return { json: { originalId: item.json.id, success: true, data: response } };
} catch (error) {
return { json: { originalId: item.json.id, success: false, error: error.message } };
}
});
const batchResults = await Promise.all(promises);
results.push(...batchResults);
// Optional delay
if (i + batchSize < items.length) await new Promise(r => setTimeout(r, 1000));
}
return results;
Retry Logic with Exponential Backoff:
async function makeRequestWithRetry(url, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await this.helpers.httpRequest({ method: 'GET', url, json: true });
if (response.statusCode === 429) throw new Error('Rate Limited');
return response;
} catch (error) {
if (attempt < maxRetries - 1) {
const delayMs = Math.pow(2, attempt) * 500;
await new Promise(resolve => setTimeout(resolve, delayMs));
continue;
}
throw error;
}
}
}
9.3 Caching & Static Data
Persist data between executions or implement in-memory caching.
Cache Pattern (Lead Enrichment):
const staticData = $getWorkflowStaticData('global');
if (!staticData.cache) staticData.cache = {};
const items = $input.all();
const results = [];
for (const item of items) {
const key = item.json.id;
if (staticData.cache[key]) {
results.push({ json: staticData.cache[key] });
} else {
// Make API call and cache
const result = await this.helpers.httpRequest({...});
staticData.cache[key] = result;
results.push({ json: result });
}
}
return results;
10. Binary Data Handling
10.1 Safe Binary to Text Conversion
return items.map(item => {
try {
const binaryData = item.binary.data; // Base64 string
const textContent = Buffer.from(binaryData, 'base64').toString('utf-8');
return { json: { ...item.json, content: textContent } };
} catch (error) {
return { json: { ...item.json, error: error.message } };
}
});
10.2 Creating Binary Files
const htmlContent = `<html><body><h1>Report</h1><p>Date: ${new Date().toISOString()}</p></body></html>`;
const buffer = Buffer.from(htmlContent, 'utf8');
const binaryData = await this.helpers.prepareBinaryData(buffer, 'report.html', 'text/html');
return [{
json: { generated: true, timestamp: new Date().toISOString() },
binary: { data: binaryData }
}];
11. Performance Optimization & Limits
11.1 Memory Management (Heap)
- Issue: JS Objects consume significant RAM (e.g., 100MB CSV -> 400MB Objects).
- External Runner Fix: Set
N8N_RUNNERS_MAX_OLD_SPACE_SIZE=4096to allow 4GB RAM (default is often 512MB).
11.2 Split In Batches Pattern
For >5,000 items, add a Split In Batches node before the Code node. This prevents memory crashes by processing chunks (e.g., 50 items) at a time. The Code node runs once per batch.
11.3 Optimization Checklist
- Use Maps: Always use
new Map()for lookups, never.find()inside a loop. - Filter Early: Filter array before expensive transformations.
- Avoid Mutating: Return new objects
{ json: { ...item.json } }(immutability). - Use Code Node: For batch processing >1000 items, Code Node is ~3x faster than Standard Set/If chains.
12. Real-World Recipes
12.1 LinkedIn URL ID Extraction (Inline Expression)
{{ $("LinkedIn").item.json.query.url.extractUrlPath().split(":")[3].replace('/', '') }}
Logic: Extracts path, splits by colon, takes 4th element, removes slash.
12.2 Google Ads Automation
Analyzes search terms, calculates cost-per-conversion, filters underperformers, and formats for Slack.
const keywords = $input.all();
const analyzed = keywords.map(item => ({
...item.json,
costPerConversion: item.json.cost / (item.json.conversions || 1)
}));
const underperforming = analyzed
.filter(k => k.costPerConversion > 50)
.sort((a, b) => b.costPerConversion - a.costPerConversion);
return [{
json: {
message: `⚠️ ${underperforming.length} keywords need attention`,
keywords: underperforming.slice(0, 10).map(k => `${k.term}: $${k.costPerConversion.toFixed(2)}`)
}
}];
12.3 Generating Personalized Invoices
const items = $input.all();
const defaultShipping = $vars.defaultShipping || 'Standard';
return items.map((item, i) => {
const data = item.json;
const total = data.items.reduce((acc, curr) => acc + (curr.qty * curr.price), 0);
const tax = total * 0.08;
const greeting = $evaluateExpression('{{ "Dear " + $json.firstName }}', i);
return {
json: {
...data,
subtotal: total.toFixed(2),
tax: tax.toFixed(2),
total: (total + tax).toFixed(2),
shipping: $ifEmpty(data.shipping, defaultShipping),
greeting,
generatedAt: new Date().toISOString()
}
};
});
13. FAQ and Checklists
13.1 Common Questions
- Can I use
requirein the Code Node?- Standard Mode: Generally No.
- Runner Mode: Yes, if the package is in the Docker image and the
ALLOW_EXTERNALlist.
- Why does my TypeScript code fail?
- Ensure you are using the External Runner. Standard n8n knows only pure JS.
- How do I console log?
-
console.log('My Var:', myVar);. Check "Browser Console" (Standard) or "Server Logs" (Runners).
-
- Difference between
$jsonand$input.item?-
$jsonis for Inline Expressions. -
$input.item.jsonis for Code Nodes.
-
- Why do I get "Unknown top-level item key"?
- You returned
{ myField: 1 }. You must return{ json: { myField: 1 } }.
- You returned
- Why is my loop not waiting for the API call?
- You likely used
.forEachwithasync. Switch tofor...oforPromise.all.
- You likely used
- Can I use JMESPath?
- Yes, via
$jmespath(data, query). Example:$jmespath(item.json, 'users[?age > \18].name').
- Yes, via
13.2 Final "Before You Run" Checklist
- [ ] Mode: Is "Run Once for All Items" selected for arrays?
- [ ] Return: Is data wrapped in
[{ json: ... }]? - [ ] Imports: If using
require('lodash'), is it allowed in env variables and Docker image? - [ ] Syntax: Using
$input.all()instead of legacyitems? - [ ] Safety: Using optional chaining
?.and$ifEmpty? - [ ] Error Handling: Wrapped async logic in
try-catch? - [ ] Immutability: Used spread syntax (
...item.json)? - [ ] Lineage: Included
pairedItemif filtering/merging is needed later?
Top comments (0)