FlowKit

n8n expressions: syntax, $json and $node variables, and the classic errors

Published 29 July 2026 · 5 min read

Expressions are the glue of n8n: every node field can contain, between double curly braces, a JavaScript fragment that reaches into the workflow's data. That's what turns a chain of static nodes into a dynamic pipeline — and it's also the first source of confusion for beginners, between $json, $('Node Name'), items and [object Object] errors. This guide lays out the full syntax, the built-in variables, and the traps that show up in every workflow.

The basic syntax: double curly braces, JavaScript inside

An expression is written {{ ... }} in any node field switched to Expression mode (the small toggle next to the field, or by drag-and-dropping data from the input panel). Inside: standard JavaScript, evaluated for each item flowing through the node.

Hi {{ $json.firstName }}, your order {{ $json.order.id }} is confirmed.
Total incl. tax: {{ ($json.subtotal * 1.2).toFixed(2) }} €

Anything JavaScript can do fits in an expression: arithmetic, string methods (.toUpperCase(), .split(), .trim()), ternaries, template literals. The limit is structural: an expression returns one value for one field. As soon as you need several steps, loops, or to restructure the items themselves, switch to the Code node — the boundary between the two is the subject of our guide on expressions and JavaScript in the Code node.

$json: the current item's data

$json is the JSON object of the item being processed, as it leaves the previous node:

  • {{ $json.email }} — simple property;
  • {{ $json['Customer name'] }} — property with a space (bracket notation required);
  • {{ $json.lines[0].amount }} — arrays and nested objects;
  • {{ $json.address?.city }} — optional chaining so a missing address doesn't crash the expression.

The reflex that saves 80% of trial and error: drag and drop the data from the node's input panel into the field. n8n writes the exact expression, nested path included. And to check what $json actually contains at a given point, the input panel in JSON view is the source of truth — the techniques from our debugging guide apply.

$('Node Name'): referencing any upstream node

The previous node isn't always enough: after an enrichment step or an AI call, you often need a field from the initial webhook. That's what $('Node Name') is for:

{{ $('Webhook').item.json.email }}      → the linked item in the Webhook node's output
{{ $('Config').first().json.threshold }} → the first item of the Config node's output
{{ $('Search').all().length }}           → how many items the Search node produced

.item follows item lineage: n8n finds the item in the referenced node that the current item descends from — which is what you want 90% of the time. .first(), .last() and .all() ignore that linkage and take the raw output. Two constraints: the name must match exactly (renaming a node breaks expressions that reference it — n8n updates them inside the editor, but beware when copy-pasting between workflows), and the referenced node must have executed in the same branch, otherwise you get "Referenced node is unexecuted".

The built-in variables worth knowing

  • {{ $now }} and {{ $today }}: the current date as a Luxon object, giving you formatting and arithmetic — {{ $now.minus({days: 7}).toFormat('yyyy-MM-dd') }}. The subtleties (time zones, parsing, diffs) are in our Luxon dates and times guide;
  • {{ $workflow.name }}, {{ $workflow.id }}, {{ $execution.id }}: invaluable for meaningful logs and error workflows;
  • {{ $env.MY_VARIABLE }}: reads a server environment variable (self-hosted) — the right way to inject per-environment URLs and configuration, detailed in our environment variables guide;
  • {{ $itemIndex }} and {{ $runIndex }}: the current item's position and the pass number inside a loop;
  • {{ $if(condition, ifTrue, ifFalse) }} and {{ $ifEmpty(value, fallback) }}: the two helpers that keep conditional fields readable without nested ternaries.

On top of these come n8n's data transformation extensions, called like methods: {{ $json.email.extractDomain() }}, {{ $json.title.toSnakeCase() }}, {{ $json.tags.removeDuplicates() }} — shortcuts documented in the official reference that spare you many one-line Code nodes.

The classic errors and how to diagnose them

  • [object Object]: the expression returns an object into a text field. Target the property ($json.customer.name) or serialize (JSON.stringify($json.customer));
  • undefined: the path doesn't exist for this item — a typo, or one item in ten missing the field. Optional chaining (?.) and $ifEmpty() make the expression robust;
  • "Referenced node is unexecuted": the target node didn't run (other branch of an IF, or not yet executed in test mode). Run the full workflow before testing a node in isolation, or merge the branches before the reference;
  • Comparing mismatched types: $json.amount > "100" sometimes works through coercion, until the day it doesn't. Convert explicitly (Number($json.amount)), especially upstream of an IF or Switch node.

These difficulties are neither anecdotal nor embarrassing: end-user programming research formalized them long ago. The study by Andrew Ko, Brad Myers and Htet Htet Aung, "Six Learning Barriers in End-User Programming Systems" (IEEE Symposium on Visual Languages and Human Centric Computing, 2004, see on Google Scholar), pinpoints exactly the barriers at play here — notably knowing which construct to use (the selection barrier) and understanding why the result doesn't match expectations (the understanding barrier). n8n's expression drag-and-drop and live preview are precisely the kind of tooling this literature recommends.

Expressions or Edit Fields: structure instead of piling up

One last architectural tip: when a node accumulates five complex expressions across five fields, it's often more readable to prepare the values upstream in an Edit Fields (Set) node — one expression per named field, testable in isolation — then reference those clean fields. That's the whole point of our Set / Edit Fields node guide: expressions do the transformation, Edit Fields gives it a maintainable shape.

In short

n8n expressions are JavaScript between {{ }} evaluated item by item: $json for the current item, $('Node Name') for anything upstream, $now/$env/$if() for context and conditions. Master item lineage, convert your types explicitly, and move complexity into Edit Fields or the Code node when an expression stops fitting on one readable line — your workflows will gain as much in robustness as in debuggability.

FAQ

Frequently asked questions

What's the difference between an n8n expression and the Code node?

An expression is a JavaScript fragment between double curly braces {{ }} evaluated inside a node field, item by item: perfect for transforming one value on the fly. The Code node runs a real script over the whole set of items, with multi-line logic, loops and functions. Rule of thumb: one line, one value → expression; multiple steps or restructuring the items themselves → Code node.

How do I read data from a node further up the workflow, not just the previous node?

With $('Node Name'): for instance {{ $('Webhook').item.json.email }} reads the email field from the matching item in the Webhook node's output, even if ten nodes ran in between. .first() and .last() grab the first or last item, .all() the full list. Caveat: the referenced node must have executed in the same branch.

Why does my expression print [object Object]?

Because the expression returns an object into a field that expects text: JavaScript converts it to the string "[object Object]". Either target a specific property ({{ $json.customer.name }} instead of {{ $json.customer }}), or serialize the object with {{ JSON.stringify($json.customer) }} if the full JSON is genuinely what you want to write.

What does the "Referenced node is unexecuted" error mean in an n8n expression?

The expression points to a node that didn't run in the current execution — typically because it sits in the other branch of an IF or Switch, or downstream. When testing, run the whole workflow first; in production, only reference nodes upstream in the same branch, or pass the value through an Edit Fields node shared by both branches.

Bundle FlowKit Complet

€269