Skip to content
Learn

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.

Lokesh Kapoor Jan 18, 2026 Updated May 30, 2026 10 min read

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 $json points 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.item is the current item, $input.all() returns every incoming item, $input.first() and $input.last() grab the ends, and $input.item.json is 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:

VariableWhat it gives youExample
$jsonJSON of the current item{{ $json.email }}
$inputThe input object (all/first/last items){{ $input.all().length }}
$node / $()Data from a specific named node{{ $('Set').item.json.id }}
$itemA specific incoming item by index{{ $item(0).$json.name }}
$nowCurrent date-time (a Luxon DateTime){{ $now.toISO() }}
$todayStart of today (a Luxon DateTime){{ $today.toISODate() }}
$workflowWorkflow metadata (id, name, active){{ $workflow.name }}
$executionCurrent execution info (id, mode){{ $execution.id }}
$varsInstance-level variables you define{{ $vars.apiBaseUrl }}
$envEnvironment variables on the host{{ $env.STAGE }}
$runIndexWhich run of the node this is{{ $runIndex }}
$itemIndexIndex 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 }} returns undefined instead of throwing if user or address is missing.
  • Nullish coalescing ?? — supplies a fallback only when the value is null or undefined: {{ $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.

Book a review

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 stringYou need multiple statements or variables
You do a quick transform on a valueYou must reshape or rename many fields at once
Logic fits readably on one lineYou need real loops, branching, or helpers
You reference one or two upstream valuesYou build a brand-new items array from scratch
No external libraries are neededYou 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, not email).
  • 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 with JSON.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.

Recommended

Ready to build for real?

Spin up n8n and start wiring expressions through your own workflows in minutes.

Try n8n

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 json and optional binary; $json is the current item's JSON.
  • Use $node / $() to reach any named node, and built-in variables like $now, $workflow, and $vars for context.
  • Dates are Luxon objects — chain .plus(), .format(), and .toISO().
  • Guard with optional chaining ?. and nullish coalescing ?? to avoid undefined.
  • 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.

Frequently asked questions

What are expressions in n8n?
Expressions are small JavaScript-powered snippets wrapped in double curly braces that pull in and transform data from other nodes at run time — for example, referencing the email field on the incoming item.
Do I need to know JavaScript to use expressions?
Not for the basics. Referencing a field is point-and-click simple. JavaScript helps for advanced string, number, array, and date work, but n8n's built-in helpers and the drag-to-map editor get beginners surprisingly far.
What is the difference between $json and $input?
$json is shorthand for the JSON of the current item coming into the node. $input is the broader object that lets you reach all incoming items with $input.all(), the first with $input.first(), and more — useful when you need more than the single current item.
How do dates work in n8n expressions?
n8n uses the Luxon library for dates. Variables like $now and $today are Luxon DateTime objects, so you can call methods such as .format(), .plus({ days: 7 }), and .toISO() directly inside an expression.
When should I use the Code node instead of an expression?
Use expressions for single values mapped into a field. Graduate to the Code node when you need loops, multiple statements, to reshape every item, call external libraries, or split and merge items in ways a one-line expression cannot express cleanly.
Why does my expression return undefined?
Usually the field path is wrong, the referenced node has not run yet, or the data is nested differently than you expect. Open the node's input panel, inspect the real JSON, and use optional chaining and the nullish coalescing operator to guard against missing values.
L

Written by

Lokesh Kapoor

Web developer, automation creator & n8n practitioner

I help creators, founders, agencies, and businesses automate smarter with n8n — from their first workflow to production-grade automation systems.