czlonkowski/n8n-skills/n8n-expression-syntax
- community
Checking that the SKILL.md file is still in the repository.
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.
Install
npx vskill@latest install czlonkowski/n8n-skills/n8n-expression-syntaxSKILL.md
n8n Expression Syntax
Expert guide for writing correct n8n expressions in workflows.
Expression Format
All dynamic content in n8n uses double curly braces:
Examples:
Core Variables
$json - Current Node Output
Access data from the current node:
$node - Reference Other Nodes
Access data from any previous node:
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:
$env - Environment Variables
Access environment variables:
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
Correct Webhook Data Access
Why: Webhook node wraps incoming data under .body property to preserve headers, params, and query parameters.
Common Patterns
Access Nested Fields
Reference Other Nodes
Combine Variables
When NOT to Use Expressions
❌ Code Nodes
Code nodes use direct JavaScript access, NOT expressions!
❌ Webhook Paths
❌ Credential Fields
The 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-skills maps jobs to nodes; their configs are in n8n-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. See n8n-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
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 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 [], 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 $jmespath in 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
{{ }} returns null silently. 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.
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).
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationPassed, 80 of 100Effective. 1 run. Evaluated 7 months ago.
Source
- SKILL.md in the repositoryCheckingskills/n8n-expression-syntax/SKILL.md
Popularity
- Repository stars
- 3,817
- Repository forks
- 663
- 7-day trend score
- 41
Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 22 Mar 2026, 7 months ago.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/czlonkowski/n8n-skills/n8n-expression-syntax)