关于 n8n-subworkflows-official
n8n-subworkflows-official 为 n8n 工作流设计提供可复用子工作流的实践指导,面向构建、维护复杂自动化的开发者。内容侧重接口约定、状态管理、复用发现与何时抽取逻辑。
主要功能
- 01复用与抽取决策指导
- 02输入输出契约设计
- 03无状态与有状态模式
- 04标签和关键词发现
- 05中间件与异步调用模式
适用场景
- 01在多个工作流间复用通用逻辑
- 02隔离步骤以便单独测试和替换
- 03为 Webhook 添加验证或限流流程
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ n8n-io/ skills --skill n8n-subworkflows-official安装后在 Claude Code 中输入/n8n-subworkflows-official确认已加载。
附带文件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/仓库模式)。应避免的是意外状态:例如,一个“validate”子工作流悄悄写入了日志表。
- 添加标签以便发现:每个子工作流都添加
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 工具),那么画布节点组才是仅用于提高可读性的分组工具:它速度更快(每次调用不会额外执行子工作流),也更简单(无需输入/输出契约),逻辑仍保留在原处。将其放在连接所在的区域,不包含触发器,并且每个方向最多连接一个成员(n8n-workflow-lifecycle-official 的 references/NODE_GROUPS.md);如果某个区域分支过多,应将其拆成更小的有效片段,而不是让整个阶段都不分组(便签只能添加注释,不能分组)。只有在确实需要复用、隔离、独立测试或作为 Agent 工具时,才提取为子工作流。参见 n8n-workflow-lifecycle-official 中的 Readability。
无状态与有状态子工作流
两者都是一等选择。如何选择取决于意图和封装需求。
无状态
接收输入并返回输出。不进行输入/输出之外的 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 }。调用方根据 ok 执行 IF 判断;失败时提前响应,成功时继续执行。
为什么使用子工作流而不是内联实现:每个需要身份验证的 Webhook 都会调用同一个子工作流。更换库、轮换签名密钥或添加刷新令牌逻辑,都只需在一个地方完成。复用目标明确,契约简洁,而且所有 API 端点的失败响应结构保持一致。
可与 n8n-error-handling-official 搭配使用,以处理 4xx/5xx 响应结构;也可与 n8n-credentials-and-security-official 搭配使用,以处理底层机密信息。
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-official中的“Readability”)。 - 检查某个章节时,只需打开它调用的子工作流。这就是封装。
- 无需阅读具体实现,就能看清顶层的编排逻辑。
如果你的工作流远超 7 个顶层方框,且大部分内容并非 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-official references/MCP_ACCESS_PER_WORKFLOW.md。
子工作流的输入和输出
子工作流由 Execute Workflow Trigger 节点触发。触发器声明输入架构。调用方通过 Execute Workflow 传递数据,子工作流则返回其最后一个节点输出的内容。
始终使用带有明确字段的“在下方定义”
Execute Workflow Trigger 有两种输入模式。默认使用“在下方定义”(类型化字段)。 只有此模式允许 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),则“在下方定义”无法使用,因为类型化字段仅支持 JSON。请切换为直通模式:
const subTrigger = trigger({
type: 'n8n-nodes-base.executeWorkflowTrigger',
config: {
parameters: {
inputSource: 'passthrough',
},
},
})
在直通模式下,子工作流会原样接收调用方的项目,包括 binary 插槽。代价是没有类型化输入架构,因此 Agent 工具无法通过 fromAi() 传递参数。对于由其他工作流(而非 Agent)调用且需要传递二进制数据的子工作流,请使用此模式。
对于需要二进制数据且由 Agent 调用的子工作流,请参阅 n8n-binary-and-data-official references/AGENT_TOOL_BINARY.md(Agent 工具无法直接传递二进制数据)。
例外 2:无输入子工作流使用直通模式
“在下方定义”要求至少声明一个字段。真正不接收任何输入的子工作流(例如“列出活跃凭据”工具、“当前计数”查询或任何零参数操作)没有地方放置空架构,因此只能使用直通模式。
在专门针对无输入情况使用直通模式时:
- 在正文开头添加一个
Set(Edit Fields)节点,将其设为“仅保留已设置”模式,且不设置任何字段。 这样可以清除调用方的 JSON,避免下游节点意外读取调用方传入的任意结构中的字段。否则,正文会悄悄接收调用方转发的内容。 - 在触发器上添加一条便笺,说明不需要任何输入。 后续查看者(以及将其重新接线为工具的 Agent)需要知道,使用直通模式并不是为了传递二进制数据,而是因为架构本来就是空的。
无输入情况下仍可接入 Agent 工具:toolWorkflow 接受输入映射中没有字段的子工作流。Agent 唯一需要决定的是是否调用它。n8n-ai-nodes-official references/TOOLS.md 中的模式(“零个 fromAi 参数”)可直接适用。
其他约定
-
在工作流
description中记录输入和输出。 包括字段名称、类型和用途。调用方(人和 Agent)会通过说明了解契约。 -
返回一致的结构。 对于预期的失败(例如解析错误),返回
{ success: false, error: '...' },而不是抛出异常。调用方无需包装错误输出,即可根据结果进行分支处理。 -
一旦有调用方,就应将输入架构视为契约。 添加可选字段是安全的。重命名或删除字段可以进行,但必须谨慎:列出每个调用方(通过
search_workflows搜索子工作流名称,并手动扫描),在同一项更改中完成迁移,并在发布前使用validate_workflow和get_workflow_details进行验证。这里的静默故障很难发现,因为 n8n 不会对无法识别的输入字段报错。子工作流只会看到undefined,调用方却毫不知情。 -
使用末尾的 Set / Edit Fields 节点整理返回内容。 这是可选的;有时是必需的(当最后一个计算节点带有无关字段时),即使并非严格要求,对子工作流来说也是一种良好实践。这样可以在边界处明确返回契约,让读者只需查看一个节点就能了解 API。这是对
n8n-expressions-official中 Set 节点反模式的合理例外:子工作流最后一个节点的隐式使用者是所有调用方,因此 Set 节点作为明确的 API 边界是有必要的。将其命名为Return或Return <thing>。 -
返回自然形态,而不是存储形态。 拥有 Data Table、S3 文件或任何存储层的子工作流,都应向调用方隐藏其表示形式。数组应以数组返回,对象应以对象返回,日期应以 ISO 字符串返回,而不受底层存储采用 JSON 字符串化文本或其他内部格式的影响。返回契约是接口。存储布局是实现细节。
常见失误:子工作流有一条“新鲜”路径(刚生成的数据,形态自然)和一条“缓存”路径(刚从
_object列读取的数据,仍为字符串化形式)。错误的做法:将新鲜路径字符串化,以“匹配”缓存路径。正确的做法:解析缓存路径,让两者都返回自然形态。调用方不应需要知道自己拿到的是哪种结果。
对于专门配置为 Agent 工具的子工作流,请参阅 n8n-ai-nodes-official 中的 references/SUBWORKFLOW_AS_TOOL.md(AI Agent 节点),或 n8n-agents-official 中的 references/TOOLS.md(n8n 一等 Agent)。一等 Agent 的工作流工具还必须与 Agent 位于同一项目中并已发布,而且其 fixed / expression 输入无法读取调用方的触发器数据。
调用子工作流:Execute Workflow 模式
除 inputs/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 中的“Fire-and-forget parallelization”。
参考文件
| 文件 | 何时阅读 |
|---|---|
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-official NAMING_CONVENTIONS.md |
| 子工作流未添加标签 | 它不会出现在任何 tags 筛选结果中,以后的你也找不到它 | 创建后立即通过 update_workflow addTags 添加标签(subworkflow、领域、tool) |
不处理二进制数据且并非有意设为零输入,却将 Execute Workflow Trigger 设为 passthrough | 没有类型化架构,Agent 工具就无法通过 fromAi 填写参数,结构化调用方也无法顺利传递值 | 使用“Define Below”并声明 workflowInputs.values(为每个字段指定名称和类型)。例外情况是接收二进制数据的子工作流,以及确实不需要任何输入的子工作流(参见“Exception 2”) |
零输入子工作流使用 passthrough 触发器,却没有 Set 清理节点和说明便签 | 正文会静默读取调用方转发的任意字段;日后的读者会误以为 passthrough 是用于二进制数据的 | 在正文顶部添加一个 Set(“Keep Only Set”,不设字段),并在触发器上添加便签,注明无需输入 |
| 将需要二进制输入的子工作流用作 Agent 工具 | Agent 工具无法直接传递二进制数据 | 查看 n8n-binary-and-data-official AGENT_TOOL_BINARY.md,了解正确模式 |
| 30 个节点的工作流没有任何拆分 | 难以阅读、测试和替换 | 将逻辑部分提取为子工作流 |