К основному содержанию
скилл Claude Code CX Codex Cursor GitHub Copilot

n8n-expression-syntax — скилл для Claude Code: проверка выражений

Скилл помогает писать и разбирать выражения n8n: от доступа к данным узлов до частых ошибок с вебхуками. Пригодится при настройке полей рабочих процессов.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add czlonkowski/n8n-skills --skill n8n-expression-syntax -a claude-code
Открыть на GitHub

Что делает n8n-expression-syntax

Скилл объясняет синтаксис {{...}} и работу с $json, $node, $now и $env. Помогает найти ошибку в выражении, обратиться к вложенным полям или данным предыдущего узла. Для вебхука важно учитывать структуру: данные запроса обычно находятся в body, а имена узлов в $node нужно указывать в кавычках с учётом регистра.

Есть рекомендации, когда вместо выражения выбрать штатный узел n8n, а когда — Code node. Для Code node нужны прямые обращения JavaScript, не обёртки {{...}}; в полях учётных данных скилл советует использовать систему credentials. Доступ к $env может блокировать настройка N8N_BLOCK_ENV_ACCESS_IN_NODE, поэтому это повод проверить конфигурацию своего экземпляра n8n.

Какие задачи решает

  • исправить выражение с одинарными или пропущенными фигурными скобками
  • получить поле из вложенного JSON или массива
  • обратиться к данным предыдущего узла по его имени
  • найти данные запроса внутри body вебхука
  • выбрать между выражением, штатным узлом и Code node

Примеры запросов агенту

  • Проверь выражение `{{$json.email}}` и объясни, почему поле не подставляется.
  • Исправь обращение к email из узла `Webhook` в моём рабочем процессе.
  • Подскажи, где взять имя из тела запроса вебхука и как сослаться на него в поле n8n.

Как установить n8n-expression-syntax

1Через Skills CLI (Vercel)

npx skills add czlonkowski/n8n-skills --skill n8n-expression-syntax -a claude-code

2Вручную: положить файлы

~/.claude/skills/n8n-expression-syntax/SKILL.md

Папка скилла целиком; для одного проекта — .claude/skills/n8n-expression-syntax/

Из того же набора9

Весь набор →
n8n-workflow-patterns czlonkowski Даёт шесть архитектурных паттернов для n8n-воркфлоу: webhook, API, база данных, AI-агент, расписание, пакетная обработка. Подсказывает, какой паттерн выбрать и какие ноды нужны. Claude Code CX Codex Cursor +1 11 700установок n8n-node-configuration czlonkowski Помогает настраивать узлы n8n с учётом операций, зависимых полей и доступных параметров. Пригодится при сборке и проверке workflow. Claude Code CX Codex Cursor +1 7 600установок n8n-code-javascript czlonkowski Помогает писать и отлаживать JavaScript в нодах Code для n8n, выбирать правильный режим выполнения и заменять цепочки промежуточных узлов. Claude Code CX Codex Cursor +1 7 100установок n8n-validation-expert czlonkowski Разбирает ошибки и предупреждения валидации n8n, объясняет, какие из них критичны, а какие ложные срабатывания. Помогает понять, почему workflow не проходит проверку, и как исправить конфигурацию узла. Claude Code CX Codex Cursor +1 6 800установок n8n-code-python czlonkowski Учит агента писать нативный Python в Code-нодах n8n 2.x: только _items и _item, без импортов по умолчанию, без старых хелперов _input/_json. Claude Code CX Codex Cursor +1 4 800установок n8n-binary-and-data czlonkowski Учит агента не терять файлы в n8n: байты лежат в $binary, а не в $json, и через Code-ноду их надо перекладывать явно. Пригодится, когда PDF или картинка исчезает по пути между нодами. Claude Code CX Codex Cursor +1 1 900установок n8n-subworkflows czlonkowski Скилл учит агента проектировать модульные подпроцессы в n8n через ноду Execute Workflow. Помогает выносить общую логику и превращать сценарии в инструменты для ИИ. Claude Code CX Codex Cursor +1 1 900установок n8n-agents czlonkowski Помогает агенту собирать AI-узлы n8n: выбрать Agent, chain или classifier, подключить память и инструменты, довести структурированный вывод до рабочего состояния. Claude Code CX Codex Cursor +1 1 900установок

Похожие

