Skip to main content

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.

Source facts

Repository
Agents-Store/codex-plugins
Last source activity
October 3, 2026 at 23:16
Detected SKILL.md language
English
Stars
0
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
4 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
n8n-expression-syntax
description
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.
# 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: ```javascript {{$json.fieldName}} {{$json['field with spaces']}} {{$json.nested.property}} {{$json.items[0].name}} ``` ### $node - Reference Other Nodes Access data from any previous node: ```javascript {{$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: ```javascript {{$now}} {{$now.toFormat('yyyy-MM-dd')}} {{$now.toFormat('HH:mm:ss')}} {{$now.plus({days: 7})}} ``` ### $env - Environment Variables Access environment variables: ```javascript {{$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 ```javascript { "headers": {...}, "params": {...}, "query": {...}, "body": { // ⚠️ USER DATA IS HERE! "name": "John", "email": "john@example.com", "message": "Hello" } } ``` ### Correct Webhook Data Access ```javascript ❌ 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 ```javascript // 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 ```javascript // 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 ```javascript // 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! ```javascript // ❌ 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 ```javascript // ❌ WRONG path: "{{$json.user_id}}/webhook" // ✅ CORRECT path: "user-webhook" // Static paths only ``` ### ❌ Credential Fields ```javascript // ❌ 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. 2. **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`. 3. **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**: | 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. ## The Set-node antipattern and branch convergence ### Delete Set nodes that feed one consumer A Set / Edit Fields node whose only job is to extract a value and hand it to **one** downstream node is dead weight. Inline its expression 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:** count how many downstream nodes reference each field the Set produces. - **0 or 1** → delete, inline at the consumer. - **2+** → it may earn its place. **Legitimate exceptions** — keep the Set when: - **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: false` it 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.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub