mcpField
N
n8n-expression-syntax

n8n-expression-syntax

github.com/czlonkowski开发6.5k6.7k 次安装4 天前更新
代码仓库

关于 n8n-expression-syntax

面向 n8n 工作流表达式编写的指南,讲解动态字段、节点数据引用及内置辅助函数,并提供选择表达式或原生节点的规则。

主要功能

  1. 01使用双花括号编写动态表达式
  2. 02引用当前及其他节点的数据
  3. 03借助 JMESPath 查询嵌套 JSON
  4. 04使用内置数组、字符串和日期辅助函数
  5. 05按任务选择原生节点、表达式或代码

适用场景

  1. 01在字段中插入上游节点数据
  2. 02筛选嵌套 JSON 并生成字段值
  3. 03格式化日期或处理数组内容

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/czlonkowski/n8n-skills --skill n8n-expression-syntax

安装后在 Claude Code 中输入/n8n-expression-syntax确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

COMMON_MISTAKES.md
EXAMPLES.md
README.md

详细介绍

n8n 表达式语法

编写正确 n8n 工作流表达式的专家指南。


表达式格式

n8n 中的所有动态内容都使用双花括号:

{{expression}}

示例:

✅ {{$json.email}}
✅ {{$json.body.name}}
✅ {{$node["HTTP Request"].json.data}}
❌ $json.email  (no braces - treated as literal text)
❌ {$json.email}  (single braces - invalid)

核心变量

$json - 当前节点输出

访问当前节点的数据:

{{$json.fieldName}}
{{$json['field with spaces']}}
{{$json.nested.property}}
{{$json.items[0].name}}
$node - 引用其他节点

访问任意前置节点的数据:

{{$node["Node Name"].json.fieldName}}
{{$node["HTTP Request"].json.data}}
{{$node["Webhook"].json.body.email}}

重要:

  • 节点名称必须加引号
  • 节点名称区分大小写
  • 必须与工作流中的节点名称完全一致
$now - 当前时间戳

访问当前日期/时间:

{{$now}}
{{$now.toFormat('yyyy-MM-dd')}}
{{$now.toFormat('HH:mm:ss')}}
{{$now.plus({days: 7})}}
$env - 环境变量

访问环境变量:

{{$env.API_KEY}}
{{$env.DATABASE_URL}}

警告:某些 n8n 实例启用了 N8N_BLOCK_ENV_ACCESS_IN_NODE,这会完全阻止访问 $env。如果 $env 返回错误,请使用替代方法:

  • 将值存储在凭据中
  • 使用 Set 节点手动输入值
  • 通过 webhook 查询参数传递值

🚨 严重警告:Webhook 数据结构

最常见的错误:Webhook 数据不在根级!

Webhook 节点输出结构
{
  "headers": {...},
  "params": {...},
  "query": {...},
  "body": {           // ⚠️ USER DATA IS HERE!
    "name": "John",
    "email": "[email protected]",
    "message": "Hello"
  }
}
正确访问 Webhook 数据
❌ WRONG: {{$json.name}}
❌ WRONG: {{$json.email}}

✅ CORRECT: {{$json.body.name}}
✅ CORRECT: {{$json.body.email}}
✅ CORRECT: {{$json.body.message}}

原因:Webhook 节点会将传入数据封装在 .body 属性下,以保留标头、参数和查询参数。


常见模式

访问嵌套字段
// 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']}}
引用其他节点
// 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}}
组合变量
// 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}}"
}

不应使用表达式的情况

❌ Code 节点

Code 节点使用直接 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 路径
// ❌ WRONG
path: "{{$json.user_id}}/webhook"

// ✅ CORRECT
path: "user-webhook"  // Static paths only
❌ 凭据字段
// ❌ WRONG
apiKey: "={{$env.API_KEY}}"

// ✅ CORRECT
Use n8n credential system, not expressions

转换把关流程

