n8n-expression-syntax

n8n-expression-syntax

热门

验证 n8n 表达式语法并修复常见错误。在编写 n8n 表达式、使用 {{}} 语法、访问 $json/$node 变量、排查表达式错误、在节点之间映射数据或在工作流中引用 webhook 数据时使用。每当配置引用前一个节点数据的节点字段时都应使用此技能——表达式是 n8n 在节点之间传递数据的方式,语法错误是工作流错误最常见的来源。当被问及复杂表达式是否影响性能时也应使用。

5825Star
981Fork
更新于 2026/7/16
SKILL.md
只读
名称
n8n-expression-syntax
描述

验证 n8n 表达式语法并修复常见错误。在编写 n8n 表达式、使用 {{}} 语法、访问 $json/$node 变量、排查表达式错误、在节点之间映射数据或在工作流中引用 webhook 数据时使用。每当配置引用前一个节点数据的节点字段时都应使用此技能——表达式是 n8n 在节点之间传递数据的方式,语法错误是工作流错误最常见的来源。当被问及复杂表达式是否影响性能时也应使用。

n8n 表达式语法

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


表达式格式

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

{{expression}}

示例

✅ {{$json.email}}
✅ {{$json.body.name}}
✅ {{$node["HTTP Request"].json.data}}
❌ $json.email  (无花括号 - 视为纯文本)
❌ {$json.email}  (单花括号 - 无效)

核心变量

$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": {           // ⚠️ 用户数据在这里!
    "name": "John",
    "email": "john@example.com",
    "message": "Hello"
  }
}

正确的 Webhook 数据访问

❌ 错误:{{$json.name}}
❌ 错误:{{$json.email}}

✅ 正确:{{$json.body.name}}
✅ 正确:{{$json.body.email}}
✅ 正确:{{$json.body.message}}

原因:Webhook 节点将传入数据包装在 .body 属性下,以保留 headers、params 和 query 参数。


常见模式

访问嵌套字段

// 简单嵌套
{{$json.user.email}}

// 数组访问
{{$json.data[0].name}}
{{$json.items[0].id}}

// 带空格的括号表示法
{{$json['field name']}}
{{$json['user data']['first name']}}

引用其他节点

// 无空格节点
{{$node["Set"].json.value}}

// 带空格节点(常见!)
{{$node["HTTP Request"].json.data}}
{{$node["Respond to Webhook"].json.message}}

// Webhook 节点
{{$node["Webhook"].json.body.email}}

组合变量

// 拼接(自动)
Hello {{$json.body.name}}!

// 在 URL 中
https://api.example.com/users/{{$json.body.user_id}}

// 在对象属性中
{
  "name": "={{$json.body.name}}",
  "email": "={{$json.body.email}}"
}

何时不使用表达式

❌ Code 节点

Code 节点使用直接的 JavaScript 访问,而不是表达式!

// ❌ 在 Code 节点中错误
const email = '={{$json.email}}';
const name = '{{$json.body.name}}';

// ✅ 在 Code 节点中正确
const email = $json.email;
const name = $json.body.name;

// 或使用 Code 节点 API
const email = $input.item.json.email;
const allItems = $input.all();

❌ Webhook 路径

// ❌ 错误
path: "{{$json.user_id}}/webhook"

// ✅ 正确
path: "user-webhook"  // 仅静态路径

❌ 凭据字段

// ❌ 错误
apiKey: "={{$env.API_KEY}}"

// ✅ 正确
使用 n8n 凭据系统,而不是表达式

转换把关者

在添加任何节点或编写任何代码来转换数据之前,请按此顺序检查,并在第一个符合条件的地方停止:

  1. 表达式{{ ... }})在消费字段中。属性访问、方法链(.map().filter().join())、三元运算符、字符串构建、Luxon 日期数学——如果是“取 A,产生 B”且没有中间变量,那就是表达式。这涵盖了大多数“只需转换这个”的情况。

  2. 箭头函数 IIFE 在 Edit Fields 字段内。 当逻辑需要中间变量、分支或注释但仍对单个项目操作时,将其包装在字段值中的立即调用的箭头函数内:

    ={{ (() => {
        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);
    })() }}
    

    外部的 (...) 将函数括起来;尾部的 () 调用它。去掉任何一个,n8n 都会拒绝运行。内部你可以使用完整的表达式作用域($json$('Node')$now、Luxon)以及 const/letif/switchtry/catch 和正则表达式。不支持 requireawait

  3. Code 节点——最后的手段。 仅当需要对整个数据集进行多项目聚合($input.all())、使用允许列表中的库或异步工作时使用。

为什么顺序很重要。 这不是风格问题——而是可读性和性能问题。Code 节点在沙箱化 VM 中运行,每次调用都有设置和值编组的开销——在逻辑运行之前,冷启动成本可能达到 500–1000 毫秒。(在热运行、高项目数运行时它会摊销,因此将此视为常见情况成本,而非通用常数。)相同的逻辑在表达式或 Edit Fields IIFE 中运行,在进程内只需几毫秒,并完全跳过沙箱。对于纯单项目整形,这是一个巨大的差距,且功能上没有区别,并且在热路径(如每个请求的 webhook)上会累积。表达式也保留在使用它的字段中,而不是隐藏在上游节点中,别人必须打开才能理解。只有当输入或作用域确实需要时,才超越一个阶段。

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 的源和目标 removeConnection,直接从源到消费者 addConnection),patchNodeField 消费者的表达式以按节点名称引用原始源,然后 removeNode 该 Set。

