czlonkowski/n8n-skills4 files

N8n Expression Syntax

Validate n8n expression syntax and fix common errors. Use when writing n8n expressions, using {{}} syntax, accessing $json/$node variables, troubleshooting expression errors, mapping data between nodes, or referencing webhook data in workflows. Use this skill whenever configuring node fields that reference data from previous nodes — expressions are how n8n passes data between nodes, and getting the syntax wrong is the most common source of workflow errors. Also use when asked whether a complex expression hurts performance.

Specification
Skill ID
czlonkowski/n8n-skills/n8n-expression-syntax
Publisher
czlonkowski
Repository
n8n-skills
Installs
0
Files
4
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

czlonkowski/n8n-skills/n8n-expression-syntaxInstalls these files
  • COMMON_MISTAKES.md
  • EXAMPLES.md
  • README.md
  • SKILL.md

What this skill tells the agent

n8n Expression Syntax

Expert guide for writing correct n8n expressions in workflows.


Expression Format

All dynamic content in n8n uses double curly braces:

{{expression}}

Examples:

✅ {{$json.email}}
✅ {{$json.body.name}}
✅ {{$node["HTTP Request"].json.data}}
❌ $json.email  (no braces - treated as literal text)
❌ {$json.email}  (single braces - invalid)

Core Variables

$json - Current Node Output

Access data from the current node:

{{$json.fieldName}}
{{$json['field with spaces']}}
{{$json.nested.property}}
{{$json.items[0].name}}

$node - Reference Other Nodes

Access data from any previous node:

{{$node["Node Name"].json.fieldName}}
{{$node["HTTP Request"].json.data}}
{{$node["Webhook"].json.body.email}}

Important:

  • Node names must be in quotes
  • Node names are case-sensitive
  • Must match exact node name from workflow

$now - Current Timestamp

Access current date/time:

{{$now}}
{{$now.toFormat('yyyy-MM-dd')}}
{{$now.toFormat('HH:mm:ss')}}
{{$now.plus({days: 7})}}

$env - Environment Variables

Access environment variables:

{{$env.API_KEY}}
{{$env.DATABASE_URL}}

Warning: Some n8n instances have N8N_BLOCK_ENV_ACCESS_IN_NODE enabled, which blocks $env access entirely. If $env returns errors, use alternative approaches:

  • Store values in credentials instead
  • Use a Set node with manually entered values
  • Pass values through webhook query parameters

🚨 CRITICAL: Webhook Data Structure

Most Common Mistake: Webhook data is NOT at the root!

Webhook Node Output Structure

{
  "headers": {...},
  "params": {...},
  "query": {...},
  "body": {           // ⚠️ USER DATA IS HERE!
    "name": "John",
    "email": "john@example.com",
    "message": "Hello"
  }
}

Correct Webhook Data Access

❌ WRONG: {{$json.name}}
❌ WRONG: {{$json.email}}

✅ CORRECT: {{$json.body.name}}
✅ CORRECT: {{$json.body.email}}
✅ CORRECT: {{$json.body.message}}

Why: Webhook node wraps incoming data under .body property to preserve headers, params, and query parameters.


Common Patterns

Access Nested Fields

// Simple nesting
{{$json.user.email}}

// Array access
{{$json.data[0].name}}
{{$json.items[0].id}}

// Bracket notation for spaces
{{$json['field name']}}
{{$json['user data']['first name']}}

Reference Other Nodes

// Node without spaces
{{$node["Set"].json.value}}

// Node with spaces (common!)
{{$node["HTTP Request"].json.data}}
{{$node["Respond to Webhook"].json.message}}

// Webhook node
{{$node["Webhook"].json.body.email}}

Combine Variables

// Concatenation (automatic)
Hello {{$json.body.name}}!

// In URLs
https://api.example.com/users/{{$json.body.user_id}}

// In object properties
{
  "name": "={{$json.body.name}}",
  "email": "={{$json.body.email}}"
}

When NOT to Use Expressions

❌ Code Nodes

Code nodes use direct JavaScript access, NOT expressions!

// ❌ WRONG in Code node
const email = '={{$json.email}}';
const name = '{{$json.body.name}}';

// ✅ CORRECT in Code node
const email = $json.email;
const name = $json.body.name;

