自动化与脚本
在 n8n 工作流中写表达式时如何正确引用数据、避免静默…
编写或排查 n8n 的 {{ }} 表达式($json/$node/$now/$env、webhook 数据、$jmespath 查询、节点间数据映射)时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
在 n8n 工作流中写表达式时如何正确引特定
解决思路
- 所有动态内容必须用双花括号包裹:`{{expression}}`;`$json.email`(无括号)会被当字面文本,`{$json.email}`(单括号)无效。
- 用 `$json` 访问当前节点输出:`{{$json.fieldName}}`、`{{$json['field with spaces']}}`、`{{$json.nested.property}}`、`{{$json.items[0].name}}`。
- 用 `$node` 引用任意前置节点:`{{$node["Node Name"].json.fieldName}}`;节点名必须加引号、区分大小写、与工作流中的名称完全一致。
- 用 `$now` 取当前时间:`{{$now}}`、`{{$now.toFormat('yyyy-MM-dd')}}`、`{{$now.toFormat('HH:mm:ss')}}`、`{{$now.plus({days: 7})}}`。
- 用 `$env` 读环境变量:`{{$env.API_KEY}}`、`{{$env.DATABASE_URL}}`;若实例启用了 `N8N_BLOCK_ENV_ACCESS_IN_NODE` 会报错,改用 credentials、Set 节点手填值,或通过 webhook query 参数传入。
- Webhook 数据不在根层,而在 `.body` 下:用 `{{$json.body.name}}`、`{{$json.body.email}}`,不要写 `{{$json.name}}`;因为 Webhook 节点把用户数据包在 `.body` 里以保留 headers/params/query。
- 嵌套字段与数组访问:`{{$json.user.email}}`、`{{$json.data[0].name}}`、`{{$json.items[0].id}}`;含空格或特殊字符的键必须用方括号:`{{$json['field name']}}`、`{{$json['user data']['first name']}}`、`{{$json['Gross Price w/o shipment']}}`、`{{$json['Cena brutto zł']}}`。
- 跨节点引用时给含空格的节点名加引号:`{{$node["HTTP Request"].json.data}}`、`{{$node["Respond to Webhook"].json.message}}`、`{{$node["Webhook"].json.body.email}}`。
- 组合变量:字符串自动拼接 `Hello {{$json.body.name}}!`;URL 中 `https://api.example.com/users/{{$json.body.user_id}}`;对象属性 `"name": "={{$json.body.name}}"`。
- 不要在 Code 节点里用表达式:改用直接 JS 访问 `const email = $json.email;`、`const name = $json.body.name;`,或用 Code 节点 API `$input.item.json.email`、`$input.all()`。
- 不要在 webhook path 里用表达式:`path: "{{$json.user_id}}/webhook"` 是错的,只能写静态路径如 `path: "user-webhook"`。
- 不要在 credential 字段里用表达式:`apiKey: "={{$env.API_KEY}}"` 是错的,应使用 n8n credential 系统。
- 转换数据前按顺序选层级,命中即停:① 在消费字段里写表达式 `{{ ... }}`(属性访问、`.map().filter().join()` 链、三元、字符串拼接、Luxon 日期运算);② 需要中间变量/分支/注释但仍是单条数据时,在 Edit Fields 字段里写箭头函数 IIFE:`={{ (() => { 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); })() }}`(外层 `(...)` 与结尾 `()` 缺一不可,内部可用 `$json`、`$('Node')`、`$now`、Luxon、`const`/`let`、`if`/`switch`、`try`/`catch`、正则,但不能 `require`、不能 `await`);③ 只有需要跨全部数据聚合(`$input.all()`)、白名单库或异步时才用 Code 节点。
- 优先用表达式/IIFE 而非 Code 节点:Code 节点跑在沙箱 VM 中,每次调用有建值开销,冷启动可达 500–1000ms;表达式与 Edit Fields IIFE 在进程内执行,单条数据只需个位数毫秒,且逻辑留在使用它的字段里可见。
- 查询嵌套 JSON 时在同一个表达式里用 `$jmespath()`,例如 `{{ $jmespath($json, "customers[?country=='PL' && revenue > `100000`].name") }}`,一个表达式即可替代 Split Out → Filter → Aggregate 链。
- 严格遵守 `$jmespath` 语法:对象在前 `$jmespath(object, "query")`(写反会抛 `expected two arguments (Object, string) for this function`);字符串用单引号 `country=='PL'`(双引号表示字段名,会静默返回 `[]`);数字/布尔用反引号 `` revenue > `100000` ``(否则解析错误使整个表达式变 `null`);用 `&&` `||` `!` `==` 而非 `and` `or` `=`;含连字符的键要加引号 `'customers[*].contact."first-name"'`;对 `.all()` 查询时要保留 `json.` 包装:`$jmespath($('Node').all(), "[?json.country=='PL'].json.name")` 或 `$jmespath($input.all().map(i => i.json), "[?country=='PL'].name")`。
- 记住 `$jmespath` 的返回语义:路径或索引缺失返回 `null`,过滤无匹配返回 `[]`,空投影上 `sum()` 返回 `0`;第一个参数必须是对象或数组,传字符串(如 HTTP Request 的文本响应)或 `undefined`(`$json.missingField`)会抛同样的 `expected two arguments` 错误并使节点失败。
- 善用 `$jmespath` 内置函数与语法:`length()`、`sum()`、`max_by(arr, &field)`、`sort_by(arr, &field)`、`reverse()`、`contains()`、`starts_with()`、`keys()`、`to_number()`;投影 `[*]`、展平 `[]`、管道 `| [0]`、重塑 `{name: name, email: contact.email}`。
- `$jmespath` 返回的是值而不是 items:用在结果只喂给单个字段的地方(消息文本、HTTP body、IF/Filter 条件);若下游需要每个匹配一条 item,先在 Edit Fields 里用 `$jmespath` 收窄,再对该字段做一次 Split Out。
- 注意 `$jmespath` 的可用范围:表达式和 JavaScript Code 节点中可用(参数顺序相同),原生 Python Code 节点中不可用。
- 删除只服务单个下游消费者的 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 }}`。
- 用 `n8n_update_partial_workflow` 干净移除 Set 节点:先改接线(对 Set 的源与目标 `removeConnection`,再从源直接 `addConnection` 到消费者),再 `patchNodeField` 把消费者的表达式改为按节点名引用原始来源,最后 `removeNode` 删除该 Set。
- 用「下游引用该 Set 产出字段的节点数」判断去留:0 或 1 个就删除并内联到消费处;2 个及以上才可能值得保留。
- 保留 Set 节点的合法例外:① 2 个以上消费者读取同一个非平凡派生值(命名有助可读且只算一次);② 它是子工作流的最终 Return 节点、用于塑造输出契约(此时「唯一消费者」是所有调用方,Set 就是 API 边界,配合 `Include Other Fields: false` 白名单化输出、避免内部临时字段泄漏);③ 你在重命名或白名单化字段,希望集中在一处可见而非散落在各消费者表达式中。
- 分支汇聚处插入 NoOp 节点做锚点:`Branch A ──┐ ├─→ [NoOp: Combine Inputs] ──→ downstream uses $('Combine Inputs').item.json.x`,并起描述性名称;因为汇聚后 `$json` 变成「最后触发的分支」,不确定且会静默取到错误数据。
- NoOp 锚点能扛住重构:之后在它与消费者之间插入转换节点也不会破坏 `$('Combine Inputs')` 引用;若各分支产出结构不同,则改用 Set 节点把两者归一成同一形状。
- 在分支较多的流程中优先用 `$('Node').item.json.x` 而非深层 `$json.x`:一旦中间插入节点或某节点清空 item 上下文(Aggregate、Run for All 的 Code、分支合并),`$json` 会静默失效并让下游拿到错误数据且不报错。
- 表达式必须用 `{{ }}` 包裹,不要嵌套双花括号:`{{{$json.field}}}` 是错的,应为 `{{$json.field}}`。
- 节点名必须精确匹配且区分大小写:`{{$node["http request"].json}}`、`{{$node["Http Request"].json}}` 都是错的,应为 `{{$node["HTTP Request"].json}}`。
- 记住运行时错误不会让节点失败而是变成 `null`(n8n 2.38 默认表达式运行时):`$json.missing.field`(TypeError)、`JSON.parse('{bad')`、`throw new Error(...)`、JMESPath 语法错误都产出 `null` 且节点仍报成功;在 Filter/IF 条件里则每个 item 静默不通过。
- 用真实数据测试表达式:在表达式编辑器里看实时预览(预览会显示错误);测试运行后检查输出值,期望有数据的地方出现 `null` 就是症状,`validate_workflow` 和绿色执行结果都不会告诉你。
- 需要看到真实错误信息时临时包裹表达式:`{{ (() => { try { return JSON.stringify(<expr>) } catch (e) { return 'ERROR: ' + e.message } })() }}`;注意 n8n 自身的错误仍会让节点失败(例如 `$jmespath` 收到非对象参数)。
- 在表达式编辑器中调试:点击含表达式的字段 → 点 "fx" 图标打开编辑器 → 查看实时预览结果 → 检查红色高亮的错误。
- 识别常见错误信息:"Cannot read property 'X' of undefined" 表示父对象不存在、检查数据路径;"X is not a function" 表示在非函数上调用方法、检查变量类型;表达式显示为字面文本表示缺少 `{{ }}`。
- 不要因为担心性能而把复杂表达式拆成一串节点:实测在 n8n 2.x 上,复杂表达式(`sqrt`、`split`、`reduce`、算术)与简单的 `{{ $json.x > 50 }}` 每 item 成本相同,约 ~0.2 ms/item,其中约 90% 是 n8n 构建逐 item 求值上下文而非执行表达式;每多一个节点都会重新逐 item 求值并复制全部数据。
- 按每 item 成本选执行模式:表达式(~0.2 ms/item)比 Code 节点「Run Once for Each Item」模式(~0.6 ms/item)约便宜 3 倍,而 Code 节点「Run Once for All Items」模式更便宜(~0.02 ms/item),因为它只跨一次逐 item 边界;这些差异只在数千条 item 时才明显,低于此量级都在 100ms 以内。
- 善用可用方法:字符串 `.toLowerCase()`、`.toUpperCase()`、`.trim()`、`.replace()`、`.substring()`、`.split()`、`.includes()`;数组 `.length`、`.map()`、`.filter()`、`.find()`、`.join()`、`.slice()`;Luxon 日期 `.toFormat()`、`.toISO()`、`.toLocal()`、`.plus()`、`.minus()`、`.set()`;数字 `.toFixed()`、`.toString()` 与 `+ - * / %`。
- 用高级写法处理常见需求:三元 `{{$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(', ')}}`。
- 处理数组时注意:首项 `{{$json.users[0].email}}`、长度 `{{$json.users.length}}`、末项 `{{$json.users[$json.users.length - 1].name}}`。
- 遵守最佳实践:始终用 `{{ }}`;含空格的字段名用方括号;webhook 数据从 `.body` 取;跨节点数据用 `$node`;在表达式编辑器中测试;不在 Code 节点、webhook path、credentials 中使用表达式;不忘记给含空格的节点名加引号;不重复包裹 `{{ }}`;不假设 webhook 数据在根层。
适用边界 · 注意事项
- 不使用 n8n 或不需要编写/排查 n8n 表达式的工作流场景。
- 在 Code 节点、webhook path、credential 字段中不应使用表达式语法(这些位置有各自的正确写法)。
- 实例启用了 `N8N_BLOCK_ENV_ACCESS_IN_NODE` 时不能依赖 `$env` 访问环境变量。