linear-cli schpet Скилл добавляет агенту команды linear CLI для создания, поиска и обновления задач в Linear прямо из терминала. Claude Code CX Codex Cursor +1 14 100установок video-understand calesthio Скилл извлекает кадры из видео и расшифровывает аудио локально через ffmpeg и Whisper. Подходит для анализа роликов без API-ключей. Claude Code CX Codex Cursor +1 1 300установок minimax-xlsx MiniMax-AI Скилл для создания, чтения, редактирования и проверки Excel-файлов (.xlsx, .xlsm, .csv, .tsv) прямо в коде. Пригодится, если нужно обработать табличные данные, построить финмодель или исправить формулы без потери форматирования. Claude Code CX Codex Cursor +1 3 800установок ffmpeg digitalsamba Готовые команды FFmpeg для конвертации, сжатия, обрезки видео и подготовки ассетов к проектам на Remotion. Claude Code CX Codex Cursor +1 11 600установок notion-api intellectronica Скилл для взаимодействия с Notion API через REST-запросы. Даёт агенту полный доступ к страницам, базам данных, блокам и комментариям — читать, создавать, обновлять и удалять контент. Claude Code CX Codex Cursor +1 94 200установок remotion-render официальный Remotion Экспортирует видео и отдельные кадры из проектов Remotion через консольные команды. CX Codex GP ChatGPT 3установок ego-browser citrolabs Скилл подключает Chromium-браузер ego-lite к ИИ-агенту для автоматизации сайтов, тестирования интерфейсов и сбора данных с сохранением авторизованных сессий пользователя. Claude Code CX Codex Cursor +1 11 900установок composio ComposioHQ Скилл выбирает подход Composio для личного агента или продукта с пользовательскими аккаунтами и помогает настроить интеграцию, выполнить задачу или найти причину сбоя. Claude Code CX Codex Cursor +1 8 000установок

Вопросы о n8n-expression-syntax

Как установить n8n-expression-syntax в Claude Code?

Выполните npx skills add czlonkowski/n8n-skills --skill n8n-expression-syntax -a claude-code.

Что делает n8n-expression-syntax?

Скилл помогает составлять и проверять выражения n8n, обращаться к данным узлов и разбирать типичные ошибки в выражениях.

Как обратиться к данным вебхука?

Входящие данные вебхука находятся в body, например {{$json.body.email}}. Обращение к $json.email в таком случае указывает не на то поле.

Исходный текст

SKILL.md · MIT
---
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": "[email protected]",
    "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 a node — or write any code — to transform data, walk this order and stop at the first that fits:

1. **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.
2. **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.
3. **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.
4. **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

```
{{ $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.

## 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 |

- `diffTo` takes a DateTime, a JS Date or an ISO string, and an optional unit (default `days`) 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 `null` or `undefined` throws: 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.

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

```javascript
❌ $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**:

```javascript
❌ {{$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**:

```javascript
❌ {{$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:

```javascript
❌ {{{$json.field}}}
✅ {{$json.field}}
```

---

## Common Mistakes

For complete error catalog with fixes, see [COMMON_MISTAKES.md](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](EXAMPLES.md)

### Example 1: Webhook to Slack

**Webhook receives**:
```json
{
  "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**:
```json
{
  "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

```javascript
// 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

```javascript
// First item
{{$json.users[0].email}}

// Array length
{{$json.users.length}}

// Last item
{{$json.users[$json.users.length - 1].name}}
```

### Objects

```javascript
// Dot notation (no spaces)
{{$json.user.email}}

// Bracket notation (with spaces or dynamic)
{{$json['user data'].email}}
```

### Strings

```javascript
// Concatenation (automatic)
Hello {{$json.name}}!

// String methods
{{$json.email.toLowerCase()}}
{{$json.name.toUpperCase()}}
```

### Numbers

```javascript
// Direct use
{{$json.price}}

// Math operations
{{$json.price * 1.1}}  // Add 10%
{{$json.quantity + 5}}
```

---

## Advanced Patterns

### Conditional Content

```javascript
// Ternary operator
{{$json.status === 'active' ? 'Active User' : 'Inactive User'}}

// Default values
{{$json.email || '[email protected]'}}
```

### Date Manipulation

```javascript
// 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

```javascript
// 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 `ExpressionError`s 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:
- [COMMON_MISTAKES.md](COMMON_MISTAKES.md) - Complete error catalog
- [EXAMPLES.md](EXAMPLES.md) - Real workflow examples

---

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

© czlonkowski · MIT · czlonkowski/n8n-skills

Смотрите также

Спонсоры проекта

Компании, которые помогают держать аналитику открытой