n8n Variables and Expressions:
Complete Reference Guide 2026

n8n's expression system is what separates simple automations from truly dynamic, data-driven workflows. The {{ }} syntax gives you access to data from any previous node, built-in variables, JavaScript functions, and environment configuration — all inline, without a separate Code node. Mastering expressions unlocks the full power of n8n. This guide is a complete reference you can bookmark and return to.

Expression Syntax Basics

Wrap any expression in double curly braces: {{ your expression here }}. Everything inside is evaluated as JavaScript. You can reference n8n variables, use string methods, do arithmetic, call ternary operators, and chain operations. Outside the braces is treated as literal text.

Expression Syntax Fundamentals

Expressions can appear in any text field in n8n — node parameters, headers, URLs, message bodies, and more. Toggle a field to expression mode by clicking the bolt icon (⚡) next to any input field, or by typing {{.

Literal vs expression mixing

# Literal text only (no expression — always shows exactly this) Hello World # Pure expression (entire field is one expression) {{ $json.name }} # Mixed: literal text with embedded expressions Hello, {{ $json.firstName }}! Your order {{ $json.orderId }} is ready. # Multiple expressions in one field {{ $json.city }}, {{ $json.country }}{{ $json.postalCode }} # Arithmetic Total: ${{ ($json.price * $json.quantity).toFixed(2) }} # Conditional (ternary) {{ $json.isVip ? 'VIP Customer' : 'Standard Customer' }}
Expression Editor

Click the bolt icon ⚡ on any field to open the full Expression Editor. It provides autocomplete for all built-in variables, a live preview showing the resolved value with your actual current data, and syntax highlighting. Use it when building complex expressions — much easier than guessing.

$json — Accessing Current Node Data

$json refers to the JSON data of the current item being processed. It's the most commonly used variable in n8n.

# Given this item data from the previous node: { "id": 42, "name": "Alice Johnson", "email": "alice@example.com", "address": { "city": "London", "zip": "SW1A 1AA" }, "tags": ["premium", "active"] } # Access flat fields: {{ $json.id }} → 42 {{ $json.name }} → "Alice Johnson" {{ $json.email }} → "alice@example.com" # Access nested objects: {{ $json.address.city }} → "London" {{ $json.address.zip }} → "SW1A 1AA" # Access array elements: {{ $json.tags[0] }} → "premium" {{ $json.tags.length }} → 2 {{ $json.tags.join(', ') }} → "premium, active" # Access fields with spaces or special characters: {{ $json['field name'] }} # bracket notation required {{ $json['user-id'] }} # hyphen in key name

$node — Data from Any Previous Node

$node gives you access to the output of any named node in your workflow, not just the immediately previous one. This is essential when you need data from several steps back.

# Access data from a node named "Get Customer": {{ $node["Get Customer"].json.name }} {{ $node["Get Customer"].json.email }} # Access data from a node named "HTTP Request": {{ $node["HTTP Request"].json.status }} # Access first item from a multi-item node output: {{ $node["List Orders"].json.orderId }} # defaults to current item index # Access a specific item by index (0-based): {{ $node["List Orders"].json[0].orderId }} # always first item # Check node run status: {{ $node["HTTP Request"].runIndex }} # which run (for looped nodes) {{ $node["HTTP Request"].itemIndex }} # current item index
Node Names Are Case-Sensitive

$node["Get Customer"] and $node["get customer"] are different. Always match the exact node name as it appears on the canvas. Rename your nodes to descriptive names before referencing them — "HTTP Request3" is a poor reference target.

$items — All Items from a Node

$items() returns the complete array of all items output by a node — not just the current item. Use this when you need to aggregate or compare across all items.

# Get all items from the immediately previous node: {{ $items() }} → Array of all items {{ $items().length }} → Total count of items # Get all items from a specific named node: {{ $items("Get Products") }} {{ $items("Get Products").length }} # Access a specific item by index: {{ $items("Get Products")[0].json.name }} # first product name {{ $items("Get Products")[2].json.price }} # third product price # Sum a field across all items (in Code node or expression): {{ $items("Get Orders").reduce((sum, item) => sum + item.json.total, 0) }} # Get all email addresses as an array: {{ $items("Get Contacts").map(item => item.json.email) }} # Find first item matching a condition: {{ $items("Products").find(i => i.json.id === $json.productId)?.json.name }}

Built-in Variables: $now, $today, $workflow, $execution

n8n provides a set of built-in variables you can use in any expression without referencing a node.

Date and time variables

VariableDescriptionExample Output
$nowCurrent datetime as a Luxon DateTime object2026-03-13T08:00:00.000Z
$todayStart of today (midnight) as Luxon DateTime2026-03-13T00:00:00.000Z
$now.toISO()ISO 8601 string"2026-03-13T08:00:00.000Z"
$now.toISODate()Date only string"2026-03-13"
$now.toFormat('dd/MM/yyyy')Custom format string"13/03/2026"
$now.toFormat('h:mm a')Time with AM/PM"8:00 AM"
$now.plus({ days: 7 }).toISODate()7 days from now"2026-03-20"
$now.minus({ hours: 24 }).toISO()24 hours agoyesterday's datetime
$now.weekdayDay of week (1=Mon, 7=Sun)5
$now.monthMonth number3
$now.yearYear2026
$now.toMillis()Unix timestamp in ms1741863600000

Workflow and execution variables

VariableDescriptionExample Output
$workflow.idUnique workflow ID"abc123"
$workflow.nameWorkflow name"Morning Weather Alert"
$workflow.activeWhether workflow is activatedtrue
$execution.idUnique ID of current execution"ex_xyz789"
$execution.mode"manual" or "trigger""trigger"
$execution.resumeUrlURL to resume a waiting executionhttps://...
$itemIndexIndex of the current item being processed0
$runIndexCurrent run index (for retried nodes)0

JavaScript in Expressions

Everything inside {{ }} is JavaScript. You can use the full JavaScript standard library — string methods, array operations, math, regex, Date, JSON, and more.

String operations

# Uppercase / lowercase {{ $json.name.toUpperCase() }} → "ALICE JOHNSON" {{ $json.name.toLowerCase() }} → "alice johnson" # Trim whitespace {{ $json.email.trim() }} # Extract substring {{ $json.sku.slice(0, 5) }} # first 5 characters {{ $json.description.slice(0, 100) + '...' }} # truncate to 100 chars # Split and rejoin {{ $json.fullName.split(' ')[0] }} # extract first name {{ $json.tags.split(',').map(t => t.trim()) }} # clean split # Replace and regex {{ $json.phone.replace(/[^0-9]/g, '') }} # strip non-digits {{ $json.url.replace('http://', 'https://') }} # Template literal (alternative to mixed expressions) {{ `Hello ${$json.name}, you have ${$json.unread} unread messages.` }} # Pad a number with leading zeros {{ String($json.id).padStart(6, '0') }} → "000042" # Check if string contains a value {{ $json.email.includes('@gmail.com') }} → true/false # Extract domain from email {{ $json.email.split('@')[1] }} → "example.com"

Number and math operations

# Arithmetic {{ $json.price * 1.2 }} # add 20% tax {{ ($json.subtotal + $json.tax).toFixed(2) }} # 2 decimal places # Math functions {{ Math.round($json.score) }} {{ Math.min($json.quantity, 100) }} # cap at 100 {{ Math.max(0, $json.balance) }} # floor at 0 {{ Math.abs($json.difference) }} # absolute value {{ Math.floor($json.rating) }} # round down {{ Math.ceil($json.pages) }} # round up {{ (($json.correct / $json.total) * 100).toFixed(1) + '%' }} # Parse string to number {{ parseInt($json.ageString, 10) }} {{ parseFloat($json.priceString) }} {{ Number($json.valueString) }} # Random number (use in Code node for reproducibility) {{ Math.floor(Math.random() * 100) }}

Array and object operations

# Check if value exists in array {{ $json.roles.includes('admin') }} → true/false # Filter array (returns new array) {{ $json.orders.filter(o => o.status === 'pending') }} # Map — transform each element {{ $json.products.map(p => p.name) }} # extract names # Reduce — aggregate to single value {{ $json.items.reduce((sum, i) => sum + i.price, 0) }} # Sort array of objects {{ $json.users.sort((a, b) => a.name.localeCompare(b.name)) }} # Get unique values {{ [...new Set($json.categories)] }} # Object manipulation {{ Object.keys($json.settings) }} # array of keys {{ Object.values($json.settings) }} # array of values {{ JSON.stringify($json.metadata) }} # convert to JSON string {{ JSON.parse($json.configString) }} # parse JSON string

Conditional Expressions

Use JavaScript ternary operators and nullish coalescing for conditional logic directly in expressions:

# Ternary: condition ? valueIfTrue : valueIfFalse {{ $json.score >= 90 ? 'A' : $json.score >= 80 ? 'B' : 'C' }} # Nullish coalescing: use default if value is null/undefined {{ $json.nickname ?? $json.firstName }} {{ $json.city ?? 'Unknown City' }} # Optional chaining: safe access of nested properties {{ $json.user?.address?.city ?? 'No city' }} # Logical OR: use default if value is falsy (0, '', null, undefined) {{ $json.displayName || $json.email }} # Logical AND: only use right side if left is truthy {{ $json.isVip && 'VIP' }} → 'VIP' or false # Checking for empty array {{ $json.items.length === 0 ? 'No items' : `${$json.items.length} items` }} # Multi-condition with logical operators {{ $json.age >= 18 && $json.country === 'US' ? 'Eligible' : 'Not eligible' }}

Environment Variables in n8n

For configuration values that shouldn't be hardcoded in workflows (API keys, base URLs, feature flags), use n8n's built-in variable system.

n8n Variables (Settings → Variables)

Added in n8n 0.225, this is the recommended way to store reusable values without environment access:

# Set in n8n UI: Settings → Variables → Add Variable Name: API_BASE_URL Value: https://api.yourapp.com/v2 # Use in any expression: {{ $vars.API_BASE_URL }} → "https://api.yourapp.com/v2" {{ $vars.API_BASE_URL + '/users' }} → "https://api.yourapp.com/v2/users" {{ $vars.SUPPORT_EMAIL }} {{ $vars.MAX_RETRY_COUNT }}

System environment variables (self-hosted only)

# Access OS environment variables (must be allowlisted in n8n config) # Add to n8n environment: N8N_CUSTOM_ENV_VARS=MY_SECRET,MY_BASE_URL {{ $env.MY_SECRET }} # environment variable {{ $env.MY_BASE_URL }} # Note: $env is NOT available on n8n Cloud for security reasons # Use $vars (n8n Variables) instead — works on both Cloud and self-hosted

Static Data: Persisting Values Between Executions

n8n provides a per-workflow key-value store called static data. Unlike regular variables, values stored here persist between executions — useful for tracking state, counters, or last-run timestamps.

# Static data is read/written in Code nodes only (not expressions) # Get the workflow's static data object const staticData = $getWorkflowStaticData('global'); # Read a value const lastRunTime = staticData.lastRunAt || 'never'; # Write a value (persists after execution ends) staticData.lastRunAt = new Date().toISOString(); staticData.processedCount = (staticData.processedCount || 0) + 1; # Use case: only process records created after last run const cutoff = staticData.lastRunAt || '2000-01-01'; staticData.lastRunAt = new Date().toISOString(); return items.filter(i => i.json.createdAt > cutoff);

Common Expression Patterns

These patterns solve the most frequent expression use cases in real workflows:

Date formatting for different APIs

# ISO 8601 (most REST APIs) {{ $now.toISO() }} → "2026-03-13T08:00:00.000Z" # Unix timestamp in seconds (Stripe, Twilio) {{ Math.floor($now.toMillis() / 1000) }} → 1741863600 # Human-readable for emails {{ $now.toFormat('MMMM d, yyyy') }} → "March 13, 2026" # Parse an incoming date string {{ DateTime.fromISO($json.createdAt).toFormat('dd/MM/yyyy') }} # Calculate days between two dates {{ DateTime.fromISO($json.dueDate).diff(DateTime.now(), 'days').days }} # Add business days (approximation) {{ $now.plus({ days: 5 }).toISODate() }} # 5 calendar days from now

Building dynamic URLs and query strings

# Dynamic URL path https://api.example.com/users/{{ $json.userId }}/orders # URL with query parameters built in expression {{ `https://api.example.com/search?q=${encodeURIComponent($json.query)}&page=${$json.page}&limit=50` }} # Conditional URL based on environment variable {{ $vars.ENV === 'production' ? 'https://api.example.com' : 'https://staging.example.com' }}

Handling null and undefined safely

# Safe nested access (won't throw if intermediate is null) {{ $json.user?.profile?.bio ?? 'No bio provided' }} # Convert null to empty string (for text fields) {{ $json.middleName ?? '' }} # Convert undefined array to empty array before .map() {{ ($json.items ?? []).map(i => i.name) }} # Safely parse a number that might be a string or null {{ $json.amount != null ? parseFloat($json.amount) : 0 }}

Using the Expression Editor Effectively

The Expression Editor (accessible via the ⚡ bolt icon on any field) is your best friend for building complex expressions. Key features:

  • Live preview: Shows the resolved value of your expression using actual data from the last execution or pinned data. If the preview shows [undefined], the path is wrong.
  • Autocomplete: Type $ to see all available variables. Type $json. to see all available fields from your current data.
  • Syntax highlighting: Makes complex expressions readable.
  • Error messages: JavaScript syntax errors are shown inline.
  • Data explorer: Browse incoming data structure in the left panel while building the expression.

Common Mistakes and How to Fix Them

1. "[undefined]" in output

Problem: {{ $json.user.name }} → "[undefined]" Causes and fixes: A) The field name is wrong — check the actual JSON keys (they're case-sensitive) Fix: {{ $json.userName }} or {{ $json.user_name }} B) The data structure is nested differently than expected Fix: Use the Expression Editor data explorer to inspect the actual structure C) You're referencing a field that doesn't exist in all items Fix: {{ $json.user?.name ?? 'Unknown' }}

2. Expression not evaluating (showing literal text)

Problem: Output shows "{{ $json.name }}" as literal text Cause: The field is in "fixed" mode, not "expression" mode Fix: Click the bolt icon ⚡ next to the field to switch to expression mode You'll see the field background change to indicate expression mode

3. TypeError on null access

Problem: Cannot read properties of null (reading 'name') Cause: {{ $json.user.name }} where $json.user is null Fix: Use optional chaining: {{ $json.user?.name }} Or with a fallback: {{ $json.user?.name ?? 'Anonymous' }}

4. Wrong data from $node reference

Problem: $node["My Node"].json is empty or wrong Common causes: - The node name changed (rename on canvas != update in expression) - The referenced node has multiple outputs and you need a specific one - The node didn't execute in this workflow run (e.g., it was on an IF branch not taken) Fix: Always check the exact node name on the canvas and update expressions after renaming

Frequently Asked Questions

Can I use async/await in n8n expressions?
No. Expressions must be synchronous. For async operations (API calls, database queries, etc.), use the HTTP Request node, the Code node (which supports async functions), or a dedicated integration node. Expressions are evaluated inline during node execution and cannot perform I/O.
What's the difference between $json and $input.item.json?
$json is a shorthand for $input.item.json — they refer to the same thing: the JSON data of the current item coming from the directly connected input node. Use $json for brevity. Use $input.item.json when you want to be explicit or when $json is ambiguous in a complex nested context.
How do I use DateTime in expressions — it's not available?
DateTime (from Luxon) is available in n8n expressions but only when accessed through $now, $today, or by constructing via DateTime.fromISO(). You do NOT need to import Luxon — it's pre-loaded. If DateTime is undefined in your expression, try $now.constructor.fromISO(dateString) as an alternative.
Can I define a reusable function and call it across multiple nodes?
Not directly in expressions. For reusable logic, create a dedicated Code node with your function, pass data through it, and then reference its output in subsequent nodes. Alternatively, use n8n Variables to store configuration, or create a sub-workflow that encapsulates the logic.
Why does my expression work in the Expression Editor preview but fail during execution?
The preview uses pinned data or the last execution's data, which may differ from live data. Common cause: the live data has a different structure than the sample. Test with real data by running the workflow manually without pinned data. Also check that all referenced nodes actually executed before the failing node.
How do I access HTTP response headers in n8n?
Enable "Include Response Headers" in the HTTP Request node's options. The headers become available as $json.headers['content-type'] (object with header names as keys). Note: response headers are normalized to lowercase in n8n regardless of what the server sent.