关于 n8n-expression-syntax
面向 n8n 工作流表达式编写的指南,讲解动态字段、节点数据引用及内置辅助函数,并提供选择表达式或原生节点的规则。
主要功能
- 01使用双花括号编写动态表达式
- 02引用当前及其他节点的数据
- 03借助 JMESPath 查询嵌套 JSON
- 04使用内置数组、字符串和日期辅助函数
- 05按任务选择原生节点、表达式或代码
适用场景
- 01在字段中插入上游节点数据
- 02筛选嵌套 JSON 并生成字段值
- 03格式化日期或处理数组内容
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ czlonkowski/ n8n-skills --skill n8n-expression-syntax安装后在 Claude Code 中输入/n8n-expression-syntax确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
COMMON_MISTAKES.mdEXAMPLES.mdREADME.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
转换把关流程
在添加节点或编写代码来转换数据之前,请按以下顺序检查,并在遇到第一个适用项时停止:
- 服务自身的节点通过自身字段完成操作:CRM 节点的字段槽、Slack 节点的 Blocks 字段、Data Table 插入操作的逐列字段。仅在没有节点支持该操作时才使用 HTTP Request。
- 用于该任务的原生节点。 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。 - 简短表达式(
{{ ... }}),写在该节点的字段中。属性访问、方法链(.map().filter().join())、三元表达式、字符串构建、Luxon 日期运算,以及下方的辅助函数——用一个表达式完成“取 A,生成 B”,无需中间变量。- 查询嵌套 JSON 以获取单个字段(在数组中选取、筛选或求和)→ 在同一个表达式中使用
$jmespath()(规则见下文)。如果下游需要每个元素各自成为一个条目,请改用 Split Out / Filter / Aggregate。
- 查询嵌套 JSON 以获取单个字段(在数组中选取、筛选或求和)→ 在同一个表达式中使用
- 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传入非对象参数)。
在表达式编辑器中测试
- 点击包含表达式的字段
- 打开表达式编辑器(点击 “fx” 图标)
- 查看结果的实时预览
- 检查是否有红色高亮的错误
常见错误消息
这些错误会出现在编辑器预览中;在运行时,其中大多数会被解析为 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:了解何时需要使用表达式
总结
基本规则:
- 使用 {{ }} 包裹表达式
- Webhook 数据位于
.body下 - Code 节点中不要使用 {{ }}
- 给包含空格的节点名称加引号
- 节点名称区分大小写
{{ }}中的运行时错误会静默地变为null。测试运行后请检查输出值$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"]}}
更多详情,请参阅:
- COMMON_MISTAKES.md - 完整的错误目录
- EXAMPLES.md - 真实工作流示例
需要帮助? 请参阅 n8n 表达式文档,或使用 n8n-mcp 验证工具检查表达式。