Complete "Lesson 1: Conditional Branching with the IF and Switch Nodes" first to unlock this lesson.Go to previous lesson

Introduction

In n8n, expressions are the bridge between raw data and dynamic logic. Instead of hardcoding values, you write expressions that read data from previous nodes, apply transformations, and pass the result to the next step — all without writing a full Code node.

Every expression in n8n is wrapped in double curly braces and evaluated at runtime. The engine behind them is a lightweight JavaScript environment with a set of built-in variables and helper methods. Understanding this layer is what separates a brittle workflow from one that actually handles real-world data.

Expression Syntax and Built-in Variables

The two most important built-in variables you will use constantly are $json and $input. $json refers to the JSON body of the current item being processed. $input gives you access to the full input object, including the ability to reference other items in the same run.

To reference a field from the current item:

javascript
{{ $json.email }}

To reference output from a specific previous node by name:

javascript
{{ $node["HTTP Request"].json.data.userId }}

To access the index of the current item in a batch:

javascript
{{ $itemIndex }}

To get the total number of items coming into the current node:

javascript
{{ $input.all().length }}

These variables are available in almost every field that shows the expression toggle — input fields, Set node values, IF conditions, HTTP Request parameters, and more.

flowchart showing how $json, $input, and $node[] variables map to data flowing between three nodes in sequence

Transforming Data with Expressions

Beyond simple field access, expressions support the full JavaScript string, array, and object API. This is where transformations happen inline without a dedicated Code node.

Convert a string to uppercase:

javascript
{{ $json.name.toUpperCase() }}

Trim whitespace and check a condition:

javascript
{{ $json.status.trim() === "active" ? "send" : "skip" }}

Build a formatted string from multiple fields:

javascript
{{ `Hello ${$json.firstName} ${$json.lastName}, your order #${$json.orderId} is ready.` }}

Parse a date and reformat it using the built-in $now and Luxon integration:

javascript
{{ DateTime.fromISO($json.createdAt).toFormat("dd/MM/yyyy") }}

n8n bundles Luxon for date handling. You reference it directly as DateTime inside any expression — no import needed.

Flatten a nested field safely using optional chaining:

javascript
{{ $json.address?.city ?? "Unknown" }}

This pattern — optional chaining plus nullish coalescing — is critical when your upstream data is inconsistent. It prevents expressions from throwing errors that would kill the entire execution.

Working with Arrays in Expressions

When a node outputs multiple items, you sometimes need to aggregate or filter them from within a downstream expression. $input.all() gives you the full array of items as objects with a json property.

Get the email from every item in the batch as an array:

javascript
{{ $input.all().map(item => item.json.email) }}

Filter items where a field meets a condition and count them:

javascript
{{ $input.all().filter(item => item.json.score > 80).length }}

Sum a numeric field across all items:

javascript
{{ $input.all().reduce((acc, item) => acc + item.json.amount, 0) }}

These expressions work well inside a Set node or a Merge node where you want to produce a single aggregated value from a batch before passing it downstream.

diagram comparing single-item expression ($json.field) vs batch expression ($input.all().map()) with arrows showing data flow

Common Mistakes

The most frequent issue is referencing a field that does not exist on some items. The expression throws a TypeError and the execution stops. Always use optional chaining when the field is not guaranteed.

Another common mistake is forgetting that $json refers to the current item, not the entire output of a node. If a node emits five items and you process them with $json.value, each item is processed independently — you are not getting an array of five values, you are getting one value per execution pass.

Confusing $node["NodeName"].json with $json is also a regular source of bugs. $node[] pins you to a specific node's last output regardless of the current execution path, which can return stale data in branching workflows. Use it deliberately and sparingly.

Finally, avoid putting complex multi-step logic inside a single expression. Once you need more than two chained operations or a loop with side effects, move that logic into a Code node. Expressions are for transformations, not for control flow.

Summary

Expressions in n8n let you read, transform, and combine data dynamically using $json, $input, and $node[] as your primary access points. JavaScript string, array, and date methods work natively inside double curly braces, and Luxon is available for date formatting. Safe access patterns like optional chaining and nullish coalescing keep your workflows resilient to inconsistent upstream data. When transformation logic grows beyond a few chained operations, the Code node is the right tool — expressions cover everything else.

Lesson practice

Lesson Checkpoint

Review what you learned and get feedback on your work.

0/1
Answered · 0/1
Lesson 2: Working with Expressions and Data Transformations