Module 3: Items and n8n's Built-In Variables
4. The current item with `$json`
Description
By the end of this lesson you will understand n8n's most used variable and, at the same time, the one that produces the most forum questions. You'll know exactly what $json is — not an approximation, its literal definition — its precise relationship with $input.item, which execution mode it's valid in and which it isn't, and what happens when you use it in the wrong mode. You'll see why the same variable you write every day in any field's {{ }} expressions behaves differently inside a Code node. And you'll meet its less fortunate sibling: $binary, which exists in expressions and is not available in the Code node.
This matters for a very practical reason. $json is the variable you carry with you: if you're coming from building workflows with expressions, you've spent months writing {{ $json.customer_name }} in node fields, and that reflex is so strong it carries over into the Code node without thinking. In Run Once for Each Item mode, that reflex is correct and saves you typing. In Run Once for All Items mode — the default mode, the one the dropdown shows when you open a new node — the same reflex produces undefined, and that's where time starts getting lost checking field names that are perfectly spelled.
Connection to the module: lesson 3 gave you full $input, including $input.item. This lesson takes that specific access and studies its shortcut, which is how you'll actually write the code. It's layer 1's last piece — the data in your hands — and with it you close out reading the input. Lesson 5 leaves layer 1 for the rest of the workflow. From Module 1, it rests on lesson 5, which already previewed that $json "doesn't have a useful meaning" in All Items mode; here you're going to see why, with the official source and the exact symptom it produces.
Three letters that mean a whole chain
Let's start with the definition, which is literal and documented:
$jsonreturns the current node's incoming JSON data, for the current item. It's a shortcut for$input.item.json.
That second sentence is the whole lesson. $json is not an independent variable: it's a short name for a three-link chain you already know.
$input → the current node's inbox tray
$input.item → the envelope I'm processing right now
$input.item.json → the letter inside that envelope
$json ≡ $input.item.json
Read it right to left and you'll see why the shortcut exists. Almost always what you want is the letter: Cumbre's order's fields. Writing $input.item.json.customer_name every time is tedious, and $json.customer_name says exactly the same thing.
The analogy: it's the difference between saying "the content of the package I'm holding in my hands right now" and saying "this." When you're only handling one package, "this" is unambiguous.
And there, exactly, is the condition that makes the shortcut valid: "this" is only unambiguous if there's a package in your hands.
The condition: per-item mode
n8n's official reference table marks almost every access as available in the Code node with no further comment. With $json it makes an explicit exception, the only one in the whole table:
$json— Shortcut for$input.item.json. A node's incoming JSON data. Available in the Code node when running once for each item.
Let's take apart why, because the "why" is what keeps it from slipping your mind.
In Run Once for Each Item mode, your code runs once for every item that comes in. On each of those runs there's a current item, well defined: the one that's up on that pass. $input.item points to it, and $json points to its content. Everything closes up.
In Run Once for All Items mode, your code runs exactly once for the whole batch. There's no current item, because you have all of them at once. Asking about "the current item" is like asking the teacher with two hundred exams on the desk which one they're grading: none in particular, or all of them. The question has no useful answer.
That's why the rule is short and worth memorizing:
$jsonbelongs to per-item mode. In All Items mode, you read from the array:$input.all()and a loop.
What exactly happens if you use it in the wrong mode
This is the part people struggle to find, because the symptom is confusing.
Writing $json.customer_name in a Code node in Run Once for All Items mode doesn't usually produce a clear red error. What it produces is undefined, or an error about reading a property of something that doesn't exist, depending on your version and what you're doing with the value. And the undefined is the dangerous part: it propagates silently through the rest of the script and the problem shows up later, when a downstream node receives an empty field.
The wrong diagnosis almost everyone makes the first time is: "the field must be named differently." The input panel gets opened, customer_name gets confirmed as the field's name, back to the code, tried with quotes, with brackets, with capitalization. Half an hour later someone asks what mode the node is in.
How to spot it in five seconds: if your script is in All Items mode and contains $json anywhere, that's the problem. It's a text search, not reasoning.
And an honest recommendation: verify the exact behavior on your instance. Write a Code node in All Items mode with return [{ json: { test: $json } }];, run it, and note what you see. It could be undefined, it could be an empty object, it could be an error. Knowing which of the three yours is turns a half-hour diagnosis into a five-second one, because you'll recognize the symptom.
Worked example: the same logic, both paths
Cumbre requirement: "for each order, add total_units with the sum of its lines' quantities, and is_priority as true if it reaches twenty units."
It's a strictly one-to-one transformation: each order resolves by looking only at its own fields, and as many come out as go in. It's per-item mode's ideal case, and seeing it side by side with the other mode is the best way to pin down $json's role.
Version A — Run Once for Each Item, with $json:
// ============================================================
// Node: Code — "Flag priority orders"
// Mode: Run Once for Each Item
//
// INPUT: a Cumbre order with line_items
// OUTPUT: the same order, with total_units and is_priority
// ASSUMPTIONS: quantity is a number. line_items might be missing or empty.
// RULE: is_priority = 20 units or more
// ============================================================
// --- ACT 1: read ------------------------------------------------
const order = $json; // the current item's content
// --- ACT 2: process -----------------------------------------------
const lines = order.line_items || []; // guard: if the field is missing, use an empty list
let totalUnits = 0;
for (const line of lines) {
totalUnits = totalUnits + line.quantity;
}
// --- ACT 3: return -------------------------------------------------
return {
json: {
...order, // keep all the original fields
total_units: totalUnits,
is_priority: totalUnits >= 20,
},
};
Version B — Run Once for All Items, no $json:
// ============================================================
// Node: Code — "Flag priority orders"
// Mode: Run Once for All Items
//
// INPUT: N Cumbre orders with line_items
// OUTPUT: the same N, with total_units and is_priority
// ASSUMPTIONS: quantity is a number. line_items might be missing or empty.
// RULE: is_priority = 20 units or more
// ============================================================
// --- ACT 1: read ------------------------------------------------
const items = $input.all();
// --- ACT 2: process -----------------------------------------------
const output = [];
for (const item of items) {
const order = item.json; // ← this is what was $json in version A
const lines = order.line_items || [];
let totalUnits = 0;
for (const line of lines) {
totalUnits = totalUnits + line.quantity;
}
output.push({
json: {
...order,
total_units: totalUnits,
is_priority: totalUnits >= 20,
},
});
}
// --- ACT 3: return -------------------------------------------------
return output;
What to expect. Both versions produce exactly the same output panel. With Module 1's five-order seed, 5 items come out, each with all its original fields plus the two new ones:
| order_id | customer_name | total_units | is_priority |
|---|---|---|---|
| ORD-2041 | Luna Coffee | 18 | false |
| ORD-2042 | North Bakery | 5 | false |
| ORD-2043 | Luna Coffee | 10 | false |
| ORD-2044 | Sunrise Market | 40 | true |
| ORD-2045 | Andes Deli | 0 | false |
Check in the JSON view that total_units shows up with no quotes — it's a number — and that is_priority shows up as true or false with no quotes — it's a real boolean, not the text "true". That detail matters: if it were text, a Filter node comparing against true would behave strangely.
Now compare both versions line by line. The body of the logic is identical: the lines calculating totalUnits don't change by a single character. The only thing that changes is the scaffolding:
| Version A (Each Item) | Version B (All Items) | |
|---|---|---|
| How I reach the order | $json | item.json, inside a loop |
| Outer loop | None | for (const item of items) |
| Output accumulator | None | const output = [] |
| What I return | An object | An array |
The mechanical equivalence worth pinning down is this: $json in per-item mode is item.json inside the loop in All Items mode. When you translate a script from one mode to the other, that's the substitution you make.
$json in expressions and $json in the Code node
Here's a nuance that confuses a lot of people and is worth clarifying well, because it explains why the expressions reflex carries over badly into the code.
When you write {{ $json.customer_name }} in an Edit Fields node's field, a Filter, or an email, you're using the same variable. And there it always works. Why?
Because n8n's nodes process each item separately, automatically. It's the product's underlying behavior we saw in lesson 2: if five items arrive at an email node, it sends five emails, and when it evaluates each one's expression, "the current item" is the one up on that pass. That is: the expressions editor always lives in something equivalent to per-item mode. There's no "all items" mode for a form field.
The Code node is the only place in n8n where you choose that behavior. And since the dropdown's default value is Run Once for All Items, the first time you open a Code node you're, without knowing it, in the only n8n context where $json doesn't apply.
Put another way: it isn't that $json behaves differently in the Code node. It's that the Code node is the only place where you can step outside the context in which $json makes sense.
That is, in my experience, the explanation that makes this topic stop causing trouble. It isn't an arbitrary exception: it's the direct consequence of the Code node having two modes while the rest of n8n has just one.
$binary: the sibling that didn't travel
Since we're on shortcuts, it's worth closing out the pair.
$json is the shortcut for $input.item.json. Its symmetrical sibling is $binary, the shortcut for $input.item.binary, giving access to the current item's file compartment — the one you studied in lesson 2.
And here's the fact almost no tutorial mentions: $binary is not available inside the Code node. The official previous-nodes reference table marks it with an X, while $json carries its conditional mark and every $input method carries a positive one. It's the only one on the list that simply isn't there.
// ✅ In an expression, inside a node's field
{{ $binary['orders-file'].fileName }}
// ❌ In a Code node: not available
const fileName = $binary['orders-file'].fileName;
So what to do if you need binary data from code. Two paths, in order of preference.
One: don't touch it from the Code node. The overwhelming majority of file manipulations have a dedicated node — Extract from File, Convert to File, Read/Write File From Disk — and that node does the work better without you handling bytes.
Two: if you genuinely need the content, use the documented route. The official cookbook has a page dedicated to getting the binary data buffer from the Code node, and that's the supported path. It isn't this guide's topic, but you should know where it is.
And what is this guide's topic, because you'll run into it: the binary compartment is still on the item even though $binary isn't available. You can read and copy it perfectly well with $input.item.binary or with item.binary inside a loop:
// Mode: Run Once for Each Item
// I transform the data and keep the attached file
return {
json: { ...$json, reviewed: true },
binary: $input.item.binary, // I do have the full envelope
};
Just because the shortcut doesn't exist doesn't mean the compartment disappears. It's a fine distinction and exactly the kind of thing that separates a two-minute diagnosis from a lost afternoon.
When to use $json and when $input.item
With everything above, the choice between the two is simple and comes down to a single question.
Do I need the letter or the full envelope? The letter — the business fields — is
$json. It's 95% of cases. The full envelope — to reachbinaryor to inspect the structure — is$input.item.
// Mode: Run Once for Each Item
$json; // the letter: { order_id: 'ORD-2041', customer_name: 'Luna Coffee', ... }
$json.order_id; // 'ORD-2041'
$input.item; // the envelope: { json: {...}, binary: {...}, pairedItem: ... }
$input.item.json; // the letter again: identical to $json
$input.item.binary; // the file compartment
A mistake worth anticipating, because it's Module 1's classic trap applied here: don't confuse levels when returning.
// ❌ An extra level: puts the envelope inside the compartment
return { json: $input.item };
// ✅ Return the content as-is
return { json: $json };
// ✅ Or, equivalently
return { json: $input.item.json };
The first version produces output where every item has a field called json that contains the data, and every downstream node breaks because now the fields have to be reached with $json.json.order_id. It's the "putting the whole item inside json" mistake you already saw in Module 1, and $input.item is exactly the variable that invites it.
A traveling companion: $itemIndex
We close with a small variable that usually shows up alongside $json, and that lesson 6 develops: $itemIndex.
What it is. A number telling you the position of the item being processed within the list of input items. It starts at 0, like every index.
What it's for in per-item mode. Since each run is independent and knows nothing about the others, $itemIndex is the only clue you have about where you are within the batch.
// Mode: Run Once for Each Item
return {
json: {
...$json,
// This order's position in the batch. Useful for debugging
// and for giving output records a stable order.
position_in_batch: $itemIndex,
},
};
What to expect. With five orders, the output items carry position_in_batch with values 0, 1, 2, 3, and 4 respectively.
And the limitation, already previewed by Module 1: $itemIndex tells you your position, but not how many items there are in total. To say "3 of 5" you need the 5, and you only have the 5 in All Items mode with $input.all().length. It's the cleanest example of the asymmetry between the two modes.
Common mistakes
Using $json in All Items mode (practical). What happens: you write $json.customer_name in a freshly created Code node — which is Run Once for All Items by default — and get undefined or an error about a property of something undefined. You start doubting the field name, which is spelled correctly. Why it happens: $json is the shortcut for $input.item.json, and in All Items mode there's no current item to point to. It's the only access in the official table with an explicit mode caveat. How to spot it: text search. If your script is in All Items mode and contains $json, that's the problem; no further reasoning needed. How to fix it: if the transformation is genuinely one-to-one, switch the mode to Run Once for Each Item and the script gets shorter. If you need to see the whole batch, stay in All Items and replace $json with item.json inside a loop over $input.all().
Carrying the expressions reflex over without translating it (conceptual). What happens: someone who's spent months writing {{ $json.field }} in node fields opens their first Code node and writes the same thing, and doesn't understand why something that always worked now fails. Why it happens: in expressions there's no concept of mode — n8n always processes item by item — so $json never fails there. The Code node is the only place in n8n where you can choose the other behavior, and its default value is exactly that one. How to spot it: if your intuition says "this always worked," ask yourself whether "always" was inside a form field. How to fix it: adopt the habit of checking the Mode dropdown before writing the first line, and writing the mode in the script's header comment so it's visible without opening the form.
Returning $input.item where $json should have gone (practical). What happens: you write return { json: $input.item }; and the output ends up with an extra level: every item has a field called json that contains your data. Every downstream node stops finding the fields. Why it happens: $input.item is the full envelope and $json is its content; putting an envelope inside another envelope's compartment produces a nested envelope. How to spot it: Module 1's verification point 3 — if the word json shows up as a field in the output's JSON view, this is it. How to fix it: { json: $json } or, equivalently, { json: $input.item.json }.
Looking for $binary in the Code node (practical). What happens: your workflow processes a file, you write $binary in the Code node to read it and get an undefined-variable error, or autocomplete doesn't offer it. Why it happens: it's the only access on the official table marked as unavailable in the Code node, and almost no tutorial mentions it. How to spot it: if your script mentions $binary, that's the point. How to fix it: the compartment does exist on the item, just not the shortcut. Read it with $input.item.binary in per-item mode, or with item.binary inside the loop in All Items mode. And if what you need is the file's content, first check whether there's a dedicated node that solves your case with no code.
Losing the original fields when using $json (practical). What happens: your node adds a field and, looking at the output, you discover the orders lost everything else. Why it happens: you wrote return { json: { total_units: totalUnits } }; instead of including the rest. The Code node returns exactly what you build; it preserves nothing on its own. How to spot it: compare the input panel's column list against the output panel's. How to fix it: three-dot notation: return { json: { ...$json, total_units: totalUnits } };. And if your item carried a file, remember to also copy binary: $input.item.binary.
Assuming $itemIndex tells you the batch size (conceptual). What happens: someone tries to build a field like "order 3 of 5" in per-item mode, and the 5 is nowhere to be found. Why it happens: $itemIndex gives the position, not the total, and per-item mode has no access to the set by design. How to spot it: if you need an aggregate piece of data from the batch — the total, the average, the count — you're in the wrong mode. How to fix it: switch to All Items mode, where $input.all().length gives you the total, or calculate the total in an earlier node and pass it inside each item.
Exercises
Exercise 1 — Translate in both directions. You're given this script in All Items mode. (a) Rewrite it in per-item mode using $json. (b) Then say whether the translation is completely equivalent or something gets lost.
// Node: Code — "Normalize customer names"
// Mode: Run Once for All Items
const output = [];
for (const item of $input.all()) {
const order = item.json;
output.push({
json: {
...order,
customer_name: order.customer_name.trim(),
shipping_city: order.shipping_city.toUpperCase(),
},
});
}
return output;
See solution
(a) The per-item mode version:
// ============================================================
// Node: Code — "Normalize customer names"
// Mode: Run Once for Each Item
//
// INPUT: a Cumbre order with customer_name and shipping_city
// OUTPUT: the same order, with both fields normalized
// ASSUMPTIONS: both fields are text and always present
// ============================================================
const order = $json;
return {
json: {
...order,
customer_name: order.customer_name.trim(),
shipping_city: order.shipping_city.toUpperCase(),
},
};
Notice the mechanics: the loop disappeared, the accumulator disappeared, item.json became $json, the push became a direct return, and the return's brackets went away. Five lines of scaffolding removed and not a single character of logic changed.
(b) What gets lost. In terms of result, nothing: both versions produce identical items. But there are three real differences worth knowing.
First, in favor of the per-item version: the link between input and output items is maintained automatically, because per-item mode always produces one item per input. It's lesson 2's "equal count" rule. In the All Items version, since new objects get built, pairedItem should strictly be set.
Second, in favor of the per-item version: if an order has corrupted data and blows up, in per-item mode the other runs can continue; in All Items mode the whole node fails and nothing comes out.
Third, in favor of the All Items version: it's the only one that can grow. The day the requirement asks for "and drop the duplicates" or "and add a summary," the per-item version has to be rewritten.
And a problem both share and neither solves: order.customer_name.trim() blows up if customer_name doesn't exist or isn't text. With Cumbre's whatsapp-channel orders, which frequently arrive with empty fields, that's plausible. The guard would be (order.customer_name || '').trim().
Why this works: translating between modes is mechanical and you already practiced it in Module 1. What's new here is seeing that the choice isn't just about convenience: it carries consequences for item linking, error handling, and the ability to grow.
Exercise 2 — Diagnose four scripts. For each one, say whether it works, and if not, the exact problem and the symptom it produces.
// A — Mode: Run Once for All Items
return [{ json: { customer: $json.customer_name } }];
// B — Mode: Run Once for Each Item
return { json: $input.item };
// C — Mode: Run Once for Each Item
const fileName = $binary['orders-file'].fileName;
return { json: { ...$json, attachment: fileName } };
// D — Mode: Run Once for Each Item
return {
json: { ...$json, position: $itemIndex, batch_size: $input.all().length },
};
See solution
A — Doesn't work. $json in All Items mode points to no item, because in that mode there's no current item. Symptom: customer comes out undefined, or the node throws an error about reading a property of something undefined, depending on the version. Fix: switch the mode to per-item, or read from the array: return $input.all().map((item) => ({ json: { customer: item.json.customer_name } }));
B — Works, but produces broken output. $input.item is the full envelope, so the output ends up with an extra level: every item has a json field containing the data. Symptom: no error, but every downstream node stops finding the fields, and has to reach them with $json.json.order_id. Fix: return { json: $json }; or return { json: $input.item.json };
C — Doesn't work. $binary isn't available in the Code node, per the official table. Symptom: an undefined-variable error. Fix: the compartment does exist, only the shortcut is missing: const fileName = $input.item.binary['orders-file'].fileName;. And a guard is worth adding, since not every item has attachments.
D — Half-works, and this is the most interesting one. $itemIndex is fine: in per-item mode it returns the current item's position. The problem is $input.all().length: you're asking for the batch size from a mode that, by design, has no access to the set. Symptom: version-dependent, and at best returns a number that doesn't mean what you think. Fix: if you need the batch size, switch to All Items mode — where $input.all().length is exactly what you want — or calculate the size in an earlier node and pass it inside each item.
Why this works: of the four, only A and C fail visibly. B runs green and delivers an incorrect structure, and D runs green and delivers a dubious number. It's Module 1's same asymmetry again: green isn't evidence of anything, and the reflex of comparing the output against what you expected is still the best control you have.
Exercise 3 — Verify the symptom on your own instance. This exercise gets solved with n8n open, and its result is yours, not mine. Create a Code node after your Cumbre orders seed and run it four times with these variants, writing down exactly what you see in the output panel:
(a) Run Once for All Items mode, with return [{ json: { test: $json } }];
(b) Run Once for All Items mode, with return [{ json: { test: $itemIndex } }];
(c) Run Once for Each Item mode, with return { json: { test: $json.order_id } };
(d) Run Once for Each Item mode, with return { json: { test: $input.all().length } };
Write down all four answers along with your n8n version.
See solution
There's no single answer, and that's the point. What I can tell you is what to expect and what each result means.
(a) It's the case the documentation marks as unavailable. You'll most likely see undefined, an empty object, or an error. Write down the exact text, because it's the symptom you're going to recognize the day you have to diagnose someone else's node.
(b) $itemIndex in All Items mode is the same kind of unanswerable question: there's no current item, so there's no position to return. Whatever you see — a zero, an undefined, an error — is informative, and either way it isn't data to build logic on.
(c) This should work with no surprises: one item per order comes out, with test holding its order_id. It's $json's canonical use and the check that your instance behaves as documented.
(d) This is the most revealing one. The documentation doesn't say what $input.all() does in per-item mode, so the result tells you something about your version. It might return 1, it might return the batch's real size, it might fail. Whichever it is, the recommendation doesn't change: don't build production logic on behavior the documentation doesn't promise.
Why this works: the four cases together give you something no table can: visual memory of the symptom. When, three months from now, you see an inexplicable undefined in a Code node, the question "which mode is it in?" is going to come to you automatically, because you've already seen that screen. And the habit of writing down the version next to it turns your notes into something still useful when n8n changes.
Summary and next step
In this lesson you took $json apart, n8n's most used variable. Its definition is literal and explains everything else: $json is a shortcut for $input.item.json, that is, the content of the item currently being processed. Three links — the tray, the current envelope, the letter — compressed into three letters.
From that definition comes its one condition, the only mode caveat n8n's official table makes: $json is valid in Run Once for Each Item mode. In Run Once for All Items mode your code runs exactly once for the whole batch, there's no current item, and asking about "the current one" has no useful answer. The symptom isn't a clear error but undefined, and the wrong diagnosis everyone makes the first time is doubting the field name. Detection is a text search: if the script is in All Items mode and contains $json, that's the problem.
You also saw why the expressions reflex carries over badly. In a node's fields, {{ $json.field }} always works because n8n's nodes process item by item automatically: the expressions editor always lives in something equivalent to per-item mode. The Code node is the only place in n8n where you can step outside that context, and its default value is exactly the one that leaves $json meaningless.
And you closed out the shortcut pair with the note almost no material carries: $binary, the symmetric shortcut for the file compartment, is not available in the Code node. The compartment still exists on the item — read with $input.item.binary or with item.binary inside the loop — but the shortcut doesn't. Missing the shortcut doesn't mean missing the data, and that distinction saves whole afternoons.
Before moving on you should be able to: write $json's exact equivalence from memory; say what symptom it produces in All Items mode and how to spot it in five seconds; explain why it never fails in expressions; and name the variable that does give access to the binary compartment from the Code node.
With this you've closed out layer 1. You know how to read everything that enters your node, every way possible. Lesson 5 goes out the side door: $('Node name'), the dispatcher's internal phone. You'll be able to read any workflow node's output, not just the one connected to your input, with the same family of methods you already know — .all(), .first(), .last() — plus two that only make sense when talking about another node: .item, which fetches the matching item following the pairedItem thread you learned in lesson 2, and .itemMatching(), its explicit version and the one the documentation recommends inside the Code node. It's the lesson that most expands what your code can do, and also the one with the whole module's silliest trap: if someone renames a node, your references stop working.
Resources
- Root-level variables — n8n Docs —
$json's literal definition as a shortcut for$input.item.json, and$binary's,$itemIndex's, and the other root-level variables'. - Reference previous nodes — n8n Docs — the table with the "Available in Code node?" column, where
$jsoncarries the mode caveat and$binaryshows up marked unavailable. - Understand n8n's data structure — n8n Docs — why n8n's nodes process each item separately, the underlying reason
$jsonnever fails in expressions. - Using the Code node — n8n Docs — the two modes with their exact names and the fact that
Run Once for All Itemsis the default. - Get the binary data buffer — n8n Docs — the supported route for working with a file's content from the Code node, since
$binaryisn't available. - Expressions — n8n Docs — the other environment where
$jsonlives, and the one Module 4 compares directly against the Code node.