## 创建 Trigger 与自动运行

<!-- slot: 目标 -->
第 8 章已经用精确版本手动跑通 Bug 分诊，本章把同一版本部署成自动 Trigger。你会创建一条按 `Asia/Shanghai` 每天 9:00 触发的 Binding，理解固定输入、不可变 Binding revision、启用与停用，并能从自动产生的 Run 反查 Trigger。读完后，你可以让流程按稳定配置自动运行，同时知道修改 Plan 默认版本不会让现有 Trigger 漂移。

<!-- slot: 边界 -->
当前自动触发模型里，**Binding 是持久部署实体，Trigger 是面向 Agent 的公共操作名称**；公共入口是 `loop exec workflow trigger`，没有 `workflow binding` 动作。当前 source 只支持 schedule，模式为 `cron`、`interval`、`absolute`、`relative`，不支持 webhook、仓库事件或任意外部事件源。本章以 cron 为主；Run 内部节点语义仍由前面章节的 Plan 决定。

<!-- slot: 提示词 -->
创建 Trigger 不需要重新生成 Plan，也不能把“每天取新 Bug”寄希望于 schedule。先准备第 7 章已发布或批准的真实 `planId` / `planVersionId`，以及第 5 章四项顶层输入对应的固定 Workspace 资源。示例 ID 都是脱敏占位；只对命令做 schema 与配置验证，不实际创建会定时发消息或发审批的部署。

若每天的 Bug 内容会变化，推荐把 `bug_summary` 改成稳定的批次说明，并让 Plan 内的分诊 Agent 经 App / Extension 查询当天数据。schedule 自身只会重复提交 revision 中冻结的输入，不会在到点时替你查询工单系统。

<!-- slot: 流程 -->
一条自动部署按以下顺序上线：

1. 用公共 `validate` 同时检查精确 PlanVersion、schedule source 和固定输入，直到 `diagnostics=[]`。
2. 用 `create` 创建 Binding 和第一个不可变 revision，先令 `enabled:false`，不要边写边上线。
3. 用 `show` 核对 Trigger aggregate 的 `revision`，以及 revision snapshot 中的 `planVersionId`、schedule、inputs 与 digest。
4. 用 `enable` 指定要激活的 Binding revision，并带最新 Trigger aggregate `expectedRevision`。
5. 到点后在 Desktop **Runs** 列表当前已加载的分页中按 Trigger 名称找到自动 Run；名称搜索只过滤已加载项，目标不在当前页时要先点 **Load More**。随后像第 8 章一样看 Graph、Timeline 与 State。
6. 维护或排障时先 `disable`，再追加 revision；复核后显式激活目标 revision。

先校验每天 9:00 的配置。这里把四项值全部放进 `schedule.inputs`，它们会成为每次 schedule Invocation 的固定输入：

```bash
$ printf '%s\n' '{"action":"validate","planId":"pln_bug_triage","planVersionId":"plv_bug_triage_v1","source":{"type":"schedule","schedule":{"mode":"cron","cronExpression":"0 9 * * *","timezone":"Asia/Shanghai","inputs":{"destination":{"kind":"channel","scopeId":"chn_bug_ops_demo"},"bug_summary":"查询并分诊当天新 Bug","triage_agent":"agt_triage_demo","approver":"mem_approver_demo"}}}}' | loop exec workflow trigger -
```

公共 validate 会按该精确 PlanVersion 的输入契约检查字段、类型与必填项，成功时返回 `diagnostics=[]` 和 digest。它不创建 revision、不激活 schedule，也不验证第三方 App / Extension 凭证。

cron 使用五段式表达式；时区必须写 `UTC` 或明确的 IANA 名称，不能写依赖调度主机的 `Local`。其它三种模式互斥：interval 需要正数 `intervalSeconds` 与明确 `anchorAt`；absolute 需要 `scheduledAt`；relative 需要正数 `relativeSeconds`。absolute 和 relative 是一次性 schedule，接受该次 occurrence 后 source 完成；cron 与 interval 会继续计算下一次。

<!-- slot: 配置 -->
校验干净后创建但暂不启用：

```bash
$ printf '%s\n' '{"action":"create","key":"daily-bug-triage","displayName":"每日 Bug 分诊","descriptionMd":"每天 09:00 自动发起分诊","planId":"pln_bug_triage","planVersionId":"plv_bug_triage_v1","source":{"type":"schedule","schedule":{"mode":"cron","cronExpression":"0 9 * * *","timezone":"Asia/Shanghai","inputs":{"destination":{"kind":"channel","scopeId":"chn_bug_ops_demo"},"bug_summary":"查询并分诊当天新 Bug","triage_agent":"agt_triage_demo","approver":"mem_approver_demo"}}},"enabled":false}' | loop exec workflow trigger -
```

