mcpField
N
n8n-expressions

n8n-expressions

github.com/n8n-io开发558349 次安装2 周前更新
代码仓库

关于 n8n-expressions

n8n-expressions 是面向 n8n 工作流构建者的技能指南,介绍表达式语法与数据引用方式。重点涵盖跨节点引用、JavaScript 变换、日期处理及避免不必要节点的实践。

主要功能

  1. 01使用节点名称引用数据
  2. 02用 JavaScript 表达式转换数据
  3. 03使用 Luxon 处理日期
  4. 04编写多行表达式与注释
  5. 05判断何时使用 Set 节点

适用场景

  1. 01在节点字段中直接计算或格式化值
  2. 02从上游节点稳定读取数据
  3. 03处理日期、数组与条件逻辑

安装

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

$npx skills add https://github.com/n8n-io/skills --skill n8n-expressions

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

详细介绍

n8n 表达式

n8n 的表达式语言是嵌入在 {{...}} 代码块中的 JavaScript。表达式会同步运行,每次处理一个条目($json),可以访问上游节点($('Name'))、用于处理日期的 Luxon,以及大部分原生 JS 功能。

不容妥协

按节点名称引用数据,而不是使用 $json。 使用 $('Node Name').item.json.field(或 .first().json.field)。$json 虽然可用,但只要有节点清除了条目上下文(Aggregate、使用 Run for All 的 Code、分支合并),或重构时加入了中间节点,就会失效。故障不会显式报错,下游会静默地拿到错误数据。按节点名称引用更稳定。

强烈建议

  • 不要添加仅用于向单个下游字段提供数据的 Set 节点。 在使用值的节点中直接写表达式。只有在 2 个或更多节点会读取同一个派生值、派生过程并非简单操作,或 Set 节点用于塑造子工作流的最终返回值时,才值得添加。参见下文“Set 节点反模式”。
  • 使用 Luxon 处理日期,而不是 DateTime 节点。 日期计算、格式化和解析都可以在表达式中完成:{{ DateTime.now().minus({ days: 7 }).toISO() }}。对于刚开始接触 n8n 的用户来说,DateTime 节点在画布上更直观,但除非用户特别要求,否则应避免使用。
  • 通常优先使用表达式,而不是额外添加节点。 在邮件节点的正文栏中编写邮件正文,在 HTTP Request 的 URL 栏中计算 URL。只有在需要重复使用转换结果,或该部分的主要目的就是执行转换时,才使用额外节点。
  • 多行表达式应缩进并添加注释。 表达式超过一行时,应像编写真正的代码那样设置格式。大多数 n8n 用户都不是程序员,因此请用简洁的行内注释解释代码。

为什么要按节点名称引用($('Name').item.json.x),而不是使用 $json.x

$json 表示“流入此节点的当前条目”。如果节点直接连接到一个数据源,且没有节点清除条目上下文(某些节点会清除,例如 Aggregate、使用 Run for All 的 Code、分支合并),那么使用 $json 没问题。

以下情况会导致问题:

  • 在数据源和使用数据的节点之间插入节点(使用数据的节点原本通过 $json.x 读取 3 步之前某个节点的数据,现在 $json 指向新插入的中间节点)。
  • 某个节点清除了上下文(Aggregate、某些合并操作,以及不会保留数据结构的 Code 节点)。
  • 分支通过 Merge 汇合。$json 指向最后触发的分支,无法保证结果一致。

$('Get User').item.json.id 指向明确。始终引用指定节点的第一个条目的 JSON,无论中间经过哪些节点。

例外情况:

当分支汇合后需要一个稳定的引用点时,在汇合处插入 NoOp 节点。为节点起一个描述性名称(例如 Combine Inputs)。下游节点按名称引用它。

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

重构时 NoOp 也能保持稳定:在 Combine Inputs 和使用数据的节点之间插入转换节点,不会影响 $('Combine Inputs') 引用。

如果下游节点需要使用上下文会被中间操作清除的节点中的数据,就必须采用这种模式。