// Or using Code node API
const email = $input.item.json.email;
const allItems = $input.all();

❌ Webhook Paths

// ❌ WRONG
path: "{{$json.user_id}}/webhook"

// ✅ CORRECT
path: "user-webhook"  // Static paths only

❌ Credential Fields

// ❌ WRONG
apiKey: "={{$env.API_KEY}}"

// ✅ CORRECT
Use n8n credential system, not expressions

The transform gatekeeper

Before you add any node — or write any code — to transform data, walk this order and stop at the first that fits:

  1. Expression ({{ ... }}) in the consuming field. Property access, method chains (.map().filter().join()), ternaries, string building, Luxon date math — if it's "take A, produce B" without intermediate variables, it's an expression. This covers most "just transform this" cases.
  • Querying nested JSON (filter an array, pick fields, sum, sort, flatten) → $jmespath() inside that same expression, before you split into items or chain .map().filter(). One query replaces a Split Out → Filter → Aggregate chain. Rules below.
  1. Arrow-function IIFE inside an Edit Fields field. When the logic needs intermediate variables, branching, or comments but still operates on one item, wrap it in an immediately-invoked arrow function right in the field value:
   ={{ (() => {
       const items = $json.line_items;
       const subtotal = items.reduce((sum, it) => sum + it.price * it.qty, 0);
       const tax = subtotal * 0.08;
       return (subtotal + tax).toFixed(2);
   })() }}

The outer (...) brackets the function; the trailing () invokes it. Drop either and n8n refuses to run. Inside you get the full expression scope ($json, $('Node'), $now, Luxon) plus const/let, if/switch, try/catch, and regex. No require, no await.

  1. Code node — last resort. Only when you need multi-item aggregation across the whole dataset ($input.all()), an allowlisted library, or async work.

Why the order matters. It's not style — it's readability and performance. The Code node runs in a sandboxed VM with per-invocation setup and value marshaling — a cold-start cost that can reach 500–1000ms before your logic runs. (It amortizes on warm, high-item-count runs, so treat this as the common-case cost, not a universal constant.) The same logic in an expression or Edit Fields IIFE runs in-process in single-digit milliseconds and skips the sandbox entirely. For pure single-item shaping that's a large gap with no functional difference, and it compounds on hot paths like per-request webhooks. The expression also stays visible in the field that uses it, instead of hiding in an upstream node someone has to open to understand. Reach past a stage only when the input or scope genuinely demands it.

$jmespath() — query nested JSON in one expression

{{ $jmespath($json, "customers[?country=='PL' && revenue > `100000`].name") }}    →  ["Acme"]

Verified on n8n 2.38: this one expression returns exactly what Split Out → Filter → Aggregate returns, with no extra nodes. The syntax is unforgiving, and most mistakes fail silently:

WriteNotWhat the wrong form does
$jmespath(object, "query"), object first$jmespath("query", object)throws expected two arguments (Object, string) for this function (JMESPath's own docs show search(query, data))
strings in single quotes: country=='PL'country=="PL"double quotes mean a field name → returns [], no error
numbers/booleans in backticks: ` revenue > 100000 `revenue > 100000parse error → whole expression becomes null (see Debugging)
&& `\\ ! ==`and or =parse error → null
hyphenated keys quoted: 'customers[*].contact."first-name"' (single-quote the JS string)contact.first-nameparse error → null
over items, keep the wrapper: $jmespath($('Node').all(), "[?json.country=='PL'].json.name") or $jmespath($input.all().map(i => i.json), "[?country=='PL'].name")"[?country=='PL'].name" on .all()items are {json: …} wrappers → []
  • Results: missing path or index → null; filter with no match → []; sum() over an empty projection → 0.
  • First argument must be an object or array. A string (e.g. an HTTP Request with a text response) or undefined ($json.missingField) throws the same expected two arguments error. That one does fail the node.
  • Handy pieces: length(), sum(), max_by(arr, &field), sort_by(arr, &field), reverse(), contains(), starts_with(), keys(), to_number(); projection [*], flatten

More from czlonkowski

Use this skill in a project

Open any RiverX project, search the Skills panel for czlonkowski/n8n-skills/n8n-expression-syntax, and install it in one click.

Use this skill