`loop exec` 的业务响应位于外层 `data` 中，同时包含 `data.trigger` 与 `data.revision`。保存 `data.trigger.id`、`data.trigger.revision`、`data.revision.revision` 和 `data.revision.planVersionId`；前两个 revision 含义不同：`data.trigger.revision` 是可变 aggregate 的并发版本，作为后续 `expectedRevision`；`data.revision.revision` 是不可变部署快照的序号，用于选择要激活哪一版。

用 `show` 重新读取权威状态：

```bash
$ printf '%s\n' '{"action":"show","triggerId":"wbd_daily_bug_triage"}' | loop exec workflow trigger -
```

确认 `data.trigger.state=disabled`、`data.trigger.latestRevision.revision=1`，并检查返回的 `data.revision` 部署快照是否钉定预期 PlanVersion、cron、时区和 inputs。公共 `show` 优先返回 Latest revision 的完整 snapshot；若没有 Latest 才回退 Active。追加新 revision 后，必须结合 `data.trigger.latestRevision` 与 `data.trigger.activeRevision` 引用判断“最新配置”和“正在运行配置”，不能只看这个 snapshot 猜 Active。Desktop 详情页会读取并展示精确 Active revision。然后显式激活：

```bash
$ printf '%s\n' '{"action":"enable","triggerId":"wbd_daily_bug_triage","revision":1,"expectedRevision":1}' | loop exec workflow trigger -
```

每次 `enable` 都会创建新的 mutable source reservation，以新的 source ID 和递增的 `activation_number` 形成 source generation；即使重新启用同一 Binding revision 也不会复用旧 source。relative schedule 会从这次启用重新锚定延迟。激活还会令 Trigger 进入 `enabled`，并把 `activeRevision` 指向指定的不可变 revision。它不会追随 Plan 的 Current / Default；以后即使发布 Plan v2，现有 Trigger 仍运行 revision 中保存的 v1。

按 canonical Workflow 规范，Binding 不增加额外的输入层；本章命令只使用公共接口同样接受的共同子集。最终输入优先级只有 **Plan defaults < schedule occurrence input**。示例把本次部署要覆盖的值写入 `schedule.inputs`，没有覆盖的可选项才使用被钉定 PlanVersion 的默认值；合并结果必须满足全部必填输入和声明类型。

<!-- slot: 异常 -->
常见故障都应先 `show`，不要盲目重复 mutation：

- `binding_revision_conflict`：`expectedRevision` 已过期。重新 `show`，确认他人修改，再用最新 aggregate revision 重做原操作。
- `binding_validation_failed` 或 diagnostics 非空：按 `code` / `path` 修 source 或输入。常见有 `binding.input.required`、`binding.schedule_cron`、`binding.schedule_timezone`。
- 到点没有新 Run：确认 `state=enabled`、`activeRevision` 正确、owner 成员仍 active，并核对时区。计划版本或 owner 暂时不可用时，当前 occurrence 会被阻塞，cursor 不推进，后续重试仍处理同一 occurrence。对 cron / interval，长期阻塞期间积累的到期 occurrence 不会自动跳过；恢复后虽然每轮处理数量有界，仍可能快速补跑一批历史周期。恢复 owner 或版本前先评估积压与下游容量，不希望补跑时应先 `disable`，不要靠重复创建 Trigger 恢复。
- 自动 Run 行为还是旧版：这是版本钉定的预期结果。追加并激活一个指向新 PlanVersion 的 Binding revision，不能只移动 Plan Default。

修改部署配置使用 `update`。它不是原地覆盖，而是追加不可变 revision；且**追加后不会自动激活**。公共 CLI 当前也没有单独的 revision list / get 或 restore 动作。Desktop 详情页目前只加载前 50 条 revision，且不会继续消费 `nextCursor`，因此只能查看当前已加载历史与精确 Active revision，不能把它当作完整历史浏览器；Agent 自动化应保存每次 mutation 回执中的 revision 信息。恢复归档 Trigger 可使用 Desktop：

```bash
$ loop exec workflow trigger - <<'JSON'
{"action":"update","triggerId":"wbd_daily_bug_triage","expectedRevision":2,
 "planVersionId":"plv_bug_triage_v2","source":{"type":"schedule","schedule":{
  "mode":"cron","cronExpression":"30 9 * * *","timezone":"Asia/Shanghai",
  "inputs":{"destination":{"kind":"channel","scopeId":"chn_bug_ops_demo"},
   "bug_summary":"查询并分诊当天新 Bug","triage_agent":"agt_triage_demo",
   "approver":"mem_approver_demo"}}}}
JSON
```

响应中的新 `data.revision.revision` 只是 Latest；旧 Active 仍继续执行。假设回执显示 `data.trigger.revision=3`、`data.revision.revision=2`，显式切换：

```bash
$ printf '%s\n' '{"action":"enable","triggerId":"wbd_daily_bug_triage","revision":2,"expectedRevision":3}' | loop exec workflow trigger -
```

这次 `enable` 会关闭旧 source generation，并为 revision 2 新建 source ID 与下一 `activation_number`；不会改写已经接受的 Invocation / Run。

维护窗口应停用 Trigger：

```bash
$ printf '%s\n' '{"action":"disable","triggerId":"wbd_daily_bug_triage","expectedRevision":4}' | loop exec workflow trigger -
```

disable 关闭 active source 并阻止新的 schedule Invocation，不会取消此前已经接受的 Run。archive 也会关闭 source 并隐藏常规列表，但公共 CLI 当前没有 restore；Desktop 恢复后状态是 disabled，仍要显式 Enable。归档 Plan 本身也不能替代 Trigger disable：已经激活的 Binding 仍钉定旧版本，可继续产生 Run。

<!-- slot: 审批 -->
创建或更新 Trigger 不会重新发起第 7 章的 Team Plan 发布审批；它只能选择已经发布的精确 PlanVersion。Binding 在创建时保存一个不可替换的人类 execution owner，Agent mutation 的 owner 来自该 Agent 当时的管理者，不能从 JSON 伪造。之后只有这个 owner 能追加、激活、停用或归档；更换 Agent manager 不会转移 immutable execution owner，新 manager 通过 Agent 发起 mutation 会因 owner 不匹配而失败，当前也没有 transfer 动作。owner 成员失效还会阻塞新的 schedule occurrence。Run 到达 `request_approval` 后仍是业务审批，与部署权限是两条独立门禁。

<!-- slot: 外部系统 -->
schedule 不是数据采集器。它不会在 9:00 调 Jira、GitHub 或内部工单 API，也不接受随 occurrence 临时变化的调用方输入；它只使用不可变 revision 中的 `schedule.inputs`，再按规范覆盖 Plan defaults。需要动态数据时，让 Plan 内 Agent 通过获授权的 App / Extension 查询，并在 Run 中留下可审计结果。Trigger source、inputs、description 与 Run payload 都不得包含 token 或密码。

schedule dispatcher 对 occurrence 使用稳定身份，接受 Invocation 与推进 cursor 在同一事务中完成；重复调度会重放同一已接受 occurrence，而不是有意创建两次 Run。精确 Binding ID / revision、source ID / activation、occurrence 与 PlanVersion 来源快照保存在自动 Run 对应的 **Invocation**；Run 本身只关联 Invocation，不持有这些 Binding 字段。当前 Runs UI 只显示按当前 Binding 行关联出的名称 / key；Plan 的 Invocation history 也只展示 schedule / manual 来源类型、状态、接受时间与 Invocation / Run ID 摘要，二者都不能证明启动时的精确 revision、source 或 occurrence。要审计“哪份部署在什么时刻启动”，须从 Invocation API / 数据模型读取该来源快照；当前 Desktop 没有展示精确 tuple 的 Invocation detail 页面。第三方副作用仍需遵守第 8 章的幂等与补偿边界，不能把调度重放等同于跨系统 exactly-once。

<!-- slot: 验收 -->
- [ ] 能解释 Binding、Trigger 公共动作、Binding revision 与 source generation 的区别。
- [ ] 能校验并创建一条钉定精确 PlanVersion、时区明确、输入完整的 disabled Trigger。
- [ ] 能从 `show` 区分 Trigger aggregate revision、Latest revision 与 Active revision。
- [ ] 能追加新 revision，并确认它只有显式 `enable` 后才接管自动运行。
- [ ] 能停用 Trigger、从 Desktop Runs 观察自动 Run，并说明 disable 不取消已接受 Run。

<!-- slot: 媒体槽位 -->
<!-- media: fig-09-01-trigger-binding-lifecycle.png -->
图 9-1 Trigger / Binding 生命周期。场景：每日 Bug 分诊从 disabled revision 1 经 Enable 产生自动 Run，再追加 revision 2 并显式切换；前置数据：一个已批准的精确 PlanVersion、四项脱敏固定输入和有权限的 execution owner。画面应同时展示 Trigger state、aggregate revision、Latest / Active revision、钉定的 PlanVersion、Asia/Shanghai cron、Runs 列表中的当前 Binding 名称，以及相邻 Plan Invocation history 中的 schedule 来源类型、状态和 Invocation / Run ID 摘要；用注释说明精确 revision / source / occurrence tuple 仅可经 Invocation API / 数据模型读取，当前 UI 不展示这些字段。脱敏要求：替换真实 Workspace、Binding、Plan、版本、owner、成员、Agent、频道、Invocation 与 Run ID，隐藏凭证和 Bug 内容。
