n8n Expressions Explained: Work With Data Like a Pro
Master n8n expressions: the items data model, built-in variables, Luxon dates, JS transforms, a copy-paste cookbook, and debugging tips.
On this page
- What an expression actually is
- The data model: items, json, and binary
- Referencing data from other nodes
- Built-in variables reference
- Transforming data with JavaScript
- Working with dates: Luxon
- Working with arrays and looping
- Expressions vs the Code node — when to graduate
- The expression cookbook
- Debugging expressions
- Key takeaways
Expressions are how data moves and transforms inside n8n. Almost every field on every node can hold either a fixed value you type, or a live expression that reads data from elsewhere in the workflow and reshapes it on the fly. Once expressions click, you stop copy-pasting between nodes by hand and start wiring data through your automation exactly the way you want it.
This guide takes you from the double-curly-brace syntax all the way to a copy-paste cookbook and a clear rule for when to reach for the Code node instead. If n8n itself is new to you, skim what is n8n and the beginner's guide first, then come back here.
What an expression actually is
An expression is a snippet wrapped in double curly braces that n8n
evaluates at run time. Inside those braces you write a JavaScript-style
expression that resolves to a single value. For example, {{ $json.email }}
pulls the email field from the data entering the current node.
Every field on a node has two modes:
- Fixed mode — you type a literal value, like
hello@example.com. - Expression mode — you write an expression that is computed for each
item, like
{{ $json.email }}.
You toggle a field into expression mode with the small fx (or gears) icon
that appears when you hover over it. In expression mode the whole field is
treated as a template: anything outside the braces is literal text, and
anything inside the braces is evaluated. So
Hi {{ $json.firstName }}, welcome! produces a personalized greeting per item.
The one idea that unlocks everything
$json always means "the JSON of the item currently entering this node."
Every other variable and helper builds on that single concept. If you only
remember one thing, remember that.
Use fixed mode when a value never changes (an API base URL, a constant subject line). Use expression mode the moment a value depends on incoming data, the current time, an environment variable, or anything computed.
The data model: items, json, and binary
n8n does not pass around a single object between nodes — it passes an array of items. Each node receives an array, runs once per item (for most nodes), and outputs an array. Understanding this shape is what separates people who fight n8n from people who flow with it.
Each item is an object with up to two keys:
json— the structured data, a plain JSON object. This is what$jsonpoints at for the current item.binary— optional attached files (images, PDFs, CSVs) with metadata.
So an items array might look like this:
[
{ "json": { "name": "Ada", "email": "ada@example.com" } },
{ "json": { "name": "Grace", "email": "grace@example.com" } }
]
When a node runs for the second item, {{ $json.name }} resolves to Grace.
The expression is re-evaluated for each item automatically — you almost
never loop manually just to process a list.
A few related accessors are worth knowing early:
$json— the JSON of the current item.$input— the structured input object.$input.itemis the current item,$input.all()returns every incoming item,$input.first()and$input.last()grab the ends, and$input.item.jsonis equivalent to$json.$item(index)— access a specific incoming item by position, e.g.{{ $item(0).$json.email }}.
Referencing data from other nodes
$json only sees the node directly upstream. To reach back to any named
node in the workflow, use $node (or its modern equivalent $()):
{{ $node["Webhook"].json.body.email }}
{{ $('Webhook').item.json.body.email }}
Both forms read the email field from the body of the node named Webhook.
The $('Node Name') syntax is the newer, friendlier one and supports
.item, .all(), .first(), and .last() just like $input.
Drag instead of typing paths
You rarely need to type these paths by hand. Open the expression editor, find the field in the left-hand data panel, and drag it into the expression. n8n writes the correct reference for you — including the exact node name and the full nested path.
This is the trick behind reading webhook payloads, combining a record from Airtable with a response from an OpenAI node, and stitching several steps together.
Built-in variables reference
n8n exposes a set of always-available variables. Here are the ones you will reach for most:
| Variable | What it gives you | Example |
|---|---|---|
$json | JSON of the current item | {{ $json.email }} |
$input | The input object (all/first/last items) | {{ $input.all().length }} |
$node / $() | Data from a specific named node | {{ $('Set').item.json.id }} |
$item | A specific incoming item by index | {{ $item(0).$json.name }} |
$now | Current date-time (a Luxon DateTime) | {{ $now.toISO() }} |
$today | Start of today (a Luxon DateTime) | {{ $today.toISODate() }} |
$workflow | Workflow metadata (id, name, active) | {{ $workflow.name }} |
$execution | Current execution info (id, mode) | {{ $execution.id }} |
$vars | Instance-level variables you define | {{ $vars.apiBaseUrl }} |
$env | Environment variables on the host | {{ $env.STAGE }} |
$runIndex | Which run of the node this is | {{ $runIndex }} |
$itemIndex | Index of the current item | {{ $itemIndex }} |
$vars and $env are great for keeping secrets and environment-specific
config out of individual nodes. For actual API keys and tokens, prefer proper
credentials rather than environment
variables.
Transforming data with JavaScript
Inside the braces you have real JavaScript expressions, so the standard methods work on whatever value you reference.
{{ $json.firstName.toUpperCase() }}
{{ $json.title.trim().toLowerCase() }}
{{ $json.price * 1.2 }}
{{ Math.round($json.score * 100) / 100 }}
{{ $json.tags.join(', ') }}
{{ $json.items.filter(i => i.active).length }}
Two operators deserve special attention because they prevent most "undefined" headaches:
- Optional chaining
?.— safely reaches into nested data that might not exist:{{ $json.user?.address?.city }}returnsundefinedinstead of throwing ifuseroraddressis missing. - Nullish coalescing
??— supplies a fallback only when the value isnullorundefined:{{ $json.name ?? 'Unknown' }}.
Combine them for robust field mapping:
{{ $json.profile?.displayName ?? 'Guest' }}.
n8n also ships data-transformation helpers that attach methods directly to
strings, numbers, arrays, and objects. For example a string gains
.toSnakeCase() and .extractEmail(), arrays gain .first() / .last(),
and you will see these suggested in the editor's autocomplete as you type a
dot after a value.
Working with dates: Luxon
n8n uses the Luxon date library, not plain JavaScript Date. Both $now
and $today are Luxon DateTime objects, which means you get a rich, chainable
API right inside an expression.
{{ $now.format('yyyy-MM-dd') }}
{{ $now.toISO() }}
{{ $now.plus({ days: 7 }).toISODate() }}
{{ $now.minus({ hours: 2 }).toFormat('HH:mm') }}
{{ $now.setZone('America/New_York').toFormat('ff') }}
{{ $now.diff($today, 'hours').hours }}
To parse an incoming date string into a Luxon object so you can do math on it,
wrap it: {{ DateTime.fromISO($json.createdAt).plus({ days: 30 }).toISODate() }}.
The DateTime object is available globally in expressions.
Working with arrays and looping
Because nodes already iterate over items, you often do not need an explicit
loop at all. Map a field and n8n applies it to every item. When you do need to
operate across the whole batch, reach for $input.all() and standard array
methods:
{{ $input.all().map(i => i.json.email).join(', ') }}
{{ $input.all().reduce((sum, i) => sum + i.json.amount, 0) }}
{{ $input.all().filter(i => i.json.status === 'paid').length }}
When you genuinely need to process items one batch at a time (rate-limited APIs, chunked uploads), use the dedicated Loop Over Items (Split In Batches) node rather than trying to express the loop inline. Expressions compute values; nodes control flow.
Wrestling with data mapping?
Bring a workflow that is fighting you and we will untangle the expressions and item flow together, live.
Expressions vs the Code node — when to graduate
Expressions are perfect for producing one value for one field. The moment your logic outgrows a single expression, the Code node is the right tool. Use this rule of thumb:
| Use an expression when... | Reach for the Code node when... |
|---|---|
| You map a single field or build one string | You need multiple statements or variables |
| You do a quick transform on a value | You must reshape or rename many fields at once |
| Logic fits readably on one line | You need real loops, branching, or helpers |
| You reference one or two upstream values | You build a brand-new items array from scratch |
| No external libraries are needed | You want to use built-in libraries or split/merge items |
A Code node returns an array of items (each with a json key), runs in
"Run Once for All Items" or "Run Once for Each Item" mode, and gives you the
full language. Think of expressions and the Code node as the same engine at
two altitudes: inline for small jobs, a full block when the job grows. This
matters a lot when you build AI agent workflows,
where payloads get nested and dynamic fast.
The expression cookbook
Copy-paste recipes for the things you will actually need. Each is a single expression; drop it into a field in expression mode.
{{ ($json.firstName + ' ' + $json.lastName).trim() }}
Build a full name from two fields, trimming stray spaces if one is empty.
{{ $json.email.split('@')[1] }}
Extract the domain portion of an email address.
{{ $json.name ?? 'there' }}
Provide a safe fallback so a missing name does not break a greeting.
{{ $json.amount.toFixed(2) }}
Format a number as a currency-style string with two decimal places.
{{ $json.tags?.join(', ') ?? 'none' }}
Join an array of tags into a comma list, with a fallback when there are none.
{{ $now.toFormat('yyyy-LL-dd HH:mm') }}
A clean, human-readable timestamp for logs or message bodies.
{{ $now.plus({ days: 30 }).toISODate() }}
Compute a date 30 days from now — handy for trial expirations or due dates.
{{ JSON.stringify($json) }}
Serialize the entire current item to a string, useful for debugging or logging.
{{ $json.status.toLowerCase() === 'active' ? 'On' : 'Off' }}
A ternary that maps a status field to a friendly label.
{{ $('Set').item.json.userId }}
Pull a value from an earlier named node, not just the immediate previous one.
{{ $input.all().map(i => i.json.id).join(',') }}
Collapse every incoming item's id into a single comma-separated string.
{{ decodeURIComponent($json.query?.ref ?? '') }}
Safely decode a URL query parameter that may be absent.
Debugging expressions
The expression editor is your best debugging tool. As you type, it shows a live preview of the resolved value using real data from the latest execution. If the preview shows what you expect, the expression is correct.
When something goes wrong, the usual suspects are:
undefined— the path is wrong, or the field genuinely is not there. Open the node's input panel and inspect the actual JSON. Field names are case-sensitive and often nested (e.g.body.data.email, notemail).- Referenced node has not run —
$('Some Node')only returns data once that node has produced output in the current execution. Run upstream nodes first. [object Object]— you returned an object where a string was expected. Reference a specific field, or wrap it withJSON.stringify(...).- Type mismatches — incoming numbers can arrive as strings from webhooks.
Coerce with
Number($json.price)before doing math.
Inspect the real data, do not guess
The single biggest source of broken expressions is assuming a data shape
instead of looking at it. Always pin the node, run the previous step, and read
the exact JSON in the input panel before writing your path. Guarding with ?.
and ?? then makes the expression resilient to the messy bits.
For more workflow ideas to practice these patterns on, browse the best automation ideas for small businesses or grab a self-hosted instance from the n8n tools page.
Ready to build for real?
Spin up n8n and start wiring expressions through your own workflows in minutes.
Affiliate link — we may earn a commission at no extra cost to you.
Key takeaways
- Expressions live in double curly braces and produce one value, evaluated per item at run time.
- The data model is an array of items, each with
jsonand optionalbinary;$jsonis the current item's JSON. - Use
$node/$()to reach any named node, and built-in variables like$now,$workflow, and$varsfor context. - Dates are Luxon objects — chain
.plus(),.format(), and.toISO(). - Guard with optional chaining
?.and nullish coalescing??to avoidundefined. - Graduate to the Code node when logic outgrows a single readable line.
Master these and you can connect any node to any other and shape the data exactly how you need it.