快速测试: 计算有多少下游节点引用 Set 产生的每个字段。

  • 0 或 1 → 删除,内联到消费者。
  • 2+ → 它可能值得保留。

合理的例外——在以下情况下保留 Set:

  • 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 $json 在插入中间节点或节点清除项目上下文(Aggregate、Run for All 模式的 Code、分支合并)时立即失效;失败是静默的,下游得到错误数据且没有错误。节点名称引用是明确的,无论源和消费者之间有什么。


验证规则

1. 始终使用 {{}}

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

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

2. 对空格和特殊字符使用引号

带有空格、变音符号或特殊字符的字段或节点名称需要括号表示法

❌ {{$json.field name}}
✅ {{$json['field name']}}

❌ {{$node.HTTP Request.json}}
✅ {{$node["HTTP Request"].json}}

// 对于包含特殊字符的键,括号表示法是必需的
✅ {{$json['Gross Price w/o shipment']}}
✅ {{$json['Cena brutto zł']}}

3. 匹配确切的节点名称

节点引用区分大小写

❌ {{$node["http request"].json}}  // 小写
❌ {{$node["Http Request"].json}}  // 错误大小写
✅ {{$node["HTTP Request"].json}}  // 精确匹配

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": "john@example.com",
    "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:格式化时间戳

// 当前日期
{{$now.toFormat('yyyy-MM-dd')}}
// 结果:2025-10-20

// 时间
{{$now.toFormat('HH:mm:ss')}}
// 结果:14:30:45

// 完整日期时间
{{$now.toFormat('yyyy-MM-dd HH:mm')}}
// 结果:2025-10-20 14:30

数据类型处理

数组

// 第一个项目
{{$json.users[0].email}}

// 数组长度
{{$json.users.length}}

// 最后一个项目
{{$json.users[$json.users.length - 1].name}}

对象

// 点表示法(无空格)
{{$json.user.email}}

// 括号表示法(有空格或动态)
{{$json['user data'].email}}

字符串

// 拼接(自动)
Hello {{$json.name}}!

// 字符串方法
{{$json.email.toLowerCase()}}
{{$json.name.toUpperCase()}}

数字

// 直接使用
{{$json.price}}

// 数学运算
{{$json.price * 1.1}}  // 加 10%
{{$json.quantity + 5}}

高级模式

条件内容

// 三元运算符
{{$json.status === 'active' ? 'Active User' : 'Inactive User'}}

// 默认值
{{$json.email || 'no-email@example.com'}}

日期操作

// 加天数
{{$now.plus({days: 7}).toFormat('yyyy-MM-dd')}}

// 减小时
{{$now.minus({hours: 24}).toISO()}}

// 设置特定日期
{{DateTime.fromISO('2025-12-25').toFormat('MMMM dd, yyyy')}}

字符串操作

// 子字符串
{{$json.email.substring(0, 5)}}

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

// 分割和连接
{{$json.tags.split(',').join(', ')}}

性能:表达式复杂性(几乎)免费

一个常见的担忧是复杂的 {{ }} 很慢。事实并非如此——成本在于 n8n 评估表达式的次数,而不是每个表达式有多复杂。

在 n8n 2.x 实例上测量,一个复杂的表达式(sqrtsplitreduce、算术)每个项目的成本与一个简单的 {{ $json.x > 50 }} 相同——大约 ~0.2 毫秒/项目,因为约 90% 的成本是 n8n 构建每个项目的评估上下文,而不是运行你的表达式。

这在实践中意味着:

  • 不要为了“速度”而将一个有效的表达式拆分成一串节点。 每个额外的节点会重新评估每个项目并重新复制所有项目;一个节点带一个更丰富的表达式胜过三个节点带简单的表达式。
  • 一个表达式(~0.2 毫秒/项目)比“每个项目运行一次”模式的 Code 节点(~0.6 毫秒/项目)便宜约 3 倍——但“所有项目运行一次”模式的 Code 节点更便宜(~0.02 毫秒/项目),因为它只跨越每个项目边界一次而不是 N 次。
  • 这仅在数千个项目时才有影响;低于此值,成本低于 100 毫秒。n8n Code JavaScript 技能有完整的每个项目边界模型。

调试表达式

在表达式编辑器中测试

  1. 点击包含表达式的字段
  2. 打开表达式编辑器(点击“fx”图标)
  3. 查看结果的实时预览
  4. 检查以红色高亮显示的错误

常见错误消息

"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()
  • 数学运算:+-*/%

最佳实践

✅ 应该做

  • 始终对动态内容使用 {{ }}
  • 对包含空格的字段名称使用括号表示法
  • .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. 节点名称区分大小写

最常见的错误

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

更多详情,请参见:


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