The ten percent no ready-made node can do: the Code node
Fourteen parts in, most of what you build is a core node wired to an integration node. Then a job turns up that none of them fit: orders that have to be regrouped along two dimensions at once, an API response buried three levels deep, a top-three ranking by share of revenue. The Code node is the escape hatch n8n gives you for exactly that — a blank JavaScript editor with items going in and items coming out. This part covers the four patterns that handle almost all of it, the two run modes that decide how the code has to be written, and the one output shape that causes more failures than everything else put together.
Everything so far, and the jobs it still cannot reach
By now the shape of a workflow should be familiar. Eight cases out of ten come down to a core node plus a SaaS integration node: pull data, filter it, send a message, write it to a database. The Edit Fields (Set) node reshapes what comes through, expressions fill values in dynamically, and Merge and the splitting nodes put the pieces back together.
Then there is the ten percent. These are the jobs where the ready-made nodes run out, and they are more specific than “something complicated”:
{ result: { data: { items: [...] } } } when all you want is the items array. One expression can reach it, but add a little more logic and it turns into a mess.{ type, elements: [...] }, which would drive you mad in the Set node.That is what the Code node is for. It gives you a blank JavaScript editor: items go in, you write a few lines, items come out. You do not need to know how to build applications. For most situations, copying one of the four patterns further down this page and changing the parameters is enough.
flowchart LR A["Upstream node
hands over an array of items"] --> B["Code node
your JavaScript runs"] B --> C["A new array of items
comes out"] C --> D["Next node
Slack, Sheets, HTTP Request"] E["Ready-made nodes already cover
about eight cases in ten"] -.-> B
The ready-made nodes are the appliances in a kitchen. The kettle boils water, it does that one job well, and nobody has to be taught how to use it. The Code node is the knife. It can do anything the appliances cannot, and it is also the one thing in the drawer that can cut you — not because it is badly made, but because nothing about it stops you doing the wrong thing.
If Set, IF or Switch can do it, do not open a Code node
The Code node looking capable is not a reason to reach for it. Code is JavaScript, and whoever maintains the workflow later — often you, months on — has to read that code and know JavaScript as well. That bar is a great deal higher than a visual node, so the question to ask before opening one is always whether the built-in nodes could have done the job.
Here is that question worked through for the seven things people most often reach for Code to do.
| What you want to do | Use this first | When to move up to Code |
|---|---|---|
Combine firstName and lastName into fullName |
The Edit Fields (Set) node — one expression, {{ $json.firstName + ' ' + $json.lastName }}, and it is done. The visual route is the fastest here. |
No need to move up. |
| Amount over 1000 takes path A, everything else takes B | The IF node — two branches drawn plainly on the canvas, so the logic reads at a glance. | No need to move up. |
| Split into three paths by order status | The Switch node — the native node for multi-way branching. | No need to move up. |
| Filter items down to the ones where amount is over 1000 | The Filter node, which newer versions have, or an IF node with one output wired. | Move up when the condition crosses several fields, or when the condition itself has to be calculated. |
| Add a field or calculate a new value on every item | The Set node plus an expression — that covers most of it. | Move up when you need a loop, or when the conditional transformation runs past three or four cases. |
| Sum all the items, average them, group them by something | The Aggregate node, which is built in, or the Summarize node. | Move up for a top-N ranking, grouping on several dimensions, or anything statistically involved. |
| Take a nested API response apart and rebuild the JSON structure | The Set node plus expressions usually holds up. | Move up only at three levels of nesting or more, or when you need map and reduce. |
It is the difference between a route written out with four turns marked and a note that says “you know the way”. The four turns take longer to write down and they take up more room on the page. They are also the only reason somebody else can drive it without ringing you up.
flowchart TD A["You need to select,
reshape or summarize data"] --> B{"Could three Set nodes
do this?"} B -->|"yes"| C["Set, IF, Switch,
Filter or Aggregate"] B -->|"no"| D{"Does it need a loop, a sort,
or three levels of nesting?"} D -->|"no"| E["Try the visual nodes first.
Come back if they run out"] D -->|"yes"| F["Open a Code node"] C --> G["Readable by the next person
without knowing JavaScript"]
Two languages in the dropdown, and one of them to pick
Open a Code node and there is a Language option toward the top right with two choices: JavaScript and Python. In practice, pick JavaScript around 99% of the time. The reasons are worth knowing, because they are not about which language is nicer.
| Language | How it runs | Upside | Downside and verdict |
|---|---|---|---|
| JavaScript (the default) |
Runs in n8n's built-in sandbox. When you self-host you can also switch to task runners, which are separate child processes and the newer architecture n8n recommends. | The one n8n leads with. The helper API is complete, there are the most examples, and it runs fast. | The default sandbox cannot require external npm packages — you have to enable NODE_FUNCTION_ALLOW_EXTERNAL for that. Pick this by default. Without a specific reason, use JavaScript. |
| Python | From n8n 2 on it uses the native Python task runner, introduced in 1.111.0 and stable in n8n 2. The earlier Pyodide route, which ran Python as WebAssembly, is marked legacy and n8n 2 no longer supports it. | You have existing Python logic to move over, and you can pip install the standard library plus third-party packages — only when you self-host and have the task runner set up. |
Native Python only takes bracket access, _input.item['json'] rather than .json. Whether Cloud has it enabled depends on your plan, and community examples are still mostly JavaScript. Pick it only when you have existing Python logic to port. |
“I am better at Python” on its own is not a good reason. The community and the examples are all on the JavaScript side, which means every answer you find when you get stuck will need translating first.
Every remaining example on this page is JavaScript. If you do end up in Python, the syntax differs in two ways worth writing down: variables start with an underscore (_input, _json), and the native Python runner only takes bracket access — _input.item['json']['amount']. The _input.item.json.amount dot syntax that old Pyodide allowed no longer works, which is exactly the line that breaks when somebody copies an old forum answer. When you need the detail, the official page is docs.n8n.io/build/code-in-n8n/using-the-code-node/.
All items at once, or one item at a time
At the top of the Parameters panel on the right, next to the Language option, there is a Mode dropdown — the official docs word it “Choose a mode”. The two options behave very differently, and picking the wrong one duplicates or drops data rather than throwing a tidy error.
| Mode | Behavior | When to use it | Typical jobs |
|---|---|---|---|
| Run Once for All Items (the default) |
The whole items array comes in at once, your code runs once, and it returns a new items array. | When you have to see all the data before you can decide: aggregate, sort, group, rank the top N. | “Total the revenue”, “take the top 5 customers”, “group by category”, “merge two upstreams”. |
| Run Once for Each Item | Your code runs once for each item, as if a for loop had already been written around it for you. | When a single item is enough to decide, and the transformation rule is the same for every one. | “Add tax to each order”, “reformat each message”, “call an API per item to fill in a field”. |
It is the difference between totalling the shopping and putting a price sticker on each tin. You cannot total the receipt one tin at a time — you need the whole bag on the counter first. You do not need the whole bag to sticker one tin, and insisting on it just makes the job slower.
$input.all() to get everything, and return has to hand back an array: [{ json: {} }, ...]. In Each Item mode, use $input.item.json to get the current one, and return hands back a single { json: {} }. Mixing the two throws Items to return were not valid, or produces empty output with no error at all.flowchart TD
A["Items arrive at the Code node"] --> B{"Which mode is selected?"}
B -->|"Run Once for All Items"| C["Your code runs once.
Read every item at once"]
C --> D["Return an array of items"]
B -->|"Run Once for Each Item"| E["Your code runs once per item.
Read the current item only"]
E --> F["Return one single item"]
D --> G["Downstream node"]
F --> G
Every example below assumes All Items mode unless it says otherwise.
Filter, map, aggregate, call an API
These four cover the overwhelming majority of Code nodes anyone writes. Read them as templates: copy one, change the field names and the conditions, and leave the structure alone.
flowchart LR A1["20 items in"] --> B1["filter
a condition removes some"] --> C1["6 items out"] A2["20 items in"] --> B2["map
every item transformed"] --> C2["20 items out"] A3["20 items in"] --> B3["aggregate
reduced to one total"] --> C3["1 item out"] A4["1 item in"] --> B4["call an API
and wait for the answer"] --> C4["1 item with the response"]
Pattern 1 · filter
Pick the items that match a condition out of a pile of them. All Items mode.
// Keep only orders over 1000
return $input.all().filter(item => item.json.amount > 1000);In plain words, reading it left to right:
$input.all()gives you every item that came in, as an array..filter(item => ...)is a built-in JavaScript array method. It runs that test on every item and keeps only the ones where the answer istrue.item.json.amount > 1000is the test itself. Change this to your own condition and you are done.return ...hands the result to the downstream node.
Swap amount > 1000 for whatever you need — item.json.status === 'paid' and item.json.tags.includes('VIP') both work the same way. Chain several conditions with && for “and” and || for “or”:
// Amount over 1000 and tier is gold
return $input.all().filter(item =>
item.json.amount > 1000 && item.json.tier === 'gold'
);Pattern 2 · map, to transform each item
Transform every item: add a field, change a field, calculate a new value. All Items mode.
// Combine firstName and lastName into fullName; keep only fullName and age
return $input.all().map(item => ({
json: {
fullName: item.json.firstName + ' ' + item.json.lastName,
age: item.json.age
}
}));.map(item => ({...}))runs that function on every item and returns a new item for each one.{ json: {...} }is the format n8n requires for an item. Every item has to be wrapped in ajsonlayer. This one is easy to forget, and forgetting it throwsItems to return were not valid.fullName:andage:are the fields you want in the output. Write whichever fields you want to keep.
Notice what that example throws away: anything not listed is gone. If you only want to add a field to the original item rather than rebuild it, use the spread syntax to keep every original field and then add the new ones:
// Keep every original field, then add taxAmount and total
return $input.all().map(item => ({
json: {
...item.json, // spread the original fields
taxAmount: item.json.amount * 0.05,
total: item.json.amount * 1.05
}
}));The three dots in ...item.json are JavaScript's spread operator — read it as “flatten every field of item.json in right here”. Writing it this way saves you listing every field by hand, and it survives the upstream node gaining a new field next month.
Pattern 3 · aggregate
Shrink a pile of items down to one total. All Items mode.
// Sum every order amount, then work out revenue and item count
const total = $input.all().reduce((sum, item) => sum + item.json.amount, 0);
const count = $input.all().length;
return [{
json: {
total: total,
count: count,
average: total / count
}
}];.reduce((sum, item) => sum + item.json.amount, 0)is JavaScript's reduce, for adding an array up as it goes.sumstarts at0, which is the second argument; each pass addsitem.json.amountto it; the total comes back at the end.$input.all().length— the length of the array is the item count.return [{ json: {...} }]— note that it still has to be wrapped in an array, even when you are returning only one item. Forget the outer[]and it blows up.
To group by something rather than totalling everything — a total per customer, say — pair reduce with an object accumulator:
// Total the amounts per customer name
const groups = $input.all().reduce((acc, item) => {
const key = item.json.customer;
acc[key] = (acc[key] || 0) + item.json.amount;
return acc;
}, {});
// turn it back into an items array and return it
return Object.entries(groups).map(([customer, total]) => ({
json: { customer, total }
}));What that does is sort every order into piles by customer, total each pile, and return one item per customer. It is the most common example of “Set cannot do it, but one block of Code can” — and it is exactly the two-line job that turns into six nodes if you insist on staying visual.
Pattern 4 · call an external API from inside the Code node
This one is rare, and it should stay rare. Inside a Code node you can await a call to an external API directly, but the HTTP Request node is usually easier to maintain — it has a proper interface for headers, authentication and retry, none of which you get for free in code. If you really do need the call inside Code:
// Call an external API and return the response as one item
const response = await this.helpers.httpRequest({
method: 'GET',
url: 'https://api.example.com/x',
headers: { 'Accept': 'application/json' }
});
return [{ json: response }];this.helpers.httpRequest(...) is a helper n8n builds in — the HTTP Request node, written out in code. Use it when one stretch of logic has to call several APIs in a row, or when a field on an item decides whether to call at all. For a plain single API call, drop in an HTTP Request node instead and keep the credential where the credential belongs.
https://api.example.com/x is deliberately not a real endpoint, and there is no key in that snippet either. Keep it that way in your own workflows: put authentication in a credential rather than typing a token into a node parameter, and strip anything sensitive out of a workflow before you export or share its JSON.this.helpers.httpRequest and the other this.helpers.* functions such as getBinaryDataBuffer work, crypto works in part, and the built-in array, string and JSON methods all work. fs, external require and a bare fetch do not, by default. For npm packages, look up the environment variable NODE_FUNCTION_ALLOW_EXTERNAL in the official docs — self-hosting only, and not possible on Cloud.The eleven variables n8n hands you, and the few you will actually use
The Code node comes with a pile of helpers n8n injects for you — the variables that start with $. Ninety percent of the time you only ever touch the first few in this list, but it is worth reading the whole thing once so you know what exists when you need it.
| How you write it | What it is | Which mode it works in |
|---|---|---|
$input.all() | The whole array of items this node received. | Mainly All Items mode. It works in Each Item too, but that is rare. |
$input.item | The current item — the full object, including json and binary. | Only meaningful in Each Item mode. |
$input.item.json | The json part of the current item, which is what an expression calls $json. | Common in Each Item mode. |
$('Node Name').all() | Grabs every item from a named upstream node. It does not have to be the direct upstream. | Works in both modes. |
$('Node Name').first() / .last() | The first or last item from a named upstream node. | Works in both modes. |
$now | The current time, as a Luxon DateTime object. | Works in both modes. |
$today | Today at 00:00, also Luxon. | Works in both modes. |
$workflow.id / $workflow.name | The current workflow's id and name. | Works in both modes. |
$execution.id | The id of this run. You can look it up afterwards on the Executions page. | Works in both modes. |
console.log(x) | Prints to that node's log tab on the Executions page. | Works in both modes — the debugging workhorse. |
this.helpers.httpRequest({...}) | Calls an external API. The equivalent of the HTTP Request node. | Works in both modes. Needs await. |
Note the quoting style in $('Node Name'): single quotes around the node's name, exactly as the node is labeled on the canvas. Rename a node on the canvas and every $('Node Name') that refers to it has to be updated by hand — nothing rewrites those for you.
The full list, including the more advanced entries, is in the official docs at docs.n8n.io/code/builtin/overview/. That page also covers $jmespath() for JMESPath queries, $max() and $min(), and DateTime for calling the Luxon constructor directly.
Four steps, six error messages, and the mistake everyone makes
The Code node throws more errors than any other node type, for the obvious reason that you are writing code in it. The good news is that n8n's error messages are usually clear enough to act on. Work through these four steps in order rather than rereading the code hopefully.
-
Step 1
Read the red error message under the node panel
When a Code node fails, the node itself turns red and an error message appears below it, usually including a line number.
SyntaxError at line 3means go to line 3 and look for an unmatched quote or an unclosed bracket. Do not start by rewriting the logic; start at the line the message named. -
Step 2
Check that node's log on the Executions page
In the left sidebar go to
Executions→ click the failed run → click the Code node → switch to the Logs tab at the top. Everyconsole.log(x)you wrote shows up here. Log as much as you like while you are still building; nobody will hold it against you. -
Step 3
Check the output is in the right format
The Code node ran fine but the downstream node gets nothing? Check whether the output is in the
[{ json: {...} }]shape — an array on the outside, every item wrapped injson. Forgetting thejsonwrapper is the most common mistake of all: a barereturn [{ name: 'Elmo' }]makes n8n read every item as empty. The correct form isreturn [{ json: { name: 'Elmo' } }]. -
Step 4
Print nested objects properly
A plain
console.log(item)sometimes prints only[object Object]and tells you nothing. Useconsole.log(JSON.stringify(x, null, 2))to turn the object into readable JSON before printing it, and even several layers of nesting become clear at a glance.
Every item n8n passes along travels in an envelope marked json. Hand it a bare sheet of paper and it does not tell you off — it forwards an envelope with nothing inside, and the next node quite honestly reports that it received nothing. That is why “the code ran, the output is empty” is almost always a missing wrapper rather than broken logic.
flowchart TD A["The Code node did not do
what you expected"] --> B{"Did the node turn red?"} B -->|"yes"| C["Read the message under the node.
It usually names a line number"] C --> D["Go to that line: unmatched quote,
unclosed bracket, wrong arrow"] B -->|"no"| E{"What did the next node receive?"} E -->|"nothing at all"| F["Check the return statement,
then check the json wrapper"] E -->|"data, but wrong"| G["Open Executions, pick the run,
read that node's Logs tab"] G --> H["Print nested objects as readable JSON
before you log them"]
The six messages you will actually see
| What you see | What it means | What to do |
|---|---|---|
SyntaxError: Unexpected token or Unexpected identifier |
A plain JavaScript syntax error — usually unmatched quotes, an unclosed bracket, or a stray semicolon. | The message gives you the line number; look at that line and the ones either side. Common cases: curly quotation marks mixed in with straight ' ones, which are not valid JavaScript quotes at all; one half of a {} pair missing; => typed as ->. |
Cannot read properties of undefined (reading 'xxx') |
The field you are reaching for has nothing in its parent. item.json.customer.email when there is no customer field, for instance. |
Three fixes, in increasing order of thoroughness: use optional chaining, item.json.customer?.email; add a guard, if (!item.json.customer) return null;; or fix the upstream node first and confirm it really does return customer. |
| The output is empty and the downstream node gets nothing | Three common causes, and it is worth checking all three rather than guessing. | (a) You forgot the return — a function with no return gives back undefined. (b) You forgot the { json: {} } wrapper, so n8n does not recognize your output. (c) The filter condition is too strict and no item matches, so the result is an empty array. That last one is legitimate behavior, but check whether you tightened the condition too far. |
| Code runs very slowly, past the 30-second timeout | Usually a large array — over 10,000 items — running .map or .filter. |
Put a Split In Batches node upstream and work in batches, feeding 500 to 1000 items into the Code node at a time. Also check the code for a nested loop, a .map with a .filter inside it: that is O(n²), and it blows up as soon as the data grows. |
Items to return were not valid |
What you returned is not in n8n's item format. | The correct shape is [{ json: {...} }] — an array on the outside, and every element an object with a json key. Wrong: return { name: 'Elmo' }, which is not an array, and return [{ name: 'Elmo' }], which has no json wrapper. Remember that Each Item mode returns a single { json: {} } while All Items mode returns an array. |
ReferenceError: fetch is not defined or require is not defined |
The Community edition sandbox does not enable fetch or require. |
Call APIs with await this.helpers.httpRequest({...}) instead. To pull in an npm package, self-hosting is the only route — set the environment variable NODE_FUNCTION_ALLOW_EXTERNAL=lodash to open it up. Not possible on Cloud. |
Questions that come up every time
I do not know any JavaScript. Do I have to learn it?
Is Python or JavaScript the better choice?
_input.item['json']['x'], so watch out when you rewrite a JavaScript example. Pick Python when you have existing Python logic to move over as-is — a string-handling function, say, or a snippet from an internal tool — and you can pip install the packages it needs. Being more comfortable in Python is not on its own a good reason.Can the Code node use npm packages such as lodash or axios?
crypto and querystring. When you self-host, yes, but you have to set the environment variable NODE_FUNCTION_ALLOW_EXTERNAL=lodash,axios on the n8n container, listing the package names you want to open separated by commas, and npm install them into the container. Cloud users who want a similar effect can split the logic into a sub-workflow, as Part 14 covers, or call an outside service with an HTTP Request node instead.Can two Code nodes share a variable?
const x = 5 declared in one is invisible in the next. To pass data between nodes, take n8n's regular route — put the value in the output's json, and read it in the next node with $('Previous Code').item.json.x. For a constant shared across a whole workflow, store it in an Edit Fields (Set) node, or use workflow variables — note that $vars is Enterprise only.Can a Code node await a call to another workflow?
Execute Workflow node's job. What a Code node can await is this.helpers.httpRequest, for calling an external API, and the other built-in helpers that return a Promise. If you want a sub-workflow to run from inside a Code node, restructure it instead: the Code node prepares the data → the Execute Workflow node runs the sub-workflow → the next Code node carries on with the result. The division of labor is clearer that way too.Can I paste Code node code that an AI assistant generated straight in?
items[0].json, which is the old v0 style, instead of $input.all()[0].json, which is the current one. The old style fails at run time with items is not defined. (b) That the output is wrapped in the { json: {} } format. When you ask for the code, say plainly what you want: “n8n Code node, JavaScript, Run Once for All Items mode, use $input.all()” — the odds of getting it right go up a lot. Run the result over a small amount of data once before you take it live.What is the difference between the Code node and an expression?
await. The Code node is a whole node: many lines, variables, loops, await, console.log. The test to apply is whether the logic fits inside one {{ }}. If it does, use an expression. If it does not — an if/else with several branches, a for loop, an intermediate value you need to hold on to — use the Code node. Expressions are for filling in values dynamically; the Code node is for handling logic.How do I know whether my code will run on Cloud?
fetch is not defined, require is not defined, Module not found. As a rule of thumb: (a) all built-in JavaScript syntax is fine — array methods, string methods, JSON, Math, Date; (b) n8n helpers such as $input, $now and this.helpers.httpRequest are fine; (c) npm packages, the fs file system and a bare fetch generally are not. If you are unsure, make a sandbox test workflow and run a few lines to feel out where the edge is.Where to go from here
You can write the logic yourself. Next, a node that writes it for you.
Part 16 covers the AI Agent: a node you give a goal and a set of tools rather than a set of steps, and what changes about a workflow when one of its nodes decides for itself what to do next.
Open the full guidePart 15 of the Woow n8n Onboarding Guide series on the Apporo blog.
Adapted from the Woow n8n Onboarding Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.
Light · Air · Water · Control · apporo