本文へ移動
cccskills
無料GitHub で公開

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.

インストール方法を見る

含まれるファイル(4)

  • SKILL.md20.4 KB
  • COMMON_MISTAKES.md12.1 KB
  • EXAMPLES.md8.1 KB
  • README.md2.6 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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.
  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:

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 [], 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.

❌ $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 match

4. 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

MistakeFix
$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": "john@example.com",
    "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:30

Data 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 || 'no-email@example.com'}}

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 an n8n 2.x instance, an elaborate expression (sqrt, split, reduce, arithmetic) costs the same per item as a trivial {{ $json.x > 50 }} — roughly ~0.2 ms/item either way, because ~90% of that is n8n building the per-item evaluation context, not running your expression.

What this means in practice:

  • Don't break a working expression into a chain of nodes for "speed." Each extra node re-evaluates per item and re-copies all items; one node with one richer expression beats three nodes with simple ones.
  • An expression (~0.2 ms/item) is ~3× cheaper than a Code node in "Run Once for Each Item" mode (~0.6 ms/item) for the same per-item check — but a Code node in "Run Once for All Items" mode is cheaper still (~0.02 ms/item), because it crosses the per-item boundary once instead of N times.
  • This only bites at thousands of items; below that it's sub-100 ms. The n8n Code JavaScript skill has the full per-item-boundary 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. null where you expected data is the symptom. validate_workflow and 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. $jmespath given a non-object argument).

Test in Expression Editor

  1. Click field with expression
  2. Open expression editor (click "fx" icon)
  3. See live preview of result
  4. 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 .body
  • Use $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:

  1. Wrap expressions in {{ }}
  2. Webhook data is under .body
  3. No {{ }} in Code nodes
  4. Quote node names with spaces
  5. Node names are case-sensitive
  6. Runtime errors inside {{ }} become null silently. Check output values after a test run
  7. $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:


Need Help? Reference the n8n expression documentation or use n8n-mcp validation tools to check your expressions.

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Design n8n AI agents the right way. Use when building or editing any @n8n/n8n-nodes-langchain.* AI node — an AI Agent, LLM chain, Text Classifier, or Information Extractor — and whenever the user mentions AI agents, LLM with tools, tool calling, $fromAI, system prompts, agent memory, sessionId, structured/JSON output, output parser, RAG, vector store, a chat assistant/bot, or human-in-the-loop review. Covers Agent-vs-chain-vs-classifier choice, the model/memory/tools/outputParser slots, tool names/descriptions as prompt, structured output with autoFix, memory, RAG, human review, and chat topologies.

日本語の概要は準備中です。原文の説明を表示しています。

czlonkowski/n8n-skills6,4062026年10月9日 更新

Handle files and binary data in n8n correctly. Use when working with files, images, PDFs, attachments, uploads or downloads, base64, vision/multimodal input, or when an AI agent needs a file as tool input or output — and whenever the user mentions $binary, binaryPropertyName, "read the PDF", "attach the file", "send the image", Merge losing binary, or a CDN for chat images. Covers the $binary vs $json split, reading/writing binary, keeping binary alive across transforms with Merge, the agent-tool binary boundary, and the CDN/URL requirement for chat surfaces.

日本語の概要は準備中です。原文の説明を表示しています。

czlonkowski/n8n-skills6,4062026年10月9日 更新

Write JavaScript code in n8n Code nodes. Use when writing JavaScript in n8n, using $input/$json/$node syntax, making HTTP requests with this.helpers / the $helpers global, working with dates using DateTime, troubleshooting Code node errors, choosing between Code node modes, or doing any custom data transformation in n8n. Always use this skill when a workflow needs a Code node — whether for data aggregation, filtering, API calls, format conversion, batch processing logic, or any custom JavaScript. Covers SplitInBatches loop patterns, cross-iteration data, pairedItem, and real-world production patterns. Also use when asked why a Code node or workflow is slow, which execution mode is faster, or how to cut per-item overhead on large datasets. EXCEPTION — for the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode, a tool attached to an AI Agent), use the n8n-code-tool skill instead; it has a different runtime contract.

日本語の概要は準備中です。原文の説明を表示しています。

czlonkowski/n8n-skills6,4062026年10月9日 更新

Write Python in n8n Code nodes (native Python, `language` pythonNative, n8n 2.x). Use when the user explicitly wants Python in a Code node, when migrating old Pyodide/"Python (Beta)" code that used _input/_json/_node/_now, or when a Python Code node fails with NameError, "Security violations detected", "Import of standard library module … is disallowed", "__build_class__ not found", "A 'json' property isn't a dictionary", or "Python runner unavailable". Covers the only two variables (_items/_item), dict-only access, imports blocked by default, the sandbox's denied builtins, accepted return shapes, and how errors interact with onError. JavaScript is the default for Code nodes — native Python has no n8n helpers and, by default, no imports. EXCEPTION — for Python in the AI-agent-callable Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode), use the n8n-code-tool skill instead (input is _query, return must be a string).

日本語の概要は準備中です。原文の説明を表示しています。

czlonkowski/n8n-skills6,4062026年10月9日 更新

Write JavaScript or Python for the n8n Custom Code Tool (@n8n/n8n-nodes-langchain.toolCode) — the AI-agent-callable tool, NOT the workflow Code node. Use when building a Code Tool attached to an AI Agent, writing code that an LLM will invoke, parsing the `query` input, returning a string result, defining an input schema for structured arguments (specifyInputSchema, jsonSchemaExample, DynamicStructuredTool), or troubleshooting errors like "Wrong output type returned", "No execution data available", "The response property should be a string, but it is an object", "Cannot assign to read only property 'name'", or an AI agent that refuses to call the tool. Covers the critical differences between Code node and Code Tool: return format (string vs `[{json:{...}}]`), unavailability of `$fromAI`/`$input`/`$helpers` in the Code Tool sandbox, naming rules for AI invocation, and when to use `toolWorkflow`/HTTP Request Tool instead.

日本語の概要は準備中です。原文の説明を表示しています。

czlonkowski/n8n-skills6,4062026年10月9日 更新

Wire n8n error handling so failures are loud, structured, and recoverable. Use when building any webhook/API workflow, a scheduled or unattended workflow, or any path where a silent failure would drop user-visible work — and whenever the user mentions error handling, onError, continueErrorOutput, error branches/outputs, retries, retryOnFail, Respond to Webhook status codes, 4xx/5xx, Error Trigger, or "my workflow fails silently". Covers per-node error outputs and wiring, retry/self-healing, error-trigger workflows, and 4xx/5xx response shapes.

日本語の概要は準備中です。原文の説明を表示しています。

czlonkowski/n8n-skills6,4062026年10月9日 更新

czlonkowski のスキルをすべて見る

このスキルの問題を報告する