在添加节点或编写代码来转换数据之前,请按以下顺序检查,并在遇到第一个适用项时停止:

  1. 服务自身的节点通过自身字段完成操作:CRM 节点的字段槽、Slack 节点的 Blocks 字段、Data Table 插入操作的逐列字段。仅在没有节点支持该操作时才使用 HTTP Request。
  2. 用于该任务的原生节点。 Filter、IF / Switch、Summarize、Aggregate、Split Out、Merge、Compare Datasets、Remove Duplicates、Sort + Limit、Date & Time、XML / HTML / Extract From File、HTTP Request 分页、Crypto。using-n8n-mcp-skills 中的任务到节点对照表会将任务映射到节点;相关配置见 n8n-node-configuration → NATIVE_NODES.md。
  3. 简短表达式({{ ... }}),写在该节点的字段中。属性访问、方法链(.map().filter().join())、三元表达式、字符串构建、Luxon 日期运算,以及下方的辅助函数——用一个表达式完成“取 A,生成 B”,无需中间变量。
    • 查询嵌套 JSON 以获取单个字段(在数组中选取、筛选或求和)→ 在同一个表达式中使用 $jmespath()(规则见下文)。如果下游需要每个元素各自成为一个条目,请改用 Split Out / Filter / Aggregate。
  4. Code 节点,仅用于以下情况:节点无法表达的循环或递归、跨运行的状态、没有节点能处理的解析、允许列表中的库、对所有条目执行大量操作(数千条,已声明或经测量),或用一个简短的 Code 节点(约 15 行)替代 4 个以上的胶水节点。请添加 Why Code: 说明。参见 n8n-code-javascript。

函数体就是代码。 如果逻辑需要 const、if、return 或多条语句——也就是在 {{ }} 中封装 (() => { ... })()——那它就已经超出表达式的范畴。请将其拆分为原生节点加简短表达式,或移入带有 Why Code: 说明的 Code 节点。长表达式比上述两种方式都更难阅读,而且其中的运行时错误会被静默转换为 null(参见“调试”)。创建/更新钩子会标记非 Code 节点中的函数体。

顺序为何重要。 可读性:原生节点会在画布上明确说明自身功能;简短表达式则直接显示在使用它的字段中,而不是藏在必须打开的上游节点里。性能很少会影响选择——条目数少于几百时,各种成本都无关紧要;经测量的模型见 n8n-code-javascript。

$jmespath() — 用一个表达式查询嵌套 JSON

{{ $jmespath($json, "customers[?country=='PL' && revenue > `100000`].name") }}    →  ["Acme"]

已在 n8n 2.38 上验证:这个表达式返回的结果与 Split Out → Filter → Aggregate 完全相同,无需额外节点。语法要求严格,大多数错误都不会显式报错:

写法而不是错误写法的结果
$jmespath(object, "query"),先传 object$jmespath("query", object)抛出 expected two arguments (Object, string) for this function(JMESPath 自己的文档中展示的是 search(query, data))
字符串使用单引号:country=='PL'country=="PL"双引号表示字段名 → 返回 [],不报错
数字/布尔值使用反引号:revenue > `100000`revenue > 100000解析错误 → 整个表达式变为 null(见调试)
&& || ! ==and or =解析错误 → null
连字符键加引号:'customers[*].contact."first-name"'(JS 字符串使用单引号)contact.first-name解析错误 → null
处理多个条目时保留包装层:$jmespath($('Node').all(), "[?json.country=='PL'].json.name") 或 $jmespath($input.all().map(i => i.json), "[?country=='PL'].name")在 .all() 上使用 "[?country=='PL'].name"条目被包装为 {json: …} → []
  • **结果:**缺失路径或索引 → null;筛选无匹配项 → [];对空投影使用 sum() → 0。
  • 第一个参数必须是对象或数组。 字符串(例如,返回文本响应的 HTTP Request)或 undefined($json.missingField)都会抛出相同的 expected two arguments 错误。该错误会导致节点失败。
  • 实用功能:length()、sum()、max_by(arr, &field)、sort_by(arr, &field)、reverse()、contains()、starts_with()、keys()、to_number();投影 [*]、展平 []、管道 | [0]、重塑 {name: name, email: contact.email}。
  • 它返回的是值,而不是条目。 当结果用于一个字段时(消息文本、HTTP 正文、IF/Filter 条件),使用它。当下游需要每个匹配项各自成为一个条目时,在 Edit Fields 中使用 $jmespath 筛选,然后对该字段执行一次 Split Out。
  • **适用位置:**表达式和 JavaScript Code 节点(参数顺序相同)。原生 Python Code 节点中不可用。

内置辅助函数

n8n 会为 {{ }} 中的值添加方法。结合原生节点,它们可以替代大多数单行 Code 节点。它们仅在表达式中存在,在 Code 节点中不可用——在 Code 中使用普通 JavaScript(并使用 toFormat() 而非 format())。 它们只能看到当前项的值:跨项处理(去重项、分组并汇总、合并两个列表)应放在节点中。在 n8n-mcp 2.97.0 及更高版本中,tools_documentation({topic: "expression_helpers", depth: "full"}) 会列出每个辅助函数及其经过测试的示例。

