关于 n8n-subworkflows
n8n-subworkflows 是用于设计可复用 n8n 子工作流的技能,帮助 Agent 搜索现有实现、定义清晰的输入输出契约,并判断何时为复用、测试或隔离而提取逻辑。
主要功能
- 01构建前先搜索
- 02类型化输入契约
- 03无状态与有状态模式
- 04用于发现的标签
- 05提取时机指南
适用场景
- 01在多个 n8n 工作流中复用逻辑
- 02创建可独立测试的工作流步骤
- 03封装数据库或 API 操作
- 04按标签查找现有子工作流
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ n8n-io/ skills --skill n8n-subworkflows安装后在 Claude Code 中输入/n8n-subworkflows确认已加载。
附带文件references/NAMING_AND_DISCOVERY.mdreferences/SUBWORKFLOW_PATTERNS.md
详细介绍
n8n 子工作流
子工作流是可复用的函数。Execute Workflow Trigger 声明输入参数,主体负责执行工作,最后一个节点返回输出。调用方可以像调用其他节点一样调用它。
这种函数式设计带来了封装、复用、可测试性和可替换性等优势。它是 n8n 中主要的复用机制,但很遗憾,使用得还不够充分。
如果没有子工作流,同一逻辑就会在多个工作流中重复。修复 Bug 时需要修改多个地方,可能会漏掉一个,导致这些「相同」的副本逐渐产生差异。
必须遵守
- 构建前先搜索。 在编写处理通用问题的逻辑之前,先检查是否已有子工作流。按标签(
search_workflows({ tags: ['subworkflow'] })、领域标签)和/或关键词(query: '<keyword>')筛选。标签是发现机制(n8n 2.27.0+)。 Execute Workflow Trigger使用「Define Below」和类型化字段,而不是直通模式。 「Define Below」是唯一能让 Agent 工具(fromAi)和结构化调用方传入值的模式。两种例外情况:(a)子工作流确实需要接收二进制数据(这种情况下,它无法直接接入 Agent 工具),或(b)子工作流完全不接收输入(「Define Below」至少要求一个字段)。请参阅下文的「子工作流输入与输出」。
强烈建议的默认做法
- 任何可复用的内容都应变成子工作流。 如果某个逻辑块有可能在其他地方用到,就将它提取出来。例外情况:简单的包装器(一次 HTTP 调用,不含逻辑)和与当前调用方紧密耦合的逻辑块。
- 纯逻辑默认使用无状态设计(输入 → 输出,不涉及外部状态)。对于会触及状态的逻辑,应有意构建有状态子工作流,并通过清晰的契约封装操作(ORM/仓储模式)。需要避免的是「意外」状态:例如,一个「验证」子工作流悄悄写入日志表。
- 添加标签以便发现:每个子工作流都应添加
subworkflow、领域标签(customer)和/或tool。search_workflows({ tags })会按标签筛选;名称应保持简洁且描述清楚(Parse RFC2822 date)。请参阅references/NAMING_AND_DISCOVERY.md。 - 在描述中包含关键词。 写明输入/输出结构和有代表性的术语,以便通过不同查询找到它。
- 仅在输入契约确实不同时拆分(二进制与 JSON、同步与异步、认证方案不同)。不要用直通模式和内部条件分支,将不同的契约塞进同一个触发器。请参阅
references/SUBWORKFLOW_PATTERNS.md中的「按输入结构拆分」。
决策树:是否应将其设为子工作流?
About to write a chunk of logic?
├── Could this plausibly be needed in another workflow?
│ ├── Yes → extract to sub-workflow
│ └── No → keep inline
│
├── Is this chunk >5 nodes and conceptually one thing?
│ └── Extract if you want testability, isolation, or reuse; if it's only for a cleaner canvas, group it inline instead (see below).
│
├── Is this chunk dealing with a generic concern (auth, retry, parsing, formatting)?
│ └── Almost certainly extract. These are the canonical reusable sub-workflows.
│
└── Is this chunk doing one HTTP call with no logic around it?
└── Don't extract. Extra workflow boundary for nothing.
如果唯一的动机是让画布更整洁(而非复用、隔离、可测试性或用作 Agent 工具),那么默认应使用画布节点组,仅用于提升可读性:速度更快(每次调用无需执行子工作流),也更简单(无需输入/输出契约),逻辑仍保留在原处。若该部分能构成有效的节点组(连通,且只有一个入口和一个出口),就将其分组;否则使用便笺。只有确实需要复用、隔离、独立测试或用作 Agent 工具时,才提取为子工作流。请参阅 n8n-workflow-lifecycle 中的「可读性」。
无状态与有状态子工作流
两者都是一等选择。如何选择取决于设计意图和封装方式。
无状态
接收输入并返回输出。不在输入/输出之外进行 I/O。纯逻辑默认采用这种方式。
示例:
Parse RFC2822 date(标签subworkflow)。输入:日期字符串。输出:ISO 日期或错误。Compute MRR from subscription(标签subworkflow)。输入:订阅对象。输出:MRR 数值。Format invoice as HTML(标签subworkflow)。输入:发票数据。输出:HTML 字符串。
需要再次使用这些逻辑时,直接调用即可,无需担心会触发副作用。
有状态(刻意为之)
在清晰的输入/输出契约背后读取或写入外部状态。类似于仓储模式:子工作流抽象状态操作,让调用方使用领域术语,而不必关注实现。
示例:
Get customer by id(标签customer)。输入:id。输出:customer 对象或{ ok: false, error: 'not_found' }。读取数据库。Write customer billing record(标签customer、billing)。输入:record。输出:{ ok: true, id }。写入数据库。Append audit event(标签audit)。输入:event。输出:{ ok: true, eventId }。写入日志存储。Send to on-call(标签notification)。输入:channel、message。输出:{ ok: true, messageId }。调用 Slack/SMTP。
将这些构建为子工作流的意义:
- 调用方使用领域术语(
get customer by id),而不是存储层术语(SELECT * FROM customers ...)。 - 可以替换其背后的存储/API(Postgres → Supabase、原生节点 → HTTP),而不影响调用方。
- 幂等性、重试和验证由子工作流负责,并集中在一处处理。
要避免的是意外状态:子工作流的名称/描述看似纯操作,却暗中写入日志表。这会让合理地认为可以安全重试或组合操作的调用方措手不及。要么将副作用纳入契约(重命名、记录说明、返回结果),要么将其移出。
何时提取
主要有两个信号:
1. 概念上的连贯性
当一组节点完成一项逻辑任务时,即便没有复用,提取出来也可能有价值,原因包括:
- 可测试性。 使用固定数据单独运行子工作流。
- 可替换性。 更换实现不会影响调用方。
单纯为了提高可读性并不足以成为提取的理由:节点组可以更简单地折叠为一个带标签的框,将五个节点合并为一个(无需子执行,也无需输入/输出契约)。只有在还需要可测试性、可替换性或复用时才提取。
1.5 即发即忘的审计日志模式
此处以审计日志作为即发即忘有状态模式的具体示例。除非用户提出要求,否则不要在工作流中添加审计日志。 这一模式本身(异步触发子工作流,不等待其完成)可推广到任何旁路观测:指标、通知等。
刻意设计为有状态的审计日志子工作流,通过 Execute Workflow 的 waitForSubWorkflow: false 调用,因此调用方不会因写入操作而阻塞。
Caller ──→ [Execute Workflow: DB audit log]
{ title: 'Email Confirmation Received',
description: <serialized data> }
waitForSubWorkflow: false
↓ (caller continues immediately)
──→ [Continue with next step]
子工作流接收标题和描述,写入日志表(或 Slack,或两者),然后返回。调用方不会等待。审计日志是一种旁路观测,不属于关键路径。
如果用户提出了要求,就在每个有意义的状态转换时触发一次(“收到电子邮件确认”“用户已验证”“处理已开始”“已作出资格判定”),这样就能根据日志还原时间线。
它的价值在于:
- 免费获得可观测性。 出现问题时可查看每次执行的时间线。
- 无耦合。 实现方式(数据库、Slack 或两者)可以更改,而不影响调用方。
- 默认异步。
waitForSubWorkflow: false意味着审计日志不会拖慢主工作流。
审计日志工作流正是合适的有状态子工作流。副作用本身就是目的。
1.7 中间件模式
当 webhook 工作流呈现 API 形态时,就按 API 的方式处理。子工作流会成为中间件:在主处理程序之前运行的小型无状态函数,可直接放行,也可提前返回 4xx。
Webhook
→ [Verify JWT] # decode + validate; 401 on failure
→ [Rate limit] # check + bump counter; 429 on failure
→ IF (all middleware ok)
→ Main handler logic
→ Respond 200
→ ELSE → Respond with the 4xx the middleware returned
典型示例:在 n8n 中实现自定义 JWT 身份验证。Verify JWT(标签 subworkflow)接收原始 Authorization 标头,解码并验证签名和有效期,然后返回 { ok: true, user_id } 或 { ok: false, status: 401, message }。调用方通过 IF 节点检查 ok,失败时提前响应,成功时继续执行。
为什么要用子工作流,而不是内联实现:每个需要身份验证的 webhook 都调用同一个子工作流。更换库、轮换签名密钥或添加刷新令牌逻辑,都只需在一个地方完成。复用目标明确、契约简单,而且每个 API 端点的失败响应格式都一致。
搭配 n8n-error-handling 处理 4xx/5xx 响应格式,搭配 n8n-credentials-and-security 处理底层机密信息。
2. 重复模式
你正准备构建一个以前做过的东西。停下来,先搜索。
search_workflows({ query: 'date' })
search_workflows({ tags: ['customer'] })
search_workflows({ tags: ['subworkflow'] })
如果找到了匹配项,就使用它。如果没有,就将其构建为子工作流并添加标签,这样下次搜索就能找到它。标签约定(subworkflow、领域标签、tool)正是实现这一点的关键。
当大部分工作都在子工作流中时,线性长工作流完全没问题
即使工作流有 20 多个节点,只要它主要是对子工作流调用和决策进行线性编排,仍然易于阅读。结构如下(仅展示审计日志节点,是因为它们能直观体现“在实际步骤之间进行旁路观察”;只有用户要求审计日志时才添加):
Webhook
→ Audit log (sub-workflow)
→ Validate
→ Audit log (sub-workflow)
→ IF auth ok
→ Look up user (or sub-workflow)
→ Audit log (sub-workflow)
→ Process step 1 (sub-workflow)
→ Audit log (sub-workflow)
→ Process step 2 (sub-workflow)
→ Audit log (sub-workflow)
→ Decide eligibility (sub-workflow)
→ Audit log (sub-workflow)
→ Send notification (sub-workflow)
→ Respond
每个“逻辑步骤”都是一次子工作流调用。调用方虽然较长,但叙述线性,易于从上到下阅读。逻辑则位于子工作流中。
这并不等同于包含 20 个内联转换的 20 节点工作流,那种工作流难以阅读。上面的模式可行,因为:
- 每个节点只有一个用途(调用特定子工作流)。
- 便笺和节点组标记各个部分(参见
n8n-workflow-lifecycle中的“可读性”)。 - 检查某个部分时,需要打开它所调用的子工作流。这就是封装。
- 无需阅读实现细节,就能看清顶层的编排逻辑。
如果你的工作流有 15 个以上节点,而且并非主要由 Execute Workflow 调用和分支组成,就在值得复用或测试的部分进行拆分,并将其余部分以内联方式分组(节点组)。
不要拆分的情况
- 只有一次 HTTP 调用,没有逻辑。 只有
Execute Workflow → HTTP Request → return的子工作流毫无必要地增加了边界。直接内联即可。 - 与调用方的特定结构紧密耦合。 如果这个片段接收深度嵌套的输入,而这种输入只有当前调用方会生成,那么拆分只会转移耦合关系。先修正数据结构。
- 性能关键的热路径。 每次子工作流调用都会增加延迟(虽然很小,但确实存在)。对于高吞吐量工作流,应先进行性能分析,再添加边界。
构建前搜索流程
当用户描述一个多步骤或听起来较为通用的需求时:
1. search_workflows with relevant tags and/or query (e.g. `tags: ['subworkflow']`, a domain tag, the operation keyword)
2. If candidates appear, fetch get_workflow_details on the top 1-3
3. Confirm fit by reading the inputs/outputs and (briefly) the body
4. If a fit exists → use it. Tell the user "I found `<name>`. Using that."
5. If no fit exists → build new and tag it (`subworkflow`, domain, `tool`) so the next search finds it
“告知用户”这一步很重要。他们可以了解自己的库中已有的内容。
如果你预期会找到的工作流没有出现,最常见的原因是未启用按工作流配置的 MCP 访问权限。请参阅 n8n-workflow-lifecycle 中的 references/MCP_ACCESS_PER_WORKFLOW.md。
子工作流的输入和输出
子工作流由 Execute Workflow Trigger 节点触发。该触发器声明输入架构。调用方通过 Execute Workflow 传递数据,子工作流则返回其最后一个节点的输出。
始终使用带明确字段的“Define Below”
Execute Workflow Trigger 有两种输入模式。默认使用“Define Below”(类型化字段)。 只有这种模式允许 Agent 工具(通过 fromAi())和任何结构化调用方传入值。如果未声明字段,Agent 就没有可填写的架构,子工作流也无法干净地连接为 toolWorkflow。
结构:
const subTrigger = trigger({
type: 'n8n-nodes-base.executeWorkflowTrigger',
config: {
parameters: {
workflowInputs: {
values: [
{ name: 'list_of_ids', type: 'array' },
{ name: 'include_transcript', type: 'boolean' },
{ name: 'session_id', type: 'string' },
],
},
},
},
})
每个已声明的输入都会成为调用方可以填写的类型化参数。在工作流中,可通过 $json.list_of_ids 等方式访问,也可在下游任意位置通过 $('When Executed by Another Workflow').first().json.<field> 访问。
请谨慎选择类型(string、number、boolean、array、object)。模型会将这些类型用作填写 Agent 工具参数时所需的类型,人工在连接调用方时也会依赖这些类型。
例外 1:二进制数据使用直通模式
如果子工作流需要接收二进制数据(图像、文件、PDF),则 Define Below 不适用,因为类型化字段仅支持 JSON。请切换为直通模式:
const subTrigger = trigger({
type: 'n8n-nodes-base.executeWorkflowTrigger',
config: {
parameters: {
inputSource: 'passthrough',
},
},
})
在直通模式下,子工作流会原样接收调用方的项目,包括 binary 槽位。代价是没有类型化输入架构,因此 Agent 工具无法通过 fromAi() 传递参数。对于由其他工作流(而非 Agent)调用且需要传递二进制数据的子工作流,请使用此模式。
对于需要二进制数据且由 Agent 调用的子工作流,请参阅 n8n-binary-and-data references/AGENT_TOOL_BINARY.md(Agent 工具无法直接传递二进制数据)。
例外 2:无输入子工作流使用直通模式
Define Below 至少需要一个已声明字段。确实不接受任何输入的子工作流(例如“列出活动凭据”工具、“当前计数”查询或任何零参数操作)没有地方可放置空架构,因此只能使用直通模式。
仅在无输入场景下使用直通模式时:
- 请在主体开头添加一个处于“Keep Only Set”模式且不含任何字段的
Set(Edit Fields)节点。 这会清除调用方的 JSON,避免下游节点意外读取调用方传入的数据结构中的字段。如果不这样做,主体会悄然获取调用方转发的任何内容。 - 在触发器上添加便笺,说明不需要任何输入。 后续查看者(以及重新将其连接为工具的 Agent)需要知道,使用直通模式并非为了传递二进制数据,而是因为架构有意设为空。
无输入场景下仍可连接 Agent 工具:toolWorkflow 接受输入映射不含任何字段的子工作流。Agent 唯一需要决定的就是是否调用。n8n-agents references/TOOLS.md 中“零个 fromAi 参数”的模式可直接适用。
其他约定
-
在工作流的
description中记录输入和输出。 字段名称、类型、用途。调用方(人类和 Agent)会通过描述了解契约。 -
返回一致的结构。 对于预期内的失败(例如解析错误),返回
{ success: false, error: '...' },而不是抛出异常。调用方可以直接分支处理,无需再用包装逻辑处理错误输出。 -
一旦有调用方,就将输入架构视为契约。 添加可选字段是安全的。重命名或删除字段可以进行,但务必谨慎:列举每个调用方(按子工作流名称使用
search_workflows,并手动扫描),在同一项更改中迁移它们,并在发布前通过validate_workflow+get_workflow_details验证。这里的静默故障很难发现,因为 n8n 不会因无法识别的输入字段而报错。子工作流只会看到undefined,调用方也毫不知情。 -
使用最终的 Set / Edit Fields 节点来整理返回值。 这是可选的,有时是必需的(当最后一个计算节点带有无关字段时),即使并非严格要求,在子工作流中也属于良好实践。它能在边界处明确返回契约,让读者只需查看一个节点就能了解 API。这是对
n8n-expressions中 Set 节点反模式的合理例外:子工作流最后一个节点的隐式使用方是所有调用方,因此 Set 节点作为明确的 API 边界有其价值。将其命名为Return或Return <thing>。 -
返回自然形态,而非存储形态。 如果子工作流管理 Data Table、S3 中的文件或任何存储层,就应向调用方隐藏这些表示形式。数组以数组形式返回,对象以对象形式返回,日期以 ISO 字符串形式返回,无论底层存储的是 JSON 字符串化文本还是其他内部格式。返回契约是接口。存储布局是实现细节。
常见错误:子工作流有一条“新鲜”路径(刚生成的数据,形态自然)和一条“缓存”路径(刚从
_object列读取的数据,仍为字符串化格式)。错误的做法:将新鲜路径字符串化,以便“匹配”缓存路径。正确的做法:解析缓存路径,让两者都返回自然形态。调用方不应需要知道自己拿到的是哪一种。
对于专门接入 Agent 工具的子工作流,请参阅 n8n-agents 中的 references/SUBWORKFLOW_AS_TOOL.md。
调用子工作流:Execute Workflow 模式
除了输入和 workflowId 之外,调用方的 Execute Workflow 节点还有两个设置:
mode默认为'all':子工作流使用全部 N 个项目作为输入,运行一次。与其他工作流一样,项目仍会逐个经过各节点。设置mode: 'each'可让子工作流分别运行 N 次,每次执行处理一个项目。对于仅按常规方式处理项目的子工作流,两种模式等效。如果子工作流的主体假定恰好只有一个项目(例如每次运行聚合、“要操作的就是这个客户”的逻辑,或应对每个输入只执行一次的最终写入),那么两种模式就有区别。mode: 'each'符合这种假设,mode: 'all'则会破坏它。当确实需要逐项迭代时,优先使用mode: 'each',而不是在子工作流内部使用 Loop Over Items 节点。waitForSubWorkflow默认为true。设置options.waitForSubWorkflow: false会触发调用并立即继续,子工作流则在后台运行。调用方下游不会收到返回数据。
mode: 'each' + waitForSubWorkflow: false 是 n8n 提供的唯一真正的并行化方式:派发 N 次子工作流执行且不等待,让它们并发运行(仍受每个实例的并发限制和每次调用的开销约束)。适用于“启动 N 个独立任务,之后再轮询或汇总”的场景。例如:为每个项目派发一个长时间运行的任务,在 Data Table 中跟踪每项任务,然后循环检查,直到所有行都标记为已完成或超时。
有关触发后即忘模式的轮询方式,请参阅 references/SUBWORKFLOW_PATTERNS.md 中的“触发后即忘并行化”。
参考文件
| 文件 | 阅读时机 |
|---|---|
references/SUBWORKFLOW_PATTERNS.md | 了解 mode: 'all' 与 'each' 的默认设置、根据输入形状拆分(binary/passthrough 与 Define Below)、通过 Data Table 轮询实现即发即弃式并行处理 |
references/NAMING_AND_DISCOVERY.md | 了解如何命名和标记新的子工作流、搜索现有子工作流以及标签约定 |
反模式
| 反模式 | 问题 | 修复方法 |
|---|---|---|
| 在三个工作流中重复相同的日期解析节点 | Bug 修复只改了两个地方,漏掉第三个 | 一次性提取为单个 Parse <format> date 子工作流(标签为 subworkflow) |
| 未搜索就构建新的子工作流 | 库中会出现重复项,今后搜索也会找到多个结果 | 始终先使用 search_workflows |
| 子工作流名称或描述看起来是纯操作,却悄悄写入日志表 | 调用方无法判断重试或幂等性,还会遭遇意料之外的副作用 | 要么将副作用纳入约定(重命名、记录说明并返回结果),要么将其移出 |
子工作流没有 description | 今后的搜索无法找到它,也没人知道它的用途 | 设置 description,说明输入/输出形状和用途 |
子工作流命名为 Helper 3 | 名称无法说明其用途 | 使用动词开头的描述性名称(Parse RFC2822 date),参见 n8n-workflow-lifecycle NAMING_CONVENTIONS.md |
| 子工作流没有标签 | 使用任何 tags 筛选器都无法找到它,以后的你也找不到 | 创建后立即通过 update_workflow addTags 为其添加标签(subworkflow、领域、tool) |
Execute Workflow Trigger 设为 passthrough,但既不处理 binary,也不是有意设计为零输入 | 没有类型化架构,Agent 工具就无法通过 fromAi 填写参数,结构化调用方也无法顺畅传入值 | 使用“Define Below”,并通过 workflowInputs.values 声明每个字段的名称和类型。例外情况是接收 binary 的子工作流,以及确实不接收任何输入的子工作流(见“例外 2”) |
| 零输入子工作流使用 passthrough 触发器,却没有用于清除输入的 Set 节点和说明性便笺 | 主体会悄悄读取调用方转发的杂散字段;今后的读者会误以为 passthrough 是用于 binary 的 | 在主体顶部添加一个 Set(“Keep Only Set”,不设字段),并在触发器上添加便笺,说明不需要输入 |
| 将需要 binary 输入的子工作流用作 Agent 工具 | Agent 工具无法直接传递 binary | 查看 n8n-binary-and-data AGENT_TOOL_BINARY.md,了解正确模式 |
| 30 个节点的工作流没有提取任何子工作流 | 难以阅读、测试和替换 | 将逻辑部分提取为子工作流 |