mcpField
N
n8n-subworkflows

n8n-subworkflows

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

关于 n8n-subworkflows

n8n-subworkflows 是用于设计可复用 n8n 子工作流的技能,帮助 Agent 搜索现有实现、定义清晰的输入输出契约,并判断何时为复用、测试或隔离而提取逻辑。

主要功能

  1. 01构建前先搜索
  2. 02类型化输入契约
  3. 03无状态与有状态模式
  4. 04用于发现的标签
  5. 05提取时机指南

适用场景

  1. 01在多个 n8n 工作流中复用逻辑
  2. 02创建可独立测试的工作流步骤
  3. 03封装数据库或 API 操作
  4. 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 时需要修改多个地方,可能会漏掉一个,导致这些「相同」的副本逐渐产生差异。

必须遵守

  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/仓储模式)。需要避免的是「意外」状态:例如,一个「验证」子工作流悄悄写入日志表。
  • 添加标签以便发现:每个子工作流都应添加 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 个节点的工作流没有提取任何子工作流难以阅读、测试和替换将逻辑部分提取为子工作流

评论

支持 Markdown