如果各分支产生的数据结构不同,请使用 Set 节点,而不是 NoOp。 NoOp 会原样传递收到的数据结构,因此下游仍需判断哪个分支触发了。Set 节点会将两个分支的数据统一为一种结构,下游只需读取一组字段:

// Set node: "Normalize Inputs"
name: `={{ $('Lookup by Email').item.json.name || $('Lookup by ID').item.json.full_name }}`
email: `={{ $('Lookup by Email').item.json.email || $('Lookup by ID').item.json.contact_email }}`

无论数据由哪个分支产生,下游节点都按 $('Normalize Inputs').item.json.name 引用。

Set 节点反模式

AI Agent 经常生成的模式:

Webhook → Set: { customer_id: $json.body.customer_id, amount: $json.body.amount }
       → Postgres: WHERE id = {{ $json.customer_id }}
       → Email: Total is {{ $json.amount }}

Set 节点没有任何实际用途。每个下游节点都可以直接从 Webhook 读取:

Webhook → Postgres: WHERE id = {{ $('Webhook').item.json.body.customer_id }}
       → Email: Total is {{ $('Webhook').item.json.body.amount }}

只有在以下情况下,Set 节点才有存在的价值:

  • 同一个派生值会被多个下游使用者使用(派生过程并不简单)。
  • 派生逻辑较复杂,给它命名有助于提高可读性。
  • 多个分支需要相同的数据结构,使用共享的上游引用更清晰。
  • **它是子工作流的最后一个节点,用于塑造返回契约。**明确例外:“单个使用者”指的是每个调用方,因此 Set 是 API 边界。对子工作流而言,这种做法可选但值得推荐;有时如果前一个节点包含噪声字段,则是必需的。参见 n8n-subworkflows。
  • **你需要通过设置 Include Other Fields: false 从条目中移除字段。**Set 是将输出结构限定为白名单的最简洁方式。这也是上面“塑造子工作流返回值”条目背后的机制(防止内部临时字段泄露给调用方),但凡是需要下游使用干净数据结构的地方都适用。
  • **你需要重命名字段。**使用 Set 可以将重命名逻辑集中在一处,而不必散布到每个使用者的表达式中。

对于“从请求体中提取一个字段并使用一次”,不要使用 Set 节点。应将表达式放在使用该字段的地方。

对于“提取一次,供多个下游使用”,Set 节点确实合理。如果只有一个使用者使用它,Set 就会成为技术债(上面的返回值塑造情形除外)。

快速判断是否需要 Set 节点

有多少个下游节点会引用每个字段?

  • 0 或 1 个 → 删除节点,将表达式内联。
  • 2 个或更多 → 可能有保留的价值,尤其是派生过程并不简单时。

多个连续的 Set 节点几乎可以肯定属于过度提取。将它们合并。

表达式可以做什么

单字段转换
{{ $json.name.toUpperCase() }}
{{ $json.email.toLowerCase().trim() }}
{{ $json.items.length }}
{{ $json.user.first_name + ' ' + $json.user.last_name }}
{{ `(${$json.user.phone.slice(0, 3)}) ${$json.user.phone.slice(3, 6)}-${$json.user.phone.slice(6, 10)}` }}
方法链:.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) }}

// Find one matching item from another node's output
{{ $('Get Models').all().find(model => model.json.id === $json.modelId).json.modelName }}

// Filter array, then check shape
{{
  $('Get User\'s Entries').all()
    .map(item => item.json)
    .filter(entry => entry.prize_eligible === 'eligible')
    .length > 0
}}
多步方法链始终缩进,并添加注释

如果方法链包含 2 个或更多方法调用,或筛选逻辑不明显,请分行格式化并添加注释。读者不一定是作者,因此注释也能让非技术读者理解代码意图。

{{
  // Find all entries that are still processing AFTER 1 hour
  // (used to allow re-submission since something likely went wrong)
  $('Get User\'s Entries').all()
    .map(item => item.json)
    .filter(entry =>
      entry.prize_eligible === 'processing' &&
      $now.diffTo(entry.created_at, 'minutes') > 60
    )
    .length > 0
}}