需求表达式说明
从每个对象中提取一个字段{{ $json.items.pluck('name') }}多个名称 → 每个对象对应一个数组
求和 / 平均值 / 最小值 / 最大值{{ $json.prices.sum() }}遇到非数字会抛出错误;也包括数字字符串
数组去重{{ $json.rows.unique('email') }}每个参数都是一个字段名;不传参数 → 比较整个值(removeDuplicates 是别名)
空值时的默认值{{ $ifEmpty($json.name, 'there') }}空值 = undefined、null、''、[]、{};恰好需要两个参数
文本中的邮箱或域名{{ $json.text.extractEmail() }}、.extractDomain()未匹配到内容时返回 undefined
对字符串进行哈希{{ $json.email.hash('sha256') }}默认使用 md5;HMAC 需要 Crypto 节点
两个日期之间的天数{{ $now.diffTo($json.created_at, 'days') }}接收者减去参数:参数为较早日期时结果为正
字符串转日期{{ $json.created_at.toDateTime().format('yyyy-MM-dd') }}然后可使用 .diffTo()、.plus(7, 'days')、.extract('month')
将数组拆分为多个批次{{ $json.ids.chunk(100) }}用于批量 API 请求体
丢弃空值{{ $json.tags.compact() }}、{{ $json.compact() }}适用于数组和对象
  • diffTo 接受 DateTime、JS Date 或 ISO 字符串,以及可选的单位(默认是 days)或单位列表(此时返回一个对象)。$json.created_at.toDateTime().diffToNow('days') 对过去的日期返回负数。
  • 对字符串使用 .sum() 会抛出错误:{{ $json.items.pluck('price').map(Number).sum() }}。
  • 对 null 或 undefined 调用辅助函数会抛出错误:使用 ?.({{ $json.email?.extractDomain() }})或 $ifEmpty() 进行保护。
  • 抛出错误的辅助函数(单位无效、参数数量错误、值缺失)会导致节点失败;{{ }} 中的普通 JavaScript 错误则会静默返回 null。无论哪种情况,都请使用实际项进行测试。

Set 节点反模式与分支汇合

删除仅为下一个节点重新映射字段的 Set 节点

如果 Set / Edit Fields 节点唯一的作用是复制或重命名字段,以便下一个节点读取,那么它就是多余的。直接在使用这些字段的节点中内联表达式即可。

❌  Webhook → Set { customer_id: {{ $json.body.customer_id }} } → Postgres: WHERE id = {{ $json.customer_id }}

✅  Webhook → Postgres: WHERE id = {{ $('Webhook').item.json.body.customer_id }}

Set 节点会增加一个跳转、让画布更杂乱,还会带来重构风险,却没有做任何使用这些字段的节点自己做不了的事。要通过 n8n_update_partial_workflow 干净地删除它:重新连接节点(从 Set 节点的源节点和目标节点移除连接,直接从源节点连接到使用这些字段的节点),使用 patchNodeField 将使用这些字段的节点中的表达式改为引用原始源节点的名称,然后使用 removeNode 移除 Set 节点。

快速测试: Set 节点是否只重新映射了下一个节点可以直接读取的字段($('Source').item.json.x)?

  • 是 → 删除它,在使用这些字段的节点中内联表达式。
  • 不是,而是创建了新内容 → 将它保留为一个命名步骤(按其生成的内容命名)。

以下情况中,Edit Fields 值得保留:

  • 它创建了一个常量条目 — 固定配置、默认负载或测试样例。
  • 它创建了错误或返回条目 — 在 Respond 之前创建 { error: 'validation_error', message: … },或创建子工作流的输出。
  • 它构建了一个负载对象,多个节点或整个请求正文都基于此对象构建。
  • 有 2 个或更多使用者读取同一个派生值,且派生过程并不简单(命名有助于提高可读性,而且只需计算一次)。
  • 它是子工作流的最终 Return 节点,用于定义输出契约。此处的“单个使用者”是所有调用者,因此 Set 节点就是 API 边界;并且设置 Include Other Fields: false 后,它会将输出形状限制在指定范围内,避免内部临时字段泄漏。
  • 你正在重命名字段或设置字段白名单,并希望在一个位置清晰呈现,而不是将其分散到各个使用者的表达式中。
分支汇合:使用 NoOp 作为锚点

