Expressions: it all starts with {{ $json.field }}
Node fields (Text on Slack, Row Values on Google Sheet, Subject on Gmail) constantly need to "drop in the data the previous node fetched". In n8n you do that with an Expression — a small piece of code wrapped in {{ }} that n8n evaluates into a real value before the node runs. This chapter covers the five forms you will use most, how to build an expression by dragging, dates, times and string handling, and how to read a red error, all in one pass.
Why you need expressions
Chapter 9 already told you that what travels between n8n nodes is items, one "row" at a time. But knowing what an item looks like is not enough — when a downstream node actually needs the data, you have to put an upstream field into a field on the downstream node, and the bridge for that is the expression.
Here are a few everyday examples you cannot build without one:
- Someone submits an order through Google Form → Slack posts "New order #1042, customer Chen Xiaoming, amount $3,500". Those three bold values come from upstream; you cannot hard-code them.
- Every day at 08:00 → Gmail sends the "2026-08-16 daily sales report", and the date has to be today, not yesterday.
- An IF node condition: "notify the manager only when the amount is > 1000". The value in front of that 1000 has to be pulled from the current item's amount field for the comparison.
- Adding a row to Google Sheet, where the name column takes the previous node's
nameand the Email column takesemail.
Without expressions you could only build one workflow per customer — which reduces n8n to a very expensive cron job. Expressions are what make one workflow serve an unlimited number of records, and no other n8n feature saves you more time.
An expression is a dynamic fill-in-the-blank
Get the idea straight first: every field inside an n8n node (say the Text field on the Slack node) can be filled in two ways.
| What to enter | What you write | What happens at run time | Example |
|---|---|---|---|
| Fixed (static) | You type directly and n8n sends it through untouched. | The same text on every run. | Type hello world in the field → Slack always sends hello world. |
| Expression (dynamic) | Wrap it in {{ }} and write a short piece of JavaScript inside. | When n8n reaches this node it evaluates what is inside {{ }} first, then writes the value back. | Type {{ $json.name }} in the field; at run time, if the current item's name is Elmo → Slack sends Elmo. |
How to switch: at the field label above the field (or on the field itself) you will see an Expression / Fixed toggle (newer UI shows a small fx icon plus an Expression tab). Left = Fixed mode, right = Expression mode. Once you switch to Expression mode the field background turns into a gridded code style and a preview block appears underneath — that is the visual marker for Expression mode, and the gridded background means you have entered the world of code.
A field can also mix static text with an expression. Say the field holds New order {{ $json.orderId }}: the static prefix is sent as it is, and only the {{ }} section gets replaced. This is the most intuitive way to build a template string.
+, .toUpperCase(), the ternary operator a ? b : c) works. What you should not do is run heavy logic or call an API in there — that is the job of the Code node (Chapter 20).The five expressions you will use most
Eight cases out of ten come down to these five. Learn them well and everything else is a combination of them.
| # | How to write it | What it means | When to use it |
|---|---|---|---|
| 1 | {{ $json.field }} |
A field on the current item. | 90% of cases — pulling the current order number into a Slack message, the customer name into a Gmail subject, the email into a new Sheet row. |
| 2 | {{ $('Google Sheets').item.json.field }} |
Takes data from a specific upstream node, however many stops sit in between. | You have been through IF, Set and Merge and still want the original form data. |
| 3 | {{ $now }} |
The current time (a Luxon DateTime object, in the workflow timezone). Equivalent to DateTime.now(). |
Adding a date to a Gmail subject, writing a timestamp into a new Sheet row, doing arithmetic against another date. |
| 4 | {{ $json.items.length }} |
The length of an array. | A Slack notification saying there are {{ $json.orders.length }} orders in total, or an IF node checking whether there are more than 10. |
| 5 | {{ $json.name.toUpperCase() }} |
Calling a method on a string (the whole JavaScript String set is available). | Normalizing case, cutting a string short, trimming whitespace, replacing text. |
$json, $now and $() here are built-in variables that n8n injects for you. They are not native JavaScript; n8n prepares them and you simply use them. Chapter 20 spells out the full list — for this chapter, get comfortable with these few.
$('Google Sheets').item.json.field is the newer form (the one promoted since 0.190+); the older form for the same thing is $node['Google Sheets'].json.field. Both still run, because n8n keeps the old form for backward compatibility. Watch the alignment semantics of the two: $('Node').item explicitly goes through pairedItem to align with the current iteration; $node['Node'].json also takes the matching item from the current run, but the form is missing the .item layer — the behavior is equivalent. Seeing both mixed in someone else's workflow is normal.Build expressions by dragging (recommended — no syntax to memorize)
Honestly, writing an expression almost never means typing code yourself — the n8n expression editor has a very good drag mechanism, so you do not have to remember any field name; one pull with the mouse and it is written. This is the one trick every n8n beginner should learn first.
-
Run the upstream node once first
Click Execute step on that upstream node (the small play button underneath it) so it produces output. Without output, the expression editor does not know which fields you can drag and shows an empty schema.
-
Open the downstream node you want the expression in
Double-click the node to open its settings panel. Find the field you want to change (say
Texton the Slack node). -
Switch the field into Expression mode
Above the field there is a Fixed / Expression tab (or a small fx icon); click the Expression side. The field background turns into a gridded code style, which means the switch worked.
-
Drag a field in from the INPUT panel on the left
Once the expression editor opens, an "INPUT" panel slides out on the left listing every field from the upstream node (you can switch between the Table / JSON / Schema views). Drag a field straight into the expression box and n8n writes the correct form for you: drag
nameand you get{{ $json.name }}. -
Check the Result preview underneath
Below the expression box there is a Result: line showing, live, the value this expression evaluates to. Seeing the value you expected means you are done; red text or an empty result means the expression has a problem.
-
Type static text straight in when you need it
You can add static text before or after the expression you dragged in. Say the expression box currently holds
{{ $json.name }}and you addHiin front → it becomesHi {{ $json.name }}, and the preview updates immediately toHi Elmo.
Dates and times: enter Luxon
n8n expressions handle time with Luxon, a JS date library (not the native Date, and not Moment). Luxon's advantages are timezone support and calendar arithmetic, with far tidier syntax than the native Date.
| How to write it | Result | When to use it |
|---|---|---|
{{ $now }} | Now, as a Luxon DateTime object (with timezone). | Timestamps, Gmail subjects, recording "when this record was processed". |
{{ $today }} | Today's date with the time at 00:00 (decided by the workflow timezone; behaves much like $now.startOf('day')). | When you want the date without the time, or to filter for records added today. |
{{ $now.plus({ days: 3 }) }} | The same moment three days later. | Setting a due date, or working out "notify if there is no reply within three days". |
{{ $now.minus({ hours: 1 }) }} | One hour ago. | An API query for "new data in the last hour". |
{{ $now.startOf('day') }} | Today at 00:00:00. | The start time when you want "everything from today". |
{{ $now.toFormat('yyyy-MM-dd') }} | The string 2026-08-16. | Dropping into a Gmail subject, a file name, or a Sheet column. |
{{ $now.toFormat('yyyy-MM-dd HH:mm') }} | The string 2026-08-16 14:30. | A timestamp that has to include hours and minutes. |
{{ DateTime.fromISO($json.created_at) }} | Turns an ISO string returned by an API into a Luxon object. | When you need to do arithmetic on, or format, a date an API gave you. |
Every Luxon calendar unit works (days, hours, months, weeks, years) — do not drop the plural s. The toFormat pattern is Luxon-specific too: yyyy is a four-digit year, MM a two-digit month, dd a two-digit day, HH the 24-hour clock, mm minutes.
Europe/London. After that $now and $today use that zone. For cross-timezone data, state the destination explicitly, for example .setZone('Europe/London').Joining strings: pick one of three styles
Slack messages, Gmail subjects and Sheet columns are all strings — to drop an upstream field into a sentence, n8n gives you three styles. Pick whichever reads best to you; they all achieve the same thing.
| How to write it | Result | When to use it |
|---|---|---|
Mixed into the field:Hi {{ $json.name }} |
Evaluates to Hi Elmo. |
The one to prefer. As long as what sits outside the {{ }} is static text, mixing directly is the shortest. |
Plus sign:{{ 'Hi ' + $json.name }} |
Evaluates to Hi Elmo. |
When you have to build the string inside the {{ }} — for example to wrap the whole thing in toUpperCase(). |
Template literal (backtick):{{ `Hi ${$json.name}, order ${$json.orderId}` }} |
Evaluates to Hi Elmo, order 1042. |
Cleanest when one {{ }} has to interpolate several variables with punctuation between them. |
Common string methods (all standard JavaScript String methods):
| How to write it | What it does | Example |
|---|---|---|
.toUpperCase() | Upper-cases everything. | {{ $json.code.toUpperCase() }} → ABC123 |
.toLowerCase() | Lower-cases everything. | {{ $json.email.toLowerCase() }} |
.trim() | Strips leading and trailing whitespace. | Use it to clean up the " Elmo " you got from Google Form. |
.slice(0, 100) | Takes the first 100 characters. | Keeps a Slack message preview from blowing up: {{ $json.body.slice(0, 100) }} |
.replace('old', 'new') | Replaces text. | {{ $json.phone.replace(/-/g, '') }} (removes every -) |
.split(',') | Splits into an array. | Turns a,b,c into ['a','b','c']. |
{{ $json.name?.toUpperCase() }}. Add a ? and, when name does not exist, you get undefined back instead of an error — far tidier than an if check.Common cases you can copy
Here are the common use cases you will meet and the expression for each — open the workflow, switch the field to Expression mode, and copy them as-is.
| Scenario | Expression | What it means |
|---|---|---|
| Slack message template: new order notification | {{ `New order #${$json.orderId} | Customer ${$json.customer} | Amount $${$json.amount}` }} |
Built in one template literal, with the separator characters typed straight in. |
| New Google Sheet row, name column | {{ $('Webhook').item.json.name }} |
Explicitly reaches back for name on the Webhook node, so it does not break even if a Set node is dropped in between. |
| Dynamic date in a Gmail subject | {{ 'Daily sales report - ' + $now.toFormat('yyyy-MM-dd') }} |
Fixed text plus Luxon formatting; at run time it becomes Daily sales report - 2026-08-16. |
| IF node condition: amount greater than 1000 | Left value {{ $json.amount }}, operator >, right value 1000 |
The IF node does not take a raw boolean — the operator has to be picked in its UI, and only the left and right boxes are expression fields. To combine conditions use several conditions plus AND/OR, or put a Set node in front to work the boolean out first and let IF test that. |
| Dynamic HTTP Request URL | https://api.example.com/orders/{{ $json.orderId }} |
URLs support mixed expressions too, so orderId goes straight into the path. |
| Airtable upload: turn the tags array into a string | {{ $json.tags.join(', ') }} |
Some Airtable fields only accept strings, so join turns the array into a comma-separated list. |
| File name with a timestamp | {{ 'report_' + $now.toFormat('yyyyMMdd_HHmm') + '.pdf' }} |
Gives report_20260816_1430.pdf, so nothing is overwritten by a same-named file. |
| Ternary operator: use the value if there is one, otherwise a default | {{ $json.nickname ? $json.nickname : $json.name }} |
Nickname first, falling back to the real name when there is none. There is a shorter form: {{ $json.nickname || $json.name }}. |
How to read a red error
Red text in an expression field, or a red border around it, is nothing to panic about — n8n tells you the reason right below the expression panel, and once you can read that message you can fix it. These four are the most common kinds of red text and their fixes.
-
The Result shows
undefinedThe most common one. Usually a misspelled field name — the real field is
nameand you typed$json.nameewith an extra e. The fix: switch the INPUT panel to the Schema view and confirm how the field is really written (case, spaces, whether it is nested likedata.name). Dragging never misspells. -
The Result shows
[Cannot read property 'X' of undefined]The parent of the thing you are reaching for is empty. Take
$json.customer.email: thecustomerfield does not exist, so n8n cannot reademailand throws this. Two fixes: (a) sidestep it with optional chaining,$json.customer?.email; (b) fix the upstream node first and confirm it really returnscustomer. -
The Result shows
[Referenced node doesn't exist]You wrote
$('Google Sheets')or$node['Google Sheets'], but there is no node called "Google Sheets" on the canvas — usually the node was renamed (double-click a node to rename it), or the case or the spacing does not match. The fix: go back to the canvas and see what the node is actually called (spaces included), then copy that name across. -
The Result shows a schema instead of real values
The preview shows the field structure but no real values? That means the upstream node has not run yet — n8n does not know what it will actually fetch. The fix: click Execute step once on the upstream node so it produces real output, and Result will have real values when you come back.
Advanced: how $json, $node and $input differ
Seeing all three mixed in someone else's workflow is confusing, but the logic is simple — they differ on whose data you are taking, and on whether you take one item or all of them.
| Variable | What it refers to | When to use it |
|---|---|---|
$json |
The current iteration's item, specifically its json part. |
90% of cases. The node runs N times in a row, and each time $json points at a different item. |
$binary |
The binary part of the current item (only there when it has an attachment). | When you handle files: $binary.attachment_0.fileName. |
$input.item |
The whole current item object (both json and binary). |
In a Code node, or when you need json and binary at once. |
$input.all() |
The array of all items this node received (each element carries .json and .binary). |
Aggregating over the whole batch ($input.all().length, $input.all().map(i => i.json.amount).reduce(...)); available both in expressions and in the Code node (Chapter 20). |
$input.first() / $input.last() |
The first / last item this node received. | Taking the head or the tail of a batch for a summary (a report node that only needs the start time, $input.first().json.startedAt). |
$('NodeName') |
The output of any upstream node, not only the one directly connected. | Taking the original data from two or three stops back. The newer form. |
$node['NodeName'] |
The same, in the older form. | You will meet it in older workflows and templates; it does the same job. |
$now / $today |
The current time / today at 00:00 (a Luxon DateTime, in the workflow timezone). | Use it for everything to do with dates and times. DateTime.now() gives you an object of the same type. |
$itemIndex |
This iteration's item and its index in this node's input array (starting at 0). | An opening line for the first record, a separator every 10 records, or putting "record N" into a message: {{ $itemIndex + 1 }}. |
$workflow |
Data about the current workflow (.id, .name, .active). |
Putting the workflow name into a failure notification so it is easy to trace. |
$execution |
Data about this particular run (.id, .mode, .resumeUrl). |
Adding the execution id to an error message makes it easy to look up later; the Wait node also resumes through .resumeUrl. |
$vars / $env |
Workflow / environment variables (every Cloud edition; on self-hosted, $vars needs Enterprise, and $env needs N8N_BLOCK_ENV_ACCESS_IN_NODE=false). |
Keeping API endpoints and thresholds in one place instead of rewriting every workflow. |
$('NodeName').first(), $('NodeName').last() and $('NodeName').all() are the three common accessors: first/last take one item from the head or the tail, all takes the whole array. $('NodeName').item takes "the item matching the current iteration" (aligned through pairedItem, as Chapter 9 explained).
Common pitfalls
-
The expression is not evaluated and Slack sends the literal text
{{ $json.name }}You forgot to flip the field's fx toggle. In Fixed mode
{{ }}is ordinary text and is never evaluated. Go back to the node, open that field, switch it to Expression mode in the top right (the background turns dark), and save it again. -
The Result shows
undefinedThree possibilities: (a) the field name is misspelled — switch to the Schema view and check the real name against it; dragging is the safest; (b) the upstream node simply does not return that field — look at the actual JSON in its output panel; (c) the case is wrong —
Nameandnameare two different things. -
You cannot reach the upstream node:
Referenced node doesn't existThe node name is wrong. The name inside
$('Google Sheets')has to be exactly what the canvas shows — spaces, case and brackets included. The safest move is to copy the node name from the canvas and paste it in rather than typing it yourself. -
The date is in the wrong timezone
$now.toFormat('yyyy-MM-dd HH:mm')showing the wrong zone? Note: open Settings → Timezone in the workflow and choose your location’s IANA timezone; for example,Europe/London. After that$nowand$todayboth follow it. To convert UTC data, state the destination explicitly, for example.setZone('Europe/London'). -
The preview shows an old value that does not match the actual run
The expression preview uses the output from the upstream node's last run — if you changed an upstream parameter and have not re-run it, the preview is the old value. Click Execute step upstream to produce fresh output, then come back and look at the preview.
-
An expression asks an array for
.lengthand getsundefinedThat field is not an array. Switch to the Schema view and confirm its type — if the API returned something that looks like an array but is really a string (for example
"[1,2,3]"), you have to turn it into a real array withJSON.parse($json.list)before.lengthworks.
FAQ
Is there a full syntax spec for expressions?
function / const / let, cannot await, cannot require an external package, and cannot write if / for statements. Dates and times go through Luxon (not Moment, not the native Date): use $now, $today, or the global DateTime to build an object (for example DateTime.now() or DateTime.fromISO(...)). For the details see the official docs at docs.n8n.io/code/expressions/ and docs.n8n.io/code/builtin/.Can I write a for loop or an if inside an expression?
a ? b : c, is fine, and so is a short || check ($json.x || 'default'). But when you genuinely need a for loop, nested ifs or a complex data transformation, do not force it into an expression — it becomes very hard to read and to debug. Move to the Code node (Chapter 20) instead, write full JavaScript or Python, and get syntax highlighting with it. Expressions suit a dynamic value you can state in one line; the Code node suits work that has logic to run.Are expressions slow to run?
How do I look up which built-in functions and variables are available?
$ ($json, $now, $workflow...) plus the common Luxon methods. Click one of the names and you get its description and a short example. If you cannot find it, go straight to the official docs at docs.n8n.io/code/builtin/, which carry the full list. For Luxon's date methods, see moment.github.io/luxon/.How is $json.field different from $('Previous Node').item.json.field?
$json says "whatever the upstream is, this is the data I just received"; $('NodeName') says "go and take it from that node". When the workflow has branches, or a Merge or Set node dropped in the middle, $('NodeName') lets you skip the intermediate stops and take the original data, so a field rewritten along the way cannot trip you up. Start with $json as a beginner; once workflows get complex you will reach for $('NodeName') naturally.Can I save an expression as a constant and reuse it?
const x = ...). There are two ways to reuse the same calculation: (a) use a Set node to work it out first and store it in a field, then have every downstream node take it with $('Set').item.json.myVar; (b) use workflow-level Variables (available on Cloud, and on self-hosted Enterprise), defined in Settings and then read anywhere in the workflow with $vars.myVar. In practice (a), the Set node, is the most direct route for a beginner.Someone's workflow writes $node['Foo'].json.bar — can I change it to $('Foo').item.json.bar?
$node[] form for backward compatibility while promoting the newer $('NodeName'). Use the new one in new projects (it is shorter, and it is what the official docs lead with); there is no need to rewrite old workflows. Mixing them will not break anything, it just looks inconsistent in style. Chapter 6, where you learn to edit templates, is where you will run into the old form most often — being able to read it is enough.