mcpField
N
n8n-subworkflows-official

n8n-subworkflows-official

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

关于 n8n-subworkflows-official

n8n-subworkflows-official 为 n8n 工作流设计提供可复用子工作流的实践指导,面向构建、维护复杂自动化的开发者。内容侧重接口约定、状态管理、复用发现与何时抽取逻辑。

主要功能

  1. 01复用与抽取决策指导
  2. 02输入输出契约设计
  3. 03无状态与有状态模式
  4. 04标签和关键词发现
  5. 05中间件与异步调用模式

适用场景

  1. 01在多个工作流间复用通用逻辑
  2. 02隔离步骤以便单独测试和替换
  3. 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 时需要修改多个地方,漏掉一个地方,就会导致这些“相同”的副本逐渐出现差异。

不可妥协的要求

  1. 构建之前先搜索。 在编写处理通用问题的逻辑之前,先检查是否已有子工作流。按标签(search_workflows({ tags: ['subworkflow'] })、领域标签)和/或关键词(query: '<keyword>')筛选。标签是发现工作流的机制(n8n 2.27.0+)。
  2. 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.mdmode: '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 个节点的工作流没有任何拆分难以阅读、测试和替换将逻辑部分提取为子工作流

评论

支持 Markdown