n8n-expression-syntax
Supports n8n expression syntax, data mapping, and troubleshooting for dynamic workflows.
Install
mkdir -p .claude/skills/n8n-expression-syntax && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/363" && unzip -o skill.zip -d .claude/skills/n8n-expression-syntax && rm skill.zipInstalls to .claude/skills/n8n-expression-syntax
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
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.Key capabilities
- →Validate {{}} expression syntax
- →Map $json data between workflow nodes
- →Debug $node references in node configurations
- →Access current date/time via $now variable
- →Extract environment variables using $env
How it works
Checks inputs against the required double-curly-brace format. It applies rules for node name access and variable types like $json and $node.
Inputs & outputs
When to use n8n-expression-syntax
- →Fix n8n expression syntax errors
- →Correctly map data from previous nodes
- →Debug webhook data references
About this skill
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 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:
-
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.
- Querying nested JSON (filter an array, pick fields, sum, sort, flatten) →
-
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) plusconst/let,if/switch,try/catch, and regex. Norequire, noawait. -
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 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.
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: 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 consum
Content truncated.
When not to use it
- →When configuring nodes using only static UI inputs
- →When writing native JavaScript outside of the n8n expression context
Limitations
- →Does not execute the workflow, only validates syntax
- →Cannot resolve issues caused by missing credentials
How it compares
It detects common pitfalls like missing quotes in node references that a standard code linter would ignore.
Compared to similar skills
n8n-expression-syntax side by side with the closest alternatives in the catalog.
| Skill | Installs | Updated | Safety | Difficulty |
|---|---|---|---|---|
| n8n-expression-syntax (this skill) | 6 | 5mo | No flags | Beginner |
| customerio-advanced-troubleshooting | 0 | 2mo | Caution | Advanced |
| telegram-bot-builder | 106 | 8mo | Review | Intermediate |
| reddit-api | 3 | 5mo | Review | Intermediate |
Try saying
Example prompts that trigger this skill in your AI assistant.
More by czlonkowski
View all by czlonkowski →You might also like
customerio-advanced-troubleshooting
jeremylongshore
Apply Customer.io advanced debugging techniques. Use when diagnosing complex issues, investigating delivery problems, or debugging integration failures. Trigger with phrases like "debug customer.io", "customer.io investigation", "customer.io troubleshoot", "customer.io incident".
telegram-bot-builder
davila7
Expert in building Telegram bots that solve real problems - from simple automation to complex AI-powered bots. Covers bot architecture, the Telegram Bot API, user experience, monetization strategies, and scaling bots to thousands of users. Use when: telegram bot, bot api, telegram automation, chat bot telegram, tg bot.
reddit-api
alinaqi
Reddit API with PRAW (Python) and Snoowrap (Node.js)
mcporter
openclaw
Use the mcporter CLI to list, configure, auth, and call MCP servers/tools directly (HTTP or stdio), including ad-hoc servers, config edits, and CLI/type generation.
calcom-api
calcom
Interact with the Cal.com API v2 to manage scheduling, bookings, event types, availability, and calendars. Use this skill when building integrations that need to create or manage bookings, check availability, configure event types, or sync calendars with Cal.com's scheduling infrastructure.
instantly-webhooks-events
jeremylongshore
Implement Instantly webhook signature validation and event handling. Use when setting up webhook endpoints, implementing signature verification, or handling Instantly event notifications securely. Trigger with phrases like "instantly webhook", "instantly events", "instantly webhook signature", "handle instantly events", "instantly notifications".