Paperclip 流水线(pipelines)与例行任务(routines):阶段、关卡、触发器怎么配

2026-08-17

让一群 Agent 各自领任务不难,难的是把「一件事从头走到尾」这件事本身固化下来:谁先做、卡在哪儿要人点头、上游改了下游怎么知道、一批子活儿全干完了父任务能不能自己往前走。

Paperclip 把这件事拆成了两个东西:pipelines 负责「阶段与关卡」,routines 负责「什么时候起头」。这两个词经常被混着说,但它们在文档里是两套接口、两套配置,配错位置的结果是流程根本不动。

下面把官方的 pipelines 教程和 routines 接口文档对着读一遍,重点是配置项长什么样、关卡靠哪个字段生效、哪些行为会直接给你甩个 409。文中的命令和字段全部照录官方文档,我们没有安装运行过这套系统,界面长什么样、点哪里,本文一概不谈。

先把 pipeline、case、routine 三个词分开

  • pipeline:一条流程定义,里面是一串 stage(阶段)。
  • case:在流水线上跑的一个具体事项。case 之间可以有父子关系(parentCaseId),也可以有阻塞关系(blockedByCaseKeys)。
  • routine:例行任务。文档的定义是「按计划、webhook 或 API 调用触发,为指定 Agent 创建一次 heartbeat run」。它可以被挂到某个 stage 上作为自动化。

教程里用了三条互相串联的流水线来演示:release-coverage(一个发布 case,回答「这个版本的内容覆盖到了没有」)、feature-content(每个通过评审的特性一个 case)、content-production(每个内容件一个 case,走草稿、素材、组装、终审、发布)。三条线通过父子 case 挂在一起,最上面的发布 case 靠子 case 全部终态来收口。

五种 stage kind,关卡都写在 config 里

创建流水线时传的是一个 stage 数组,每个 stage 至少有 keynamekindposition,关卡行为放在 config 里。教程用到的 kind 有这几种:

kind含义教程里的例子
open敞口阶段Release Coverage 的 intake
working干活阶段draftingassetsassemblypublishing
review评审阶段suggestion_reviewfinal_review
done完成终态coveredpublished
cancelled取消终态cancelleddropped

position 是排序数(教程里用 100、200、900、1000 这样留空档)。review 阶段的三个出口全写在 config 里:

{
  "key": "final_review",
  "name": "Final Review",
  "kind": "review",
  "position": 400,
  "config": {
    "approveToStageKey": "publishing",
    "rejectToStageKey": "dropped",
    "requestChangesToStageKey": "drafting",
    "requireRejectReason": true,
    "reviewerKind": "human"
  }
}

值得留意的是 requestChangesToStageKey 指回 drafting:教程明确说这条路是「同一个 case 重新进入草稿,工作引用继续沿用」,不是新开一个 case。另外 drafting 阶段的 config 里写了 "autonomy": "suggest",对应的行为是 Agent 不能自己把 case 挪走,只能提建议。

如果想把可走的路彻底锁死,用 set-transitions 打开 enforceTransitions 并列出允许的边:

paperclipai pipelines set-transitions \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$RELEASE_PIPELINE" \
  --file /tmp/release-transitions.json

教程里发布流水线只允许 intake -> coveredintake -> cancelled 两条边,剩下的全部走不通。

两种「等」:等孩子干完,和等上游解锁

这两个机制经常被当成一回事,其实触发条件完全不同。

autoAdvanceOnChildrenTerminal 写在 stage 的 config 里,值是目标 stage 的 key。子 case 全部进入终态(done 或 cancelled 都算)之后,父 case 自动前进。教程在三个地方用了它:Release Coverage 的 intakecovered、Feature Content 的 producingcovered、Content Production 的 assemblyfinal_review。注意最后一个:组装阶段建一个 package 子 case,把它推到终态,博客 case 就自动进终审了。

blockedByCaseKeys 是写在 case 上的。教程里 launch-tweet 声明了 blockedByCaseKeys: ["blog-post"],在同一批 ingest 里 CLI 会把这个 key 解析成对应的 case。在博客 case 到达 done 终态之前,推动这条推文 case 的请求会失败,返回 409 code=blocked

顺带一个容易忽略的点:被 reject 掉的东西也算终态,也计入父级收口。教程最后拉 rollup 的返回形状是:

{
  "total": 8,
  "done": 5,
  "cancelled": 3,
  "open": 0,
  "complete": true
}

八个 case,五个 done 三个 cancelled,complete 就是 true。这个口径在设计流程时挺关键:想让父级卡住,靠的不是「别取消」,而是别让子 case 进终态。

expectedVersion:并发改同一个 case 的 409

