模块化与复用
sub_workflow、把一段逻辑封装成模块、Promote / Demote 一键重构 —— 让工作流不再一条条复制粘贴。
当你做到第二、第三条工作流时,会发现某段逻辑反复出现 —— 拉 ASA 数据、发 Slack 消息、生成 Excel 报表。再复制粘贴下去,改一处要改三处、测试重复、版本失控。这时候就该做模块化了。
模板 = 起始文档的快照,克隆后独立演化;模块 = 可被引用的子流程,升级后调用方自动受益。两者不是替代关系,是互补的。本页讲模块。
什么是「模块」
一个"模块"本质上就是一个声明了 WorkflowModuleContract 顶层字段的 workflow。它额外公开了:
- inputs — 调用方必须传入的参数(带名称、类型、必填、默认值、描述)
- outputs — 执行完产生的结构化结果(调用方可直接引用)
- version — 语义化版本号,便于调用方锁定
name: my-team-slack-notifier
version: "1.0.0"
module:
enabled: true
inputs:
webhook_url:
type: string
required: true
description: Slack incoming webhook URL
channel:
type: string
required: false
default: "#general"
message:
type: string
required: true
mentions:
type: array
items: { type: string }
required: false
outputs:
message_ts:
type: string
description: Slack 返回的 message timestamp,可用于后续更新消息
status:
type: string
enum: [sent, failed]
# 模块自己的步骤(内部细节,对调用方黑盒)
steps:
- id: format_text
type: code
language: node
code: |
const msg = process.env.WF_VAR_MESSAGE;
const mentions = JSON.parse(process.env.WF_VAR_MENTIONS || '[]');
return { text: mentions.map(u => `<@${u}>`).join(' ') + ' ' + msg };
- id: post
type: code
depends_on: [format_text]
credentials: [slack_oauth]
code: |
// 调 Slack API,返回 message_ts / status …有了契约之后,调用方(父 workflow)的 sub_workflow step 会:
- 在保存时静态校验 —— 必填 inputs 缺一项就不让保存;
- 在运行时校验 —— 类型不符 / 枚举不在列表里,直接报错;
- 在编辑器里自动补全 —— 你写 inputs: 时能下拉看到模块公开了哪些字段。
父工作流如何调用模块
steps:
- id: build_report
type: code
# ...
- id: notify
type: sub_workflow
module: my-team-slack-notifier # 引用我们上面定义的模块
version: "^1.0.0" # 锁主版本
inputs:
webhook_url: "{{credentials.slack_webhook}}"
channel: "#ops-alerts"
message: "日报已生成:{{steps.build_report.data.summary}}"
mentions: ["U1234", "U5678"]
- id: record
type: code
depends_on: [notify]
code: |
const ts = process.env.STEP_OUTPUT_NOTIFY_MESSAGE_TS;
// 把 ts 存到你自己的数据库,方便后续更新那条消息 ...要点:
module: dingyue-module-slack-deliver是模块 id,平台会按名字 + version 解析。inputs:和模块契约里的 inputs 一一对应。- 调用完成后,可通过
{{steps.deliver.outputs.message_id}}读模块输出。
让已有工作流变成模块:Promote to module
大部分时候你不会从零写一个模块,而是从现有工作流里抽。流程:
- 打开一个你觉得"这段逻辑值得复用"的工作流。
- 在画布里用鼠标框选或 Shift+点击选中若干连续 step。
- 右键菜单 →「Promote to module」,或顶部工具栏同名按钮。
- 弹窗里:
- 起一个有意义的模块名字(全局唯一,建议前缀你团队名,例:my-team-slack-notifier);
- 平台会自动根据选中片段推断 inputs / outputs 初稿,你可以编辑;
- 确认后点「创建模块」。
- 原工作流里这几步会被替换成一个 sub_workflow 节点 —— 参数和连线自动对齐。
就像 IDE 里的"提取函数"重构。你选一段代码,IDE 帮你抽成一个带参数和返回值的函数,并把原位置替换成调用。Braidrun 做的是完全一样的事 —— 只不过对象是 workflow step。
模块升级与版本兼容
每次保存一个模块都会创建一个新版本。父 workflow 在 sub_workflow step 里可以选择:
version: latest— 自动用最新版(适合你自己维护的模块)version: "1.2.0"— 锁定到具体版本(适合引用别人的模块、怕升级破坏兼容)version: "^1.2.0"— 允许小版本升级,禁止跨大版本
什么是"兼容的变更"?
- ✓ 加一个 optional input(必填 / 默认值都不算破坏)
- ✓ 加一个 output 字段
- ✗ 删 / 改名 input
- ✗ 删 / 改名 output
- ✗ 改 input 类型
做破坏兼容的改动时,语义版本应该升到下一个 MAJOR。平台不会强制,但调用方锁 ^X.Y.Z 时会受保护。
环路检测
模块 A 不能间接引用自己。平台在两层做了检测:
- 发布时静态检测 — 保存模块时,DFS 扫所有它引用的其它模块的依赖图,有环则拒绝保存。
- 运行时动态检测 — 执行期间如果出现 runtime 解析到的环(譬如条件分支后才被引用),会立刻抛错。
最大递归深度默认 8 层。这是"父调子调孙"的深度,不是单次调用的 fanout。
变量隔离
模块跑起来是一个"子 WorkflowExecutionContext",和父上下文完全隔离:
- 父的 variables / steps outputs / agents 默认不继承给子;
- 子的内部 step 输出也不会溢出到父 —— 父只能看到契约里声明的 outputs;
- 跨父子传数据的唯一方式是 inputs / outputs(显式契约)。
这种隔离是模块化可靠性的基石 —— "黑盒"不是比喻,是字面意义上的。
Demote from module:把模块还原为内联 step
如果你改着改着发现某段逻辑其实只有一处在用,做成模块反而增加了理解成本,可以反向 Demote:
- 在父 workflow 里选中那个 sub_workflow 节点;
- 右键 →「Demote inline」;
- 平台把模块的 step 展开回父 workflow,sub_workflow 节点消失。
原模块本身不会被删除 —— 如果还有其它工作流引用它,那些仍然有效。
在哪里管理所有模块
左侧主导航「模块库」是你的总管视图。这里可以:
- 看所有模块列表(自带的 12 个 + 你团队创建的)
- 按 "谁在引用我" 反向查询 —— 改模块前先看谁会受影响
- 看模块的版本历史 / diff
- 发布 / 归档 / 转移所有权
什么时候该做模块
- ✓ 同一段逻辑在 2 个以上 workflow 里出现过
- ✓ 一段逻辑稳定且对外有清晰契约(典型的 "发消息 / 查报表 / 生成 Markdown")
- ✓ 一个单 workflow 已经超过 15 个 step,可读性下降
- ✗ 一段逻辑只用一次且未来不会复用 —— 直接内联
- ✗ 一段 2–3 行的小工具函数 —— 用 code step 的代码复用比模块更自然