N8n Expression Syntax
Romuald CzlonkowskiCommunityCreated by Romuald Czlonkowski
- Skill
- Tool integration
- Automation and integrations
- MIT
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a comprehensive guide to writing, validating, and troubleshooting expressions in n8n. Expressions are the primary mechanism for passing data between nodes using the {{}} syntax. The guide covers core variables like $json, $node, $now, and $env, explains the critical data structure of Webhook nodes, and provides best practices for handling nested JSON, date manipulation, and conditional logic. It also outlines the 'transform gatekeeper' to help users decide when to use a native node, a short expression, or a Code node, ensuring workflows remain readable and maintainable.
What it does
- Explains the {{}} syntax for dynamic content in n8n fields.
- Details how to access data from current and previous nodes using $json and $node.
- Provides rules for referencing webhook data, which is nested under the .body property.
- Offers guidance on using $jmespath for complex JSON querying within a single expression.
- Lists built-in helper methods for strings, arrays, dates, and numbers.
- Defines the hierarchy of data transformation: native nodes, expressions, then Code nodes.
When to use it
- When configuring node fields that need to reference data from previous steps.
- When troubleshooting workflow errors caused by incorrect data mapping.
- When you need to perform light data manipulation like formatting dates or concatenating strings.
- When you are unsure if a complex expression is necessary or if a native node is better suited.
- When you need to access environment variables or current timestamps in your workflow.
Advantages
- Reduces workflow errors by enforcing correct syntax and data pathing.
- Improves workflow readability by favoring native nodes and short expressions over complex code.
- Provides clear rules for handling common pitfalls like webhook data structures and node naming.
- Helps maintain performance by clarifying that expression complexity is generally not a bottleneck.
- Standardizes the approach to data transformation across different team members.
Usage examples
- “How do I access the email field from a Webhook node?”
- The agent will explain that webhook data is not at the root and provide the correct syntax: {{ $json.body.email }}.
- “I have a node named 'HTTP Request' with a space. How do I reference it?”
- The agent will provide the correct bracket notation: {{ $node["HTTP Request"].json.data }}.
- “Can I use expressions inside a Code node?”
- The agent will clarify that Code nodes use direct JavaScript and provide the correct syntax, such as const email = $json.email;.
- “How do I filter a list of customers by country and revenue in one step?”
- The agent will demonstrate the use of the $jmespath() helper function with the correct syntax for filtering arrays.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/czlonkowski/n8n-expression-syntax/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip n8n-expression-syntax.zip -d .agents/skills
Documentation by Romuald Czlonkowski
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": "[email protected]",
"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 expressionsThe transform gatekeeper
Before you add a node — or write any code — to transform data, walk this order and stop at the first that fits:
The service's own node does it in its own fields: a CRM node's field slots, the Slack node's Blocks field, a Data Table insert's per-column slots. HTTP Request only when no node covers the operation.
A native node for the job. Filter, IF / Switch, Summarize, Aggregate, Split Out, Merge, Compare Datasets, Remove Duplicates, Sort + Limit, Date & Time, XML / HTML / Extract From File, HTTP Request pagination, Crypto. The job-to-node table in
using-n8n-mcp-skillsmaps jobs to nodes; their configs are inn8n-node-configuration→ NATIVE_NODES.md.A short expression (
{{ ... }}) inside that node's field. Property access, method chains (.map().filter().join()), a ternary, string building, Luxon date math, the helper functions below — "take A, produce B" in one expression, without intermediate variables.Querying nested JSON for one field (pick, filter, sum inside an array) →
$jmespath()in that same expression (rules below). When downstream needs one item per element, use Split Out / Filter / Aggregate instead.
Code node, only for the closed list: loops or recursion no node expresses, state across runs, parsing no node handles, an allowlisted library, heavy all-items work (thousands of items, stated or measured), or one short Code node (~15 lines) replacing 4+ glue nodes. Give it a
Why Code:note. Seen8n-code-javascript.
A function body is code. When the logic needs const, if, return or several statements — an
(() => { ... })() wrapped in {{ }} — it has outgrown an expression. Split it into a native node plus short
expressions, or move it into a Code node with a Why Code: note. A long expression is harder to read than either, and a
runtime error inside it resolves silently to null (see Debugging). The create/update hooks flag function bodies in
non-Code nodes.
Why the order matters. Readability: a native node says on the canvas what it does, and a short expression stays
visible in the field that uses it, instead of hiding in an upstream node someone has to open. Performance rarely decides
it — below a few hundred items none of the costs matter, and the measured model lives in n8n-code-javascript.
$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:
| Write | Not | What 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 > 100000 | parse 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-name | parse 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 sameexpected two argumentserror. 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[], pipe| [0], reshape{name: name, email: contact.email}.It returns a value, not items. Use it where the result feeds one field (message text, HTTP body, IF/Filter condition). When downstream needs one item per match, narrow with
$jmespathin Edit Fields and follow with a single Split Out on that field.Where it exists: expressions and JavaScript Code nodes (same argument order). Not in native Python Code nodes.
Built-in helper functions
n8n adds methods to values inside {{ }}. Together with a native node they replace most one-line Code nodes. They
exist only in expressions, not in Code nodes — inside Code use plain JavaScript (and toFormat() instead of format()).
They see only the current item's values: jobs across items (dedupe items, group and total, join two lists) belong in a
node. On n8n-mcp 2.97.0+, tools_documentation({topic: "expression_helpers", depth: "full"}) lists every helper with a
tested example.
| Need | Expression | Note |
|---|---|---|
| One field from every object | {{ $json.items.pluck('name') }} | several names → one array per object |
| Sum / average / min / max | {{ $json.prices.sum() }} | throws on non-numbers, numeric strings included |
| Dedupe an array | {{ $json.rows.unique('email') }} | each argument is one field name; none → compares whole values (removeDuplicates is an alias) |
| Default for an empty value | {{ $ifEmpty($json.name, 'there') }} | empty = undefined, null, '', [], {}; exactly two arguments |
| Email or domain in text | {{ $json.text.extractEmail() }}, .extractDomain() | undefined when nothing matches |
| Hash a string | {{ $json.email.hash('sha256') }} | md5 by default; HMAC needs the Crypto node |
| Days between dates | {{ $now.diffTo($json.created_at, 'days') }} | receiver minus argument: positive when the argument is earlier |
| String to date | {{ $json.created_at.toDateTime().format('yyyy-MM-dd') }} | then .diffTo(), .plus(7, 'days'), .extract('month') |
| Split an array into batches | {{ $json.ids.chunk(100) }} | for bulk API bodies |
| Drop empty values | {{ $json.tags.compact() }}, {{ $json.compact() }} | arrays and objects |
diffTotakes a DateTime, a JS Date or an ISO string, and an optional unit (defaultdays) or a list of units (then it returns an object).$json.created_at.toDateTime().diffToNow('days')is negative for past dates..sum()over strings throws:{{ $json.items.pluck('price').map(Number).sum() }}.A helper called on
nullorundefinedthrows: guard with?.({{ $json.email?.extractDomain() }}) or$ifEmpty().A helper that throws (bad unit, wrong argument count, a missing value) fails the node; a plain JavaScript error inside
{{ }}returnsnullsilently. Test with real items either way.
The Set-node antipattern and branch convergence
Delete Set nodes that only re-map fields for the next node
A Set / Edit Fields node whose only job is to copy or rename fields so the next node can read them is dead weight. Inline its expressions at the consumer instead.
❌ Webhook → Set { customer_id: {{ $json.body.customer_id }} } → Postgres: WHERE id = {{ $json.customer_id }}
✅ Webhook → Postgres: WHERE id = {{ $('Webhook').item.json.body.customer_id }}The Set node adds a hop, more canvas clutter, and a refactor hazard, while doing nothing the consumer couldn't do itself. To remove it cleanly with n8n_update_partial_workflow: rewire the connection (removeConnection from the Set's source-and-target, addConnection straight from source to consumer), patchNodeField the consumer's expression to reference the original source by node name, then removeNode the Set.
Quick test: does the Set only re-map fields the next node could read directly ($('Source').item.json.x)?
Yes → delete it, inline the expressions at the consumer.
No, it creates something → keep it as a named step (name it for what it produces).
Edit Fields earns its place when:
It creates a constant item — fixed config, a default payload, a test fixture.
It builds an error or return item —
{ error: 'validation_error', message: … }before a Respond, or a sub-workflow's output.It builds a payload object several nodes, or a whole request body, are built from.
2+ consumers read the same derived value and the derivation is non-trivial (a name aids readability and you compute it once).
It's a sub-workflow's final Return node, shaping the output contract. Here the "single consumer" is every caller, so the Set is the API boundary — and with
Include Other Fields: falseit whitelists the output shape so internal scratch fields don't leak.You're renaming or whitelisting fields and want that visible in one place rather than spread across consumer expressions.
Branch convergence: anchor with a NoOp
When branches converge (after IF/Switch/Merge), $json becomes "whichever branch fired last" — non-deterministic, and a silent source of wrong data. Insert a NoOp node at the convergence, name it descriptively (Combine Inputs), and have downstream nodes reference it by name:
Branch A ──┐
├─→ [NoOp: Combine Inputs] ──→ downstream uses $('Combine Inputs').item.json.x
Branch B ──┘The NoOp survives refactors: inserting a transform later between it and the consumer doesn't break the $('Combine Inputs') reference. (If the branches produce different shapes, use a Set node instead of a NoOp to normalize both into one shape — see the exceptions above.)
More broadly in branchy flows, prefer $('Node').item.json.x over deep $json.x. $json breaks the moment an intermediate node is inserted or a node clears item context (Aggregate, Code with Run for All, branching merges); the failure is silent and downstream gets the wrong data with no error. A node-name reference is unambiguous regardless of what sits between source and consumer.
Validation Rules
1. Always Use {{}}
Expressions must be wrapped in double curly braces.
❌ $json.field
✅ {{$json.field}}2. Use Quotes for Spaces and Special Characters
Field or node names with spaces, diacritics, or special characters require bracket notation:
❌ {{$json.field name}}
✅ {{$json['field name']}}
❌ {{$node.HTTP Request.json}}
✅ {{$node["HTTP Request"].json}}
// Bracket notation is mandatory for keys with special characters
✅ {{$json['Gross Price w/o shipment']}}
✅ {{$json['Cena brutto zł']}}3. Match Exact Node Names
Node references are case-sensitive:
❌ {{$node["http request"].json}} // lowercase
❌ {{$node["Http Request"].json}} // wrong case
✅ {{$node["HTTP Request"].json}} // exact match4. No Nested {{}}
Don't double-wrap expressions:
❌ {{{$json.field}}}
✅ {{$json.field}}Common Mistakes
For complete error catalog with fixes, see COMMON_MISTAKES.md
Quick Fixes
| Mistake | Fix |
|---|---|
$json.field | {{$json.field}} |
{{$json.field name}} | {{$json['field name']}} |
{{$node.HTTP Request}} | {{$node["HTTP Request"]}} |
{{{$json.field}}} | {{$json.field}} |
{{$json.name}} (webhook) | {{$json.body.name}} |
'={{$json.email}}' (Code node) | $json.email |
Working Examples
For real workflow examples, see EXAMPLES.md
Example 1: Webhook to Slack
Webhook receives:
{
"body": {
"name": "John Doe",
"email": "[email protected]",
"message": "Hello!"
}
}In Slack node text field:
New form submission!
Name: {{$json.body.name}}
Email: {{$json.body.email}}
Message: {{$json.body.message}}Example 2: HTTP Request to Email
HTTP Request returns:
{
"data": {
"items": [
{"name": "Product 1", "price": 29.99}
]
}
}In Email node (reference HTTP Request):
Product: {{$node["HTTP Request"].json.data.items[0].name}}
Price: ${{$node["HTTP Request"].json.data.items[0].price}}Example 3: Format Timestamp
// Current date
{{$now.toFormat('yyyy-MM-dd')}}
// Result: 2025-10-20
// Time
{{$now.toFormat('HH:mm:ss')}}
// Result: 14:30:45
// Full datetime
{{$now.toFormat('yyyy-MM-dd HH:mm')}}
// Result: 2025-10-20 14:30Data Type Handling
Arrays
// First item
{{$json.users[0].email}}
// Array length
{{$json.users.length}}
// Last item
{{$json.users[$json.users.length - 1].name}}Objects
// Dot notation (no spaces)
{{$json.user.email}}
// Bracket notation (with spaces or dynamic)
{{$json['user data'].email}}Strings
// Concatenation (automatic)
Hello {{$json.name}}!
// String methods
{{$json.email.toLowerCase()}}
{{$json.name.toUpperCase()}}Numbers
// Direct use
{{$json.price}}
// Math operations
{{$json.price * 1.1}} // Add 10%
{{$json.quantity + 5}}Advanced Patterns
Conditional Content
// Ternary operator
{{$json.status === 'active' ? 'Active User' : 'Inactive User'}}
// Default values
{{$json.email || '[email protected]'}}Date Manipulation
// Add days
{{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}}
// Subtract hours
{{$now.minus({hours: 24}).toISO()}}
// Set specific date
{{DateTime.fromISO('2025-12-25').toFormat('MMMM dd, yyyy')}}String Manipulation
// Substring
{{$json.email.substring(0, 5)}}
// Replace
{{$json.message.replace('old', 'new')}}
// Split and join
{{$json.tags.split(',').join(', ')}}Performance: expression complexity is (almost) free
A common worry is that a complex {{ }} is slow. It isn't — what costs is how many times n8n evaluates an
expression, not how elaborate each one is. Measured on n8n 2.x, an elaborate expression (sqrt, split, reduce)
costs about the same per item as {{ $json.x > 50 }}: most of the cost is n8n building the per-item evaluation context.
What this means in practice:
Don't split a working short expression into extra nodes for speed, and don't grow it into a function body for speed either. Choose by readability: native node, then a short expression, then Code (the gatekeeper above).
Per-item costs only bite at thousands of items, mostly through the number of per-item boundaries (Code in "Each Item" mode is the worst case). The n8n Code JavaScript skill has the measured per-item model.
Debugging Expressions
Runtime errors don't fail the node — they become null
Verified on n8n 2.38 with the default expression runtime: during execution the handler around
each {{ }} re-throws only n8n's own ExpressionErrors and swallows other JavaScript errors, so
the field resolves to empty/null and the node still reports success. $json.missing.field
(TypeError), JSON.parse('{bad'), throw new Error(...) and JMESPath syntax errors all
produced null. In a Filter or IF condition every item then silently fails the check. On other
versions or expression engines the same mistake may fail the node instead. Either way, never
trust a green run on its own.
This isn't a reason to avoid expressions (a Code node has silent traps of its own). It's a reason to test with real items:
Check the expression editor preview against real data. The preview does show the error.
After a test run, look at the output values.
nullwhere you expected data is the symptom.validate_workflowand a green execution won't tell you.To surface the real message, wrap the expression temporarily:
{{ (() => { try { return JSON.stringify(<expr>) } catch (e) { return 'ERROR: ' + e.message } })() }}n8n's own errors still fail the node (e.g.
$jmespathgiven a non-object argument).
Test in Expression Editor
Click field with expression
Open expression editor (click "fx" icon)
See live preview of result
Check for errors highlighted in red
Common Error Messages
These appear in the editor preview; at runtime most of them resolve to null instead (see above).
"Cannot read property 'X' of undefined" → Parent object doesn't exist → Check your data path
"X is not a function" → Trying to call method on non-function → Check variable type
Expression shows as literal text → Missing {{ }} → Add curly braces
Expression Helpers
Available Methods
String:
.toLowerCase(),.toUpperCase().trim(),.replace(),.substring().split(),.includes()
Array:
.length,.map(),.filter().find(),.join(),.slice()
DateTime (Luxon):
.toFormat(),.toISO(),.toLocal().plus(),.minus(),.set()
Number:
.toFixed(),.toString()Math operations:
+,-,*,/,%
JSON query:
$jmespath(object, "query"): filter/pick/aggregate nested JSON (see the$jmespath()section for the quoting rules)
Best Practices
✅ Do
Always use {{ }} for dynamic content
Use bracket notation for field names with spaces
Reference webhook data from
.bodyUse $node for data from other nodes
Test expressions in expression editor
❌ Don't
Don't use expressions in Code nodes
Don't forget quotes around node names with spaces
Don't double-wrap with extra {{ }}
Don't assume webhook data is at root (it's under .body!)
Don't use expressions in webhook paths or credentials
Related Skills
n8n MCP Tools Expert: Learn how to validate expressions using MCP tools
n8n Workflow Patterns: See expressions in real workflow examples
n8n Node Configuration: Understand when expressions are needed
Summary
Essential Rules:
Wrap expressions in {{ }}
Webhook data is under
.bodyNo {{ }} in Code nodes
Quote node names with spaces
Node names are case-sensitive
Runtime errors inside
{{ }}becomenullsilently. Check output values after a test run$jmespath(object, "query"):'string',`number`,"field";json.prefix over.all()
Most Common Mistakes:
Missing {{ }} → Add braces
{{$json.name}}in webhooks → Use{{$json.body.name}}{{$json.email}}in Code → Use$json.email{{$node.HTTP Request}}→ Use{{$node["HTTP Request"]}}
For more details, see:
COMMON_MISTAKES.md - Complete error catalog
EXAMPLES.md - Real workflow examples
Need Help? Reference the n8n expression documentation or use n8n-mcp validation tools to check your expressions.
Files
- ATTRIBUTION.mdAdded by AutoGineer899 B
- COMMON_MISTAKES.md12.1 KiB
- EXAMPLES.md8.3 KiB
- LICENSELicence file added by AutoGineer1.1 KiB
- README.md2.6 KiB
- SKILL.md22.8 KiB
COMMON_MISTAKES.md
Common n8n Expression Mistakes
Complete catalog of expression errors with explanations and fixes.
1. Missing Curly Braces
Problem: Expression not recognized, shows as literal text
❌ Wrong:
$json.email✅ Correct:
{{$json.email}}Why it fails: n8n treats text without {{ }} as a literal string. Expressions must be wrapped to be evaluated.
How to identify: Field shows exact text like "$json.email" instead of actual value.
2. Webhook Body Access
Problem: Undefined values when accessing webhook data
❌ Wrong:
{{$json.name}}
{{$json.email}}
{{$json.message}}✅ Correct:
{{$json.body.name}}
{{$json.body.email}}
{{$json.body.message}}Why it fails: Webhook node wraps incoming data under .body property. The root $json contains headers, params, query, and body.
Webhook structure:
{
"headers": {...},
"params": {...},
"query": {...},
"body": { // User data is HERE!
"name": "John",
"email": "[email protected]"
}
}How to identify: Webhook workflow shows "undefined" for fields that are definitely being sent.
3. Spaces in Field Names
Problem: Syntax error or undefined value
❌ Wrong:
{{$json.first name}}
{{$json.user data.email}}✅ Correct:
{{$json['first name']}}
{{$json['user data'].email}}Why it fails: Spaces break dot notation. JavaScript interprets space as end of property name.
How to identify: Error message about unexpected token, or undefined when field exists.
4. Spaces in Node Names
Problem: Cannot access other node's data
❌ Wrong:
{{$node.HTTP Request.json.data}}
{{$node.Respond to Webhook.json}}✅ Correct:
{{$node["HTTP Request"].json.data}}
{{$node["Respond to Webhook"].json}}Why it fails: Node names are treated as object property names and need quotes when they contain spaces.
How to identify: Error like "Cannot read property 'Request' of undefined"
5. Incorrect Node Reference Case
Problem: Undefined or wrong data returned
❌ Wrong:
{{$node["http request"].json.data}} // lowercase
{{$node["Http Request"].json.data}} // wrong capitalization✅ Correct:
{{$node["HTTP Request"].json.data}} // exact matchWhy it fails: Node names are case-sensitive. Must match exactly as shown in workflow.
How to identify: Undefined value even though node exists and has data.
6. Double Wrapping
Problem: Literal {{ }} appears in output
❌ Wrong:
{{{$json.field}}}✅ Correct:
{{$json.field}}Why it fails: Only one set of {{ }} is needed. Extra braces are treated as literal characters.
How to identify: Output shows "{{value}}" instead of just "value".
7. Array Access with Dots
Problem: Syntax error or undefined
❌ Wrong:
{{$json.items.0.name}}
{{$json.users.1.email}}✅ Correct:
{{$json.items[0].name}}
{{$json.users[1].email}}Why it fails: Array indices require brackets, not dots. Number after dot is invalid JavaScript.
How to identify: Syntax error or "Cannot read property '0' of undefined"
8. Using Expressions in Code Nodes
Problem: Literal string instead of value, or errors
❌ 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();Why it fails: Code nodes have direct access to data. The {{ }} syntax is for expression fields in other nodes, not for JavaScript code.
How to identify: Literal string "{{$json.email}}" appears in Code node output instead of actual value.
9. Missing Quotes in $node Reference
Problem: Syntax error
❌ Wrong:
{{$node[HTTP Request].json.data}}✅ Correct:
{{$node["HTTP Request"].json.data}}Why it fails: Node names must be quoted strings inside brackets.
How to identify: Syntax error "Unexpected identifier"
10. Incorrect Property Path
Problem: Undefined value
❌ Wrong:
{{$json.data.items.name}} // items is an array
{{$json.user.email}} // user doesn't exist, it's userData✅ Correct:
{{$json.data.items[0].name}} // access array element
{{$json.userData.email}} // correct property nameWhy it fails: Wrong path to data. Arrays need index, property names must be exact.
How to identify: Check actual data structure using expression editor preview.
11. Using = Prefix Outside JSON
Problem: Literal "=" appears in output
❌ Wrong (in text field):
Email: ={{$json.email}}✅ Correct (in text field):
Email: {{$json.email}}Note: The = prefix is only needed in JSON mode or when you want to set entire field value to expression result:
// JSON mode (set property to expression)
{
"email": "={{$json.body.email}}"
}
// Text mode (no = needed)
Hello {{$json.body.name}}!Why it fails: The = is parsed as literal text in non-JSON contexts.
How to identify: Output shows "=[email protected]" instead of "[email protected]"
12. Expressions in Webhook Path
Problem: Path doesn't update, validation error
❌ Wrong:
path: "{{$json.user_id}}/webhook"
path: "users/={{$env.TENANT_ID}}"✅ Correct:
path: "my-webhook" // Static paths only
path: "user-webhook/:userId" // Use dynamic URL parameters insteadWhy it fails: Webhook paths must be static. Use dynamic URL parameters (:paramName) instead of expressions.
How to identify: Webhook path doesn't change or validation warns about invalid path.
13. Forgetting .json in $node Reference
Problem: Undefined or wrong data
❌ Wrong:
{{$node["HTTP Request"].data}} // Missing .json
{{$node["Webhook"].body.email}} // Missing .json✅ Correct:
{{$node["HTTP Request"].json.data}}
{{$node["Webhook"].json.body.email}}Why it fails: Node data is always under .json property (or .binary for binary data).
How to identify: Undefined value when you know the node has data.
14. Template Literals & Concatenation Outside {{ }}
Problem: A backtick template literal or + concatenation shows up as literal text
❌ Wrong (written as the whole field value, with no {{ }}):
`Hello ${$json.name}!` // bare backticks — printed verbatim
"Hello " + $json.name + "!" // bare concatenation — printed verbatim✅ Correct (any of these):
Hello {{$json.name}}! // adjacent text + {{ }} auto-concatenate
{{ `Hello ${$json.name}!` }} // a template literal INSIDE {{ }}
{{ "Hello " + $json.name + "!" }} // + concatenation INSIDE {{ }}Why it fails: n8n only evaluates what's inside {{ }}; everything else is literal text. The backticks and + aren't the problem — the missing {{ }} is. Inside an expression, backtick template literals with ${...} interpolation are fully supported modern JavaScript, so this is valid and evaluates:
={{ $json.items.map(i => `${i.name} — ${i.qty}`).join(', ') }}The same holds for optional chaining ({{ $json.user?.email }}) and string-keyed bracket access ({{ $json['some-prop'] }}) — both are valid n8n expressions. As of n8n-mcp ≥ 2.63.0 the validator no longer flags template literals, optional chaining, or bracket access inside expressions (earlier versions raised false-positive errors on them).
How to identify: Literal backticks or + symbols appear in output → the code wasn't wrapped in {{ }}.
15. Empty Expression Brackets
Problem: Literal {{}} in output
❌ Wrong:
{{}}
{{ }}✅ Correct:
{{$json.field}} // Include expression contentWhy it fails: Empty expression brackets have nothing to evaluate.
How to identify: Literal "{{ }}" text appears in output.
16. JMESPath String in Double Quotes
Problem: $jmespath filter returns [] even though matching data exists — no error
❌ Wrong:
{{ $jmespath($json, 'customers[?country=="PL"].name') }} // → []✅ Correct:
{{ $jmespath($json, "customers[?country=='PL'].name") }} // → ["Acme", "Bar"]
{{ $jmespath($json, "customers[?revenue > `100000`].name") }} // numbers in backticksWhy it fails: in JMESPath, "PL" is a quoted field name, not a string. The filter compares country to a field called PL, which doesn't exist, so nothing matches. String literals use single quotes; numbers and booleans use backticks. A bare number (revenue > 100000) or and/= instead of &&/== is a parse error, and the whole expression becomes null (see #17).
Same trap over items: $jmespath($('Node').all(), "[?country=='PL']") also returns []. Items are {json: …} wrappers, so write [?json.country=='PL'].json.name, or map first: $input.all().map(i => i.json).
How to identify: [] or null from a query whose data you can see in the input panel.
17. Runtime Error Hidden as null
Problem: A field comes out null/empty (or a Filter drops every item) while the node and execution show success
❌ Wrong assumption: "It ran green, so the expression works."
Why it happens: verified on n8n 2.38 with the default expression runtime: at runtime n8n swallows JavaScript errors inside {{ }} other than its own ExpressionErrors. $json.missing.field, JSON.parse on bad input, a thrown Error and a JMESPath syntax error all resolved to null instead of failing the node. The editor preview shows the error; the execution doesn't. Other versions or engines may fail the node instead. Either way, never trust a green run on its own.
✅ Fix / check:
{{ (() => { try { return JSON.stringify($json.payload.items.map(i => i.id)) } catch (e) { return 'ERROR: ' + e.message } })() }}Wrap temporarily to see the message, fix the path, then remove the wrapper. Always inspect output values after a test run, and guard optional paths with ?. / ??.
How to identify: unexpected nulls in output; Filter/IF sending everything to the false side.
Quick Reference Table
| Error | Symptom | Fix |
|---|---|---|
| No {{ }} | Literal text | Add {{ }} |
| Webhook data | Undefined | Add .body |
| Space in field | Syntax error | Use ['field name'] |
| Space in node | Undefined | Use ["Node Name"] |
| Wrong case | Undefined | Match exact case |
| Double {{ }} | Literal braces | Remove extra {{ }} |
| .0 array | Syntax error | Use [0] |
| {{ }} in Code | Literal string | Remove {{ }} |
| No quotes in $node | Syntax error | Add quotes |
| Wrong path | Undefined | Check data structure |
| = in text | Literal = | Remove = prefix |
| Dynamic path | Doesn't work | Use static path |
| Missing .json | Undefined | Add .json |
Template literal / + outside {{ }} | Literal text | Wrap in {{ }} (both work inside) |
| Empty {{ }} | Literal braces | Add expression |
JMESPath "PL" in a filter | [], no error | 'PL' for strings, `100` for numbers |
JMESPath over .all() without json. | [] | [?json.field=='x'].json.name |
| Any JS error inside {{ }} at runtime | null, node still succeeds | Check preview + output values; try/catch wrapper to see the message |
Debugging Process
When expression doesn't work:
Check braces: Is it wrapped in {{ }}?
Check data source: Is it webhook data? Add
.bodyCheck spaces: Field or node name has spaces? Use brackets
Check case: Does node name match exactly?
Check path: Is the property path correct?
Use expression editor: Preview shows actual result
At runtime errors become
nullsilently. If output isnull, the preview's error is the real cause.
Check context: Is it a Code node? Remove {{ }}
Related: See EXAMPLES.md for working examples of correct syntax.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs an API key, Needs Node.js
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Romuald Czlonkowski
- Publisher
- Romuald Czlonkowski (Community)
- Source
- czlonkowski/n8n-skills · skills/n8n-expression-syntax · commit e037563
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Romuald Czlonkowski.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download n8n-expression-syntax.zip