case 上有 version,几乎所有写操作都要带 --expected-version。版本对不上时 API 返回 409code=version_conflict,并带上当前 version 和当前 stage。文档给的恢复动作只有一条:重新读一次 case,拿当前版本重试。

paperclipai pipelines case get -C "$PAPERCLIP_COMPANY_ID" "$BLOG_CASE" --json

这里还有个和「乐观锁」配套的机制:上游 case 发生实质性修改时,会往下游已关联的 work issue 上贴一条 drift 评论。关联方式是给 case 挂 issue link:

curl -sS -X POST \
  -H "Authorization: Bearer $PAPERCLIP_API_KEY" \
  -H "Content-Type: application/json" \
  --data "$(jq -cn --arg issueId "$TWEET_WORK_ISSUE" '{ issueId: $issueId, role: "work" }')" \
  "$PAPERCLIP_API_URL/api/cases/$TWEET_CASE/issue-links"

也就是说,「上游改了下游不知道」这个老问题,Paperclip 的解法是系统评论加人工判断,不是自动回退。至于 case 和 issue 之间更细的关系,可以对照 issues 与 agents 两组核心接口 一起看。

把 routine 挂到某个阶段上

set-automation 是 pipeline 和 routine 的接合点:指定 stage、指定 routine id。

paperclipai pipelines set-automation \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CONTENT_PIPELINE" \
  --stage drafting \
  --routine "$DRAFTING_ROUTINE_ID" \
  --note "Template-versioned with the routine prompt."

流水线本身还能挂一份 guidance 文档(pipelines guidance put),教程说这份文档承载的是「持久的评判标准」,比如终审三个出口分别什么时候用。

有一点教程自己标得很清楚:typedWorkRefsbriefedFromVersion 这类字段只是普通的 case fields,不是新增的原语。它们记录的是「这个 case 指向哪份工作产物」「下游素材简报钉住了上游哪个版本」。教程把这些叫作 convention(约定),并说 v1 的「模板」是靠 routine prompt 加批量文件版本化的,不是挂在 pipeline 上——原话是这属于「与长期形态之间被接受的偏离」。这种自曝设计债的写法,比一份只讲优点的文档有用得多。

routine 的三类触发器和两组策略

routine 的字段表里,titleassigneeAgentIdprojectId 标为必填,goalIdparentIssueIdprioritystatus 可选。priority 取值 critical / high / medium(默认)/ lowstatus 取值 active(默认)/ paused / archived。项目和目标这两层怎么组织,见 目标与项目层怎么组织

触发器分三种,一个 routine 可以同时挂多种:

kind关键字段说明
schedulecronExpressiontimezone按 cron 表达式触发,文档示例是 0 9 * * 1Europe/Amsterdam
webhooksigningModereplayWindowSec外部 POST 到生成的 URL;签名模式 bearer(默认)或 hmac_sha256;重放窗口 30–86400 秒,默认 300
api只能通过 Manual Run 显式调用触发

外部系统打 webhook 走 POST /api/routine-triggers/public/{publicId}/fire,需要合法的 Authorization,或者 X-Paperclip-Signature + X-Paperclip-Timestamp 这对头,取决于该触发器的签名模式。密钥可以轮换(rotate-secret),文档写明旧密钥立即失效——没有重叠期,轮换前得先安排好外部系统的切换。

两组策略决定了「撞车」和「漏跑」怎么办:

类别取值行为
并发coalesce_if_active(默认)新来的 run 立即被终结为 coalesced 并链到活跃 run,不建新 issue
并发skip_if_active新来的 run 立即被终结为 skipped 并链到活跃 run,不建新 issue
并发always_enqueue不管有没有活跃 run,一律新建
补跑skip_missed(默认)错过的计划运行直接丢弃
补跑enqueue_missed_with_cap错过的运行按内部上限补入队

教程里那条 drafting routine 用的是 always_enqueue + skip_missed。选 always_enqueue 的代价是可能堆积,选 coalesce_if_active 的代价是「本该跑两次只跑了一次」,这两个坑没有免费选项,得看这条 routine 的产物能不能合并。

手动触发用 POST /api/routines/{routineId}/run,body 里可以带 sourcetriggerIdpayloadidempotencyKey。两个细节:并发策略对手动运行同样生效triggerId 可省,给了的话服务端会校验它属于这条 routine(否则 403)且处于启用状态(否则 409),并更新它的 lastFiredAt

权限与生命周期:Agent 只能管自己的

routines 的权限边界写得很硬:Agent 能读公司里所有 routine,但只能创建和管理指派给自己的,且不能把 routine 改派给别的 Agent——改派只有 board 能做。创建、更新、增删触发器、轮换密钥、手动运行,Agent 一律限于「自己的」。

生命周期是 active -> paused -> active,或者 -> archived归档后不再触发,也无法重新激活,这是个单向门,别拿 archive 当「先停一下」用,停用请用 paused

routine 的定义有 append-only 的 revision 历史,GET /api/routines/{routineId}/revisions 按最新在前返回,快照里只包含 routine 字段和安全的触发器元数据,webhook 密钥值和 secretId 永远不会返回。恢复历史版本走 POST /api/routines/{routineId}/revisions/{revisionId}/restore,它的做法是复制一份旧定义作为新的最新版本,历史行、运行历史、活动历史都保留。如果恢复过程需要重建一个被删掉的 webhook 触发器,响应里可能带一次性的替换密钥材料。更新 routine 时可以带 baseRevisionId,值陈旧会返回 409 Conflict 并给出当前 revision id;这个字段为了向后兼容是可选的,但既然要防并发改,还是老实带上。

排查思路:出问题先看事件流

教程最后一步讲的是从 case 事件里捞证据:

paperclipai pipelines case events \
  -C "$PAPERCLIP_COMPANY_ID" \
  "$CHANGELOG_CASE" \
  --json

要找的事件类型很具体:review_decided(看 payload.decisionrequest_changesapprove 还是 reject)、children_terminal 以及紧随其后的自动 transitioned。前者告诉你「为什么被打回」,后者告诉你「父级是不是真的因为子级终态而自动跳的」。评审卡住时的排查路径可以参考 审批卡住怎么办

教程还提供了一条端到端冒烟:

PAPERCLIP_API_URL=http://localhost:3100 \
PAPERCLIP_COMPANY_ID=<company-id> \
PAPERCLIP_API_KEY=<token> \
pnpm smoke:pipelines-tutorial

它断言的东西正好是上面这些机制:三条流水线按预期建出来、特性评审一批一驳、批量 ingest 正确接上 blockedByCaseKeys、就绪走 suggest 加人工接受、上游漂移给关联 work issue 贴系统评论、陈旧编辑返回 409 code=version_conflict、组装阶段的子级终态门自动把父级推进终审、终审三个出口都通、发布 rollup 完整且 done/cancelled 数目对得上。

什么时候不适用,以及文档没说清的

先说场景。这套东西的重量都在「关卡」上:review 阶段、enforceTransitionsexpectedVersion、blocker。如果你的流程本来就是一条直线、也没有需要人点头的节点,那配一堆 stage 只是给自己加摩擦,用普通任务流转就够了(见 任务从建到关的流转)。反过来,只要出现「上游改了下游要重来」或者「一批子活儿全完了才能收口」,pipelines 提供的两种等待机制才开始值回票价。

几处需要注意的边界:

  • 教程是对着 dev 实例写的,前置条件写明用 http://localhost:3100,且需要一个能管理 pipelines、routines、issues 的 board token 或 agent token。生产环境的差异文档没展开。
  • 两处口径不一致:routines 接口文档把 projectId 标为必填,但教程里创建 drafting routine 的示例 payload 只有 title、description、priority、status、两个策略字段和可选的 assigneeAgentId。真要照抄,以接口文档的字段表为准,先把 projectId 补上再试。
  • enqueue_missed_with_cap 的上限值文档没给,只说是「内部上限」。指望靠补跑追平长时间停机,得先自己实测这个数。
  • 模板机制目前是约定不是原语。教程自己承认 v1 的模板版本化落在 routine prompt 加批量文件上,不在 pipeline 上,并把这标注为将来可能升级为原语的候选。这意味着现在照着教程搭的模板层,后面官方改了原语形态是要动的。
  • 归档不可逆always_enqueue 会堆积,webhook 密钥轮换没有重叠期。这三个是配置时最容易事后后悔的地方。

真要动手,建议顺序反过来:先只建一条流水线、只配一个 review 阶段,把 expectedVersion 的 409 和 review 的三个出口走通,再往上加父子 case 和自动前进;routine 留到最后接,因为它一旦按 cron 跑起来,调试时产生的噪音会盖住你真正想看的事件。

延伸阅读


本文依据 Paperclip 官方仓库(github.com/paperclipai/paperclip,MIT 协议)的 docs/ 用户文档 与 doc/ 下的规范、运维与连接器手册整理,核对日 2026-08-17。 我们没有部署或运行过 Paperclip,因此不涉及界面外观与操作手感; 部分规范文档描述的是目标架构而非当前实现,文中已就地标注,不构成对实际行为的保证。 请以仓库最新内容为准。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。