这类逻辑常见于路由节点(Switch、IF)中。不加注释时,大多数用户都难以读懂。

.all().map() 会引发“执行一次”的问题

当你使用 $('Source Node').all().map(...)(或 .filter()、.reduce())处理整个数据集时,表达式本身会进行迭代。如果节点使用默认的逐条目执行模式,它会对每个输入条目运行一次,但每次运行都会执行完整的 .all() 聚合:这会造成资源浪费,也可能导致结果错误。

在以下情况下,将节点设置为仅执行一次:

  • 表达式使用了 .all().map() / .all().filter() / .all().reduce()。
  • 输出应为单个聚合结果,而不是逐条目输出。

这对应节点上的 executeOnce: true。大多数节点都支持此设置。

const aggregateNode = node({
    type: 'n8n-nodes-base.set',
    config: {
        executeOnce: true,            // important when using .all() in expressions
        parameters: {
            assignments: {
                assignments: [
                    {
                        name: 'totalEligible',
                        value: `={{
                            $('Get Entries').all()
                                .map(item => item.json)
                                .filter(entry => entry.eligible)
                                .length
                        }}`,
                        type: 'number',
                    },
                ],
            },
        },
    },
})

忘记设置 executeOnce 通常仍能正常运行,但 N 个条目会执行 N 次工作。更糟的是,如果下游预期只有一个条目,你会得到 N 个。

**反例:将 .all() 用作逐条目查找,而非聚合。**如果 .all() 读取的是另一个节点,并根据当前条目的标识进行筛选,那么就应该逐条目执行。每次迭代都会产生不同的结果,因此这是实际所需的工作,并非资源浪费。

// Workflow: Get Tags (200 items) → Search Posts (10 items) → this Set Fields node.
// Each post carries a `tag_ids` array. Set Fields runs per-item (10 times)
// and resolves each post's tag_ids into the full tag objects.
tags: ={{
  $('Get Tags').all()
    .filter(tag => $('Search Posts').item.json.tag_ids.includes(tag.json.id))
}}

在这里设置 executeOnce: true 会将 10 个输出合并为 1 个。

区分这两种情况的关键:

  • $source.all() 单独使用(对整个数据集进行聚合)→ executeOnce: true。
  • $source.all().filter(... matches $other.item.json.x)(根据当前条目进行查找)→ 保持 executeOnce 关闭。

**快速判断:**表达式是否使用了 .all(),但没有将它与另一个节点的 .item 结合?如果是,节点很可能应该设置为 executeOnce: true。

如需进一步了解迭代和显式循环,请参见 n8n-loops 技能。

条件表达式
{{ $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 }}    // days from now (negative if past)
跨节点引用(优先于 $json)
{{ $('Webhook Trigger').item.json.body.customer_id }}
{{ $('Lookup customer').item.json.email }}
{{ $('Combine Inputs').item.json.coupon_code }}    // NoOp convergence point

对于只处理单个项目的节点,.item 和 .first() 基本等效,任选其一即可。.first() 更明确,.item 更简短。

使用 IIFE 箭头函数编写多行逻辑

如果逻辑太复杂,无法写成一行,但只处理单个项目,可以将其包装在一个立即调用的箭头函数中:

{{ (() => {
    // Compute total including tax
    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),以及在任何函数中都能编写的 JS:const/let、if/switch、try/catch、正则表达式。

参数不起作用。 表达式没有调用方来传递参数,因此 (text) => text.replace(...) 没有任何东西可以调用它。请直接引用外部作用域中的值。函数仍需使用 IIFE 包装((...)())才能实际执行。

{{ (() => $json.text.replace(/\b(?:foo|bar)\b/gi, 'baz'))() }}

外层的 ( 和末尾的 )() 都是必需的:第一对括号将函数表达式括起来,末尾的 () 则调用它。省略任意一部分,n8n 都会报错并拒绝运行工作流。

为什么选择这种方式而不是 Code 节点? Code 节点在沙箱 VM 中运行:最坏情况下耗时约 500-1000ms。表达式 IIFE 则在与外围表达式相同的上下文中运行:耗时稳定在 1-10ms。对于纯粹的单项目处理,这两者相差 100 倍,功能却没有区别。这是资深用户常用的方法。

对于多项目聚合($input.all())、外部库或异步任务,Code 节点仍然有用。决策树请参阅 n8n-code-nodes,更多示例和格式规则请参阅 n8n-code-nodes 中的 ARROW_FUNCTIONS_IN_EDIT_FIELDS.md。

可用的原生 JS

String、Array、Number、Object、Map、Set、JSON.parse、JSON.stringify、Math、正则表达式、Date(但只能使用 Luxon)。

实用惯用写法

字段可能缺失时使用默认值
{{ $json.id || "fallback-id-here" }}

也可以使用可选链:

{{ $json.user?.profile?.id ?? "anonymous" }}

这对用于查询的筛选值尤其有用:传入一个无法匹配任何行的默认值,而不是让查询因 undefined 失败。

在文本字段中嵌入 JSON:选择哪种序列化方式

两种序列化方式,适用于两种场景:

  • .toJsonString() 用于不在意格式的紧凑 JSON。典型场景:AI 提示词。体积更小、更节省 token,也更容易在提示词模板中浏览。
    {{ $('Get Data').item.json.toJsonString() }}
    
  • JSON.stringify(value, null, 2) 用于在意格式的美化 JSON。典型场景:电子邮件正文、Slack 消息、调试输出,以及其他需要让人阅读结果的场景。
    {{ JSON.stringify($('Source Node').item.json, null, 2) }}
    

请谨慎选择。在 LLM 提示词中美化输出会浪费 token,并让模型的上下文变得杂乱。在电子邮件中使用紧凑 JSON 则难以阅读。

JSON.stringify 和 JSON.parse:应该用在哪里

JSON.stringify 和 JSON.parse 常用于表达式中,两者都没问题。关键原则:序列化和解析属于存储层操作,而非接口层操作。

  • 当你要写入的存储列无法原生保存该类型时,进行序列化。 最典型的情况:Data Tables 中后缀为 _object 的字符串列,实际存储的是数组或对象。请参阅 n8n-data-tables。
  • 从该存储列读回数据时,进行解析。 在拥有该存储的工作流内进行。
  • 不要跨越边界传递序列化后的结构。 子工作流返回值、Webhook 响应、Agent 工具结果、下游使用方,都应接收自然结构(数组就是数组,对象就是对象),而不是需要调用方记得用 JSON.parse 的序列化外壳。

一个常见的错误:子工作流有一条“新鲜”路径(数据刚由大模型生成,已经是数组)和一条“缓存”路径(数据刚从 _object 列读出,仍是字符串)。错误的做法是把新鲜路径序列化,“以匹配”缓存路径。正确的做法是解析缓存路径,让两个分支在输出时都采用相同的自然结构。

存储表示形式应留在拥有该存储的工作流内。越过这个边界后,应使用自然结构进行交互。n8n-subworkflows SKILL.md 中的“返回自然结构,而非存储结构”从子工作流的角度阐述了这一点,而 n8n-data-tables 则从存储的角度进行了说明。

返回正确的类型:何时使用 ={{ ... }} 包裹

如果不告诉 n8n 将值作为表达式求值,一些节点字段会把值视为字符串字面量。使用 ={{ ... }} 包裹(= 前缀会将字段切换为表达式模式),即可返回内部代码生成的实际类型:

// String literal (default behavior)
foo: 'plain string'

// Number
foo: '={{ 100 }}'

// Boolean
foo: '={{ true }}'

// Object (the `={{ ... }}` is what makes the receiver see an object, not a string)
foo: '={{ { "valid": true, "items": [] } }}'

// Array
foo: '={{ ["a", "b", "c"] }}'

// Reference to another node's value (preserves whatever type that value already is)
foo: '={{ $("Source Node").item.json.payload }}'

需要关注类型的场景:Set / Edit Fields 上的对象/数组字段(将列的 Type 设为 Object 或 Array 时)、HTTP Request 的 JSON 正文参数、子工作流的类型化 workflowInputs.values[type] 输入、Agent 工具参数,以及任何接收节点会校验类型的场景。如果不使用 ={{ ... }} 包裹,传递的就是字符串,接收方要么强制转换,要么报错。

请按节点名称引用,而不是使用 $json,遵循上文不可妥协的第 1 条:

// WRONG
foo: '={{ $json.payload }}'

// RIGHT
foo: '={{ $("Source Node").item.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"
}}

逻辑不明显时,建议添加注释。 这样后续阅读代码的人就能看到注释。

表达式无法完成的操作

  • 使用外部库(不能使用 require)。
  • 使用 Async / await。

$json 本身仅表示当前项,但表达式可以通过 $input.all()、$input.all()[3]、$('Source Node').all() 等方式访问其他项。请参阅上文的“方法链”。

相关内容请参阅 n8n-code-nodes。

如何选择:表达式、Edit Fields 还是 Code 节点?

根据 n8n-code-nodes 的决策树:

1. Single-field transform → expression in the field
2. Multi-step pure logic on one item → arrow function in Edit Fields
3. Multi-source aggregation, libraries, or stateful → Code node

默认使用表达式。只有在输入或作用域有要求时,才考虑其他方式。

“额外节点”异味

常见的额外节点用法其实可以保留在表达式中:

添加此节点更好的做法
使用 DateTime 节点格式化日期在使用方的表达式中使用 DateTime.fromISO(...).toFormat(...)
使用 Set 节点构建电子邮件正文在电子邮件节点的正文栏位中直接内联表达式
使用 Set 节点计算只使用一次的派生字段在使用方内联计算
使用两个节点(Set + IF)先计算再测试使用一个 IF 节点,在其条件表达式中进行计算
使用 Code 节点调用 .toUpperCase()直接使用表达式

为转换添加节点会造成更多视觉杂乱、拖慢工作流,并增加阅读难度。

适合添加额外节点的情况:

  • 该转换会被多个下游使用方重复使用。
  • 该转换开销较大(适合使用 Code 节点)。
  • 该转换是某个部分的主要目的(明确的“计算 X”步骤)。

反模式

反模式问题所在修正方法
添加 Set 节点,只为从 webhook 正文中提取一个字段,供一个下游使用方使用本应内联却多加了节点,重构时也容易出错删除 Set 节点,直接在使用方中引用 $('Webhook').item.json.body.x
连续添加多个 Set 节点,每个节点只定义一个字段工作流臃肿合并。大多数节点都没有必要;确实需要的字段则集中到一个 Set 节点中
在包含多个分支和中间转换的工作流深处使用 $json.x添加中间节点或清除上下文时引用会失效使用 $('Source Node').item.json.x。如果分支合并,请添加一个 NoOp 汇合点。
添加 DateTime 节点来格式化时间戳为一行 Luxon 表达式多加了一个节点{{ DateTime.fromISO($('Source').item.json.x).toFormat('yyyy-MM-dd') }}
使用 Set 节点构建电子邮件 HTML,然后在 Email 节点中读取本可使用一个表达式,却用了两个节点直接在电子邮件节点的正文栏位中构建 HTML
使用 new Date($json.created_at),而不是 Luxon无法使用格式化和操作功能DateTime.fromISO($('Source').item.json.created_at)
实际有 200 个字符的单行表达式难以阅读改为多行,使用箭头函数、缩进和注释
使用 $json.foo.bar.baz,却未检查 $json.foo 是否存在缺少中间字段时会崩溃使用 ?. 链:$('Source').item.json.foo?.bar?.baz
在表达式中硬编码本应作为配置的值出现魔法字符串使用 $vars.X(n8n Variables,付费方案)或 Data Table
分支合并后在下游使用 $json 引用最后触发的分支会胜出,结果不确定在合并处插入一个 NoOp(“Combine Inputs”),并按名称引用
在任何表达式中使用 $env.X无法工作;运行时会报错配置请使用 $vars.X(付费方案)或 Data Table。机密信息请使用凭据系统

评论

支持 Markdown