分支汇合时(在 IF/Switch/Merge 之后),$json 会变成“最后触发的那个分支的数据”——这种情况是不确定的,也是错误数据的隐蔽来源。在汇合处插入一个 NoOp 节点,为它起一个描述性名称(Combine Inputs),并让下游节点按名称引用它:

Branch A ──┐
           ├─→ [NoOp: Combine Inputs] ──→ downstream uses $('Combine Inputs').item.json.x
Branch B ──┘

NoOp 节点能经受重构:之后即使在它和使用它的节点之间插入转换节点,也不会破坏 $('Combine Inputs') 引用。(如果各分支生成的形状不同,请使用 Set 节点而非 NoOp 节点,将两者规范化为同一种形状 — 参见上面的例外情况。)

更广泛地说,在包含多个分支的工作流中,优先使用 $('Node').item.json.x,而不是深层访问 $json.x。 插入中间节点或某个节点清除条目上下文(Aggregate、设置为 Run for All 的 Code、分支合并)时,$json 就会失效;这种失败是无声的,下游会在没有任何错误提示的情况下收到错误数据。无论源节点和使用它的节点之间有什么节点,按名称引用节点都不会产生歧义。


验证规则

1. 始终使用 {{}}

表达式必须用双大括号括起来。

❌ $json.field
✅ {{$json.field}}
2. 含空格和特殊字符时使用引号

名称中含空格、变音符号或特殊字符的字段或节点,必须使用方括号表示法:

❌ {{$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. 使用完全匹配的节点名称

节点引用区分大小写:

❌ {{$node["http request"].json}}  // lowercase
❌ {{$node["Http Request"].json}}  // wrong case
✅ {{$node["HTTP Request"].json}}  // exact match
4. 不要嵌套使用 {{}}

不要重复包裹表达式:

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

常见错误

完整的错误目录及修复方法,请参见 COMMON_MISTAKES.md

快速修复
错误修复方法
$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 节点)$json.email

工作示例

真实工作流示例,请参见 EXAMPLES.md

示例 1:Webhook 到 Slack

Webhook 接收内容:

{
  "body": {
    "name": "John Doe",
    "email": "[email protected]",
    "message": "Hello!"
  }
}

Slack 节点文本字段中的内容:

New form submission!

Name: {{$json.body.name}}
Email: {{$json.body.email}}
Message: {{$json.body.message}}
示例 2:HTTP Request 转 Email

HTTP Request 返回:

{
  "data": {
    "items": [
      {"name": "Product 1", "price": 29.99}
    ]
  }
}

在 Email 节点中(引用 HTTP Request):

Product: {{$node["HTTP Request"].json.data.items[0].name}}
Price: ${{$node["HTTP Request"].json.data.items[0].price}}
示例 3:格式化时间戳
// 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

数据类型处理

数组
// First item
{{$json.users[0].email}}

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

// Last item
{{$json.users[$json.users.length - 1].name}}
对象
// Dot notation (no spaces)
{{$json.user.email}}

// Bracket notation (with spaces or dynamic)
{{$json['user data'].email}}
字符串
// Concatenation (automatic)
Hello {{$json.name}}!

// String methods
{{$json.email.toLowerCase()}}
{{$json.name.toUpperCase()}}
数字
// Direct use
{{$json.price}}

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

高级模式

条件内容
// Ternary operator
{{$json.status === 'active' ? 'Active User' : 'Inactive User'}}

// Default values
{{$json.email || '[email protected]'}}
日期操作
// 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')}}
字符串操作
// Substring
{{$json.email.substring(0, 5)}}

// Replace
{{$json.message.replace('old', 'new')}}

// Split and join
{{$json.tags.split(',').join(', ')}}

性能:表达式复杂度几乎没有成本

人们常担心复杂的 {{ }} 会很慢。其实不会——真正影响性能的是 n8n 计算表达式的次数,而不是每个表达式有多复杂。在 n8n 2.x 上进行的测量显示,复杂表达式(sqrt、split、reduce)每个项目的成本与 {{ $json.x > 50 }} 大致相同:大部分成本来自 n8n 构建每个项目的求值上下文。

实际应用中:

  • 不要为了提升速度而把一个可用的短表达式拆分到额外节点中,也不要为了速度把它扩展成函数体。应根据可读性选择:原生节点,然后是短表达式,最后是 Code(上文的把关规则)。
  • 每个项目的成本只有在项目数达到数千时才会产生影响,主要取决于每个项目边界的数量(Code 处于 “Each Item” 模式时是最糟的情况)。n8n Code JavaScript 技能介绍了经过测量的每项目模型。

调试表达式

运行时错误不会导致节点失败,而是变成 null

在 n8n 2.38 的默认表达式运行时中验证过:执行期间,每个 {{ }} 外层的处理器只会重新抛出 n8n 自身的 ExpressionError,并吞掉其他 JavaScript 错误,因此字段会解析为空值或 null,节点仍会报告成功。$json.missing.field(TypeError)、JSON.parse('{bad')、throw new Error(...) 和 JMESPath 语法错误都会产生 null。在 Filter 或 IF 条件中,之后每个项目都会静默地检查失败。在其他版本或表达式引擎中,同样的错误可能会导致节点失败。无论哪种情况,都不要仅凭绿色运行状态就认定一切正常。

这并不意味着应该避免使用表达式(Code 节点也有自己的隐蔽陷阱),而是说明要用真实项目进行测试:

  • 在表达式编辑器中检查针对真实数据的预览。预览会显示错误。
  • 测试运行后,检查输出值。如果预期的数据变成了 null,这就是问题的表现。validate_workflow 和绿色的执行状态都无法告诉你原因。
  • 若要显示实际错误消息,可临时包装表达式: {{ (() => { try { return JSON.stringify(<expr>) } catch (e) { return 'ERROR: ' + e.message } })() }}
  • n8n 自身的错误仍会导致节点失败(例如,向 $jmespath 传入非对象参数)。
在表达式编辑器中测试
  1. 点击包含表达式的字段
  2. 打开表达式编辑器(点击 “fx” 图标)
  3. 查看结果的实时预览
  4. 检查是否有红色高亮的错误
常见错误消息

这些错误会出现在编辑器预览中;在运行时,其中大多数会被解析为 null(见上文)。

“Cannot read property 'X' of undefined” → 父对象不存在 → 检查数据路径

“X is not a function” → 尝试在非函数上调用方法 → 检查变量类型

表达式显示为普通文本 → 缺少 {{ }} → 添加花括号


表达式辅助方法

可用方法

字符串:

  • .toLowerCase()、.toUpperCase()
  • .trim()、.replace()、.substring()
  • .split()、.includes()

数组:

  • .length、.map()、.filter()
  • .find()、.join()、.slice()

日期时间(Luxon):

  • .toFormat()、.toISO()、.toLocal()
  • .plus()、.minus()、.set()

数字:

  • .toFixed()、.toString()
  • 数学运算:+、-、*、/、%

JSON 查询:

  • $jmespath(object, "query"):筛选、选取或聚合嵌套 JSON(引号规则见 $jmespath() 一节)

最佳实践

✅ 应该做
  • 动态内容始终使用 {{ }}
  • 字段名称中包含空格时使用方括号表示法
  • 从 .body 引用 webhook 数据
  • 使用 $node 获取其他节点的数据
  • 在表达式编辑器中测试表达式
❌ 不应该做
  • 不要在 Code 节点中使用表达式
  • 不要忘记给包含空格的节点名称加引号
  • 不要额外嵌套 {{ }}
  • 不要假设 webhook 数据位于根级别(它在 .body 下!)
  • 不要在 webhook 路径或凭据中使用表达式

相关技能

  • n8n MCP Tools Expert:学习如何使用 MCP 工具验证表达式
  • n8n Workflow Patterns:查看真实工作流示例中的表达式
  • n8n Node Configuration:了解何时需要使用表达式

总结

基本规则:

  1. 使用 {{ }} 包裹表达式
  2. Webhook 数据位于 .body 下
  3. Code 节点中不要使用 {{ }}
  4. 给包含空格的节点名称加引号
  5. 节点名称区分大小写
  6. {{ }} 中的运行时错误会静默地变为 null。测试运行后请检查输出值
  7. $jmespath(object, "query"):'string'、`number`、"field";在 .all() 前加上 json. 前缀

最常见的错误:

  • 缺少 {{ }} → 添加花括号
  • webhook 中使用 {{$json.name}} → 使用 {{$json.body.name}}
  • Code 中使用 {{$json.email}} → 使用 $json.email
  • 使用 {{$node.HTTP Request}} → 使用 {{$node["HTTP Request"]}}

更多详情,请参阅:


需要帮助? 请参阅 n8n 表达式文档,或使用 n8n-mcp 验证工具检查表达式。

评论

支持 Markdown