自动化与脚本
写 n8n 表达式时如何用节点名引用、Luxon…
在编写或审查 n8n 表达式({{...}}、$json/$node、Luxon 日期、表达式报错)时,用节点名引用、内联表达式和 Luxon 取代脆弱的 $json 与冗余节点。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
写 n8n 表达式时如何用节点名引用、L特定
解决思路
- 用节点名引用数据而非 $json:使用 `$('Node Name').item.json.field`(或 `.first().json.field`)。$json 在节点清空 item 上下文(Aggregate、Code 的 Run for All、分支合并)或重构插入中间节点时会静默失效,导致下游拿到错误数据且无报错;节点名引用稳定。
- 不要创建仅用于给单个下游字段供数的 Set 节点,把表达式内联到消费方。只有当 2 个以上消费者读取同一派生值、派生逻辑非平凡,或该 Set 是子工作流的最终返回整形节点时,Set 才值得存在。
- 日期用 Luxon 而非 DateTime 节点:日期运算、格式化、解析都可在表达式中完成,例如 `{{ DateTime.now().minus({ days: 7 }).toISO() }}`。除非用户明确要求,否则避免使用 DateTime 节点。
- 总体上优先用表达式而非额外节点:在邮件节点的 body 字段里构建正文,在 HTTP Request 的 URL 字段里计算 URL。只有当转换被复用或构成某段落的*主要目的*时才加节点。
- 多行表达式要缩进并加注释:当表达式超过一行时,像真实代码一样格式化,并用简洁的行内注释解释,因为多数 n8n 用户不是程序员。
- 当分支汇合且需要稳定引用点时,在汇合处插入一个 NoOp 节点,用描述性名称命名(如 `Combine Inputs`),下游按名称引用它。当中间操作会清空某节点上下文而下游仍需其数据时,此模式是必需的。
- 如果汇合的分支产出不同形状,改用 Set 节点而非 NoOp:NoOp 原样透传到达的形状,下游仍需知道哪个分支触发;Set 节点把两个分支归一为一种形状,下游只读一组字段,例如 `name: ={{ $('Lookup by Email').item.json.name || $('Lookup by ID').item.json.full_name }}`。
- 判断 Set 节点是否必要:统计每个字段被多少下游节点引用。0 或 1 个 → 删除并内联表达式;2+ 个 → 可能值得保留,尤其当派生非平凡时。多个连续的 Set 节点几乎肯定是过度抽取,应合并。
- 用数组方法链 `.map()`、`.filter()`、`.find()`、`.reduce()` 替代大量节点,例如 `{{ $json.tags.filter(tag => tag.active).map(tag => tag.name).join(', ') }}`、`{{ Object.values($json.scores).reduce((sum, score) => sum + score, 0) }}`、`{{ $('Get Models').all().find(model => model.json.id === $json.modelId).json.modelName }}`。
- 当方法链有 2+ 次调用或过滤逻辑不明显时,跨行缩进并加注释,说明意图,便于非技术读者理解,这在 Switch、IF 等路由节点中很常见。
- 当表达式使用 `$('Source Node').all().map()` / `.all().filter()` / `.all().reduce()` 对整个数据集聚合、且输出应为单个聚合结果时,把节点设为 `executeOnce: true`,否则会按输入项数重复执行全量聚合,浪费算力甚至产出 N 个 item。
- 区分 `.all()` 的两种用法:`$source.all()` 单独出现(跨数据集聚合)→ 设 `executeOnce: true`;`$source.all().filter(... matches $other.item.json.x)`(按当前 item 做查找)→ 保持默认逐项执行,否则会把 10 个输出塌缩成 1 个。快速判断:表达式用了 `.all()` 但没有与另一个节点的 `.item` 结合,则该节点大概率应设 `executeOnce: true`。
- 用三元表达式做条件判断,例如 `{{ $json.status === 'active' ? 'Active' : 'Inactive' }}`、`{{ $json.amount >= 100 ? 'Large' : ($json.amount >= 10 ? 'Medium' : 'Small') }}`。
- 用 Luxon 做日期运算,例如 `{{ DateTime.now().toISO() }}`、`{{ DateTime.fromISO($json.created_at).toFormat('yyyy-MM-dd') }}`、`{{ DateTime.now().minus({ days: 7 }).startOf('day').toISO() }}`、`{{ DateTime.fromISO($json.due).diffNow('days').days }}`。
- 跨节点引用优先于 $json,例如 `{{ $('Webhook Trigger').item.json.body.customer_id }}`、`{{ $('Lookup customer').item.json.email }}`、`{{ $('Combine Inputs').item.json.coupon_code }}`。`.item` 与 `.first()` 对单 item 节点基本等价,选一个即可,`.first()` 更显式,`.item` 更短。
- 当逻辑对单 item 过于复杂但一行放不下时,用立即调用箭头函数(IIFE)包裹,例如 `{{ (() => { const items = $json.line_items; const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0); const tax = subtotal * 0.08; return (subtotal + tax).toFixed(2) })() }}`。内部可用完整表达式作用域($json、$('Node Name')、$now、Luxon)以及 const/let、if/switch、try/catch、正则。
- IIFE 不能带参数:表达式没有调用方传参,所以 `(text) => text.replace(...)` 无法被调用,应直接引用外层作用域的值,但仍需 `(...)()` 包裹才能执行,例如 `{{ (() => $json.text.replace(/\b(?:foo|bar)\b/gi, 'baz'))() }}`。外层 `(` 和结尾 `)()` 是必需的,缺任一个 n8n 会报错并拒绝运行工作流。
- 单 item 纯整形优先用表达式 IIFE 而非 Code 节点:Code 节点在沙箱 VM 中运行,最坏约 500-1000ms;表达式 IIFE 在同一上下文运行,稳定 1-10ms,约 100 倍差距且功能无差异。Code 节点仍适用于多 item 聚合($input.all())、外部库或异步工作。
- 字段可能缺失时给默认值:`{{ $json.id || "fallback-id-here" }}` 或 `{{ $json.user?.profile?.id ?? "anonymous" }}`。对喂给查询的过滤值尤其有用,传一个匹配不到任何行的默认值,而不是让查询因 undefined 失败。
- 嵌入 JSON 时按上下文选择序列化器:`.toJsonString()` 用于紧凑 JSON、格式不重要处,典型是 AI 提示词(更小、省 token、易扫读);`JSON.stringify(value, null, 2)` 用于需要格式化的美化 JSON,典型是邮件正文、Slack 消息、调试输出。要有意选择:在 LLM 提示词里美化打印浪费 token 并弄乱上下文,在邮件里放紧凑 JSON 则不可读。
- 把 `JSON.stringify` 和 `JSON.parse` 当作存储层操作而非接口层操作:写入不能原生保存该类型的存储列(如 Data Tables 中带 `_object` 后缀的字符串列实际存数组/对象)时 stringify;从该存储列读回时 parse;不要把字符串化后的形状传播到边界之外(子工作流返回、webhook 响应、agent 工具结果、下游消费者都应收到自然形状)。典型错误:子工作流有“新鲜”路径(LLM 刚产出、已是数组)和“缓存”路径(刚从 `_object` 列读出、仍是字符串),错误做法是把新鲜路径也 stringify 去“对齐”,正确做法是 parse 缓存路径,让两个分支都产出相同的自然形状。
- 当字段类型重要时,用 `={{ ... }}` 包裹以返回实际类型而非字符串字面量:`foo: '={{ 100 }}'`(数字)、`foo: '={{ true }}'`(布尔)、`foo: '={{ { "valid": true, "items": [] } }}'`(对象)、`foo: '={{ ["a", "b", "c"] }}'`(数组)、`foo: '={{ $("Source Node").item.json.payload }}'`(保留原类型)。类型重要的场景:Set / Edit Fields 上类型为 Object 或 Array 的字段、HTTP Request 的 JSON body 参数、子工作流类型化 `workflowInputs.values[type]` 的结构化输入、agent 工具参数,以及任何接收方会校验类型的地方。
- 按节点名引用而非 $json(同上第一条):`foo: '={{ $("Source Node").item.json.payload }}'` 正确,`foo: '={{ $json.payload }}'` 错误。例外:若 $json 确实是正确的(无中间转换、无汇合)且该字段是直接下游单源节点的逐项槽位,即便如此,命名引用也更抗重构。
- 非显而易见逻辑的多行表达式加解释性注释,例如 `{{ // Default to avoid query errors when user_id is missing. // The fallback UUID is a known-empty row. $json.id || "305f7106-6988-4651-b26a-18979641b7b5" }}`,为下一位读者留下说明。
- 按决策树选择表达式、Edit Fields 还是 Code 节点:1) 单字段转换 → 字段内表达式;2) 单 item 多步纯逻辑 → Edit Fields 中的箭头函数;3) 多源聚合、需要库或有状态 → Code 节点。表达式是默认选择,只有输入或作用域要求时才越级。
- 避免“额外节点”气味:格式化日期用 `DateTime.fromISO(...).toFormat(...)` 而非 DateTime 节点;构建邮件正文内联到邮件节点 body 字段而非 Set 节点;计算只用一次的派生字段内联到消费方;计算后测试用带条件表达式的单个 IF 而非 Set + IF 两个节点;调用 `.toUpperCase()` 直接用表达式而非 Code 节点。额外节点只在转换被多个下游复用、转换很重(Code 节点领域)或转换是某段落的*主要目的*时才正确。
- 避免反模式:为单个下游消费者从 webhook body 抽取一个字段的 Set 节点(应删除并直接引用 `$('Webhook').item.json.body.x`);多个连续 Set 节点各定义一个字段(应合并);在多分支和中间转换的深层用 `$json.x`(应改用 `$('Source Node').item.json.x`,分支汇合处加 NoOp);加 DateTime 节点格式化时间戳(应用一行 Luxon);用 Set 节点构建邮件 HTML 再在 Email 节点读取(应直接在邮件节点 body 字段构建);用 `new Date($json.created_at)` 而非 Luxon;一行 200 字符的表达式(应用多行箭头函数加缩进和注释);`$json.foo.bar.baz` 不检查 `$json.foo` 是否存在(应用 `?.` 链);在表达式中硬编码本应配置的值(应用 `$vars.X`(付费计划)或 Data Table);分支汇合后下游用 `$json` 引用(应插入 NoOp 并按名引用);在任何表达式中使用 `$env.X`(不工作,运行时抛错;配置用 `$vars.X`(付费计划)或 Data Table,密钥用凭证系统)。
- 表达式不能使用外部库(无 `require`),也不能用 async / await;这些场景改用 Code 节点。$json 本身只是当前 item,但表达式可通过 `$input.all()`、`$input.all()[3]`、`$('Source Node').all()` 等跨 item 访问。
适用边界 · 注意事项
- 需要多 item 聚合($input.all())、外部库或异步工作时,不要用表达式,改用 Code 节点。
- 当分支产出不同形状时,不要用 NoOp 做汇合点,改用 Set 节点归一形状。
- 当 $json 确实是正确的(无中间转换、无汇合)且字段是直接下游单源节点的逐项槽位时,不必强行改用节点名引用(但命名引用更抗重构)。