这是什么
Workflows(特性 WORKFLOW_SCRIPTS)让千手大师用确定性 JavaScript
脚本编排多个子 agent。模型通过 Workflow 工具提交脚本,工具把它交给引擎在
后台执行并立即返回一个 runId,完成时以 task 通知回传返回值。
脚本里可使用一组固定的编排原语:
agent()—— 派发一个子 agent,无 schema 返回文本,带 schema 返回校验后的对象;parallel()—— 并发跑一组 thunk,屏障语义,等全部完成;pipeline()—— 每个 item 独立链式过各 stage,阶段间无屏障(默认推荐);phase()/log()—— 标记阶段与输出进度旁白;workflow()—— 内联运行一层子 workflow;args/budget—— 透传参数与读取 token 预算。
引擎位于独立包 packages/workflow-engine/
(@claude-code-best/workflow-engine,对核心层零运行时依赖),
核心侧的薄适配层在 src/workflow/:负责装配 Workflow 工具、
解析命名脚本、驱动进度、持久化运行状态。
Ultracode 只是把「如何编排」的手册注入上下文; Workflows 是真正执行脚本的引擎。两者配合给出「编排工作法 + 执行载体」。
前置条件
| 项 | 要求 |
|---|---|
| feature flag |
编译期 WORKFLOW_SCRIPTS 开启时,Workflow 工具与
/workflows 命令才注册。
|
| 运行时 | 脚本在 CLI 进程内执行,无文件系统 / Node.js API 访问。 |
| 脚本语言 |
纯 JavaScript。引擎不转译 TypeScript,.ts 文件中含类型语法会直接报错,
推荐 .js / .mjs。
|
| 命名脚本目录 |
.claude/workflows/<name>.ts|js|mjs(相对项目根目录);
发现的脚本自动生成 /<name> 命令。
|
安装启用
-
确认 Workflow 特性已开启
Build 产物默认包含
WORKFLOW_SCRIPTS。dev 模式可显式开启:# dev 模式手动开启工作流脚本特性 FEATURE_WORKFLOW_SCRIPTS=1 bun run dev -
建立命名 workflow(可选)
在项目根目录创建
.claude/workflows/,放入脚本。文件即命令:review-changes.mjs会生成/review-changes。mkdir -p .claude/workflows # 放入 review-changes.mjs(见「实战示例」),即可用 /review-changes 调用 -
启动一个 workflow
三种入口:内联
script、命名name、文件scriptPath。# 模型侧调用 Workflow 工具(示例) Workflow({ scriptPath: ".claude/workflows/review-changes.mjs", args: ["src/a.ts"] }) # 或使用命名脚本生成的斜杠命令 /review-changes -
用 /workflows 观察进度
面板打开后按
Tab切 run、←/→切左右列、q退出。
编排 API 与约束
脚本内可用的钩子(注入为函数形参,直接调用,不要 import):
| 原语 | 签名 | 语义与失败行为 |
|---|---|---|
agent |
agent(prompt, opts?) => Promise<any>,opts:
label / phase / schema / model /
isolation: 'worktree' / agentType
|
派发子 agent;无 schema 返回最终文本,有 schema(JSON Schema)
强制走结构化输出并返回校验对象。用户 skip 或 agent 终态死亡返回 null。
|
parallel |
parallel(thunks) => Promise<any[]> |
屏障:Promise.all 全部 thunk 后返回。单项抛错 → 该项为
null,调用本身不 reject(用 .filter(Boolean))。
|
pipeline |
pipeline(items, ...stages) => Promise<any[]> |
无屏障:每个 item 独立走 stage1 → stage2 → …。stage 回调收到
(prevResult, originalItem, index)。某 stage 抛错 → 该 item
null 并跳过后续 stage。
|
phase |
phase(title) => void |
开启新阶段,后续 agent / log 归入该组;面板按此分组展示。 |
log |
log(message) => void |
向用户输出一行进度旁白,无状态变更。 |
workflow |
workflow(name | {scriptPath}, args?) => Promise<any>
|
内联运行一层子 workflow,共享并发 / 计数 / 预算;子 workflow 内再嵌套 → 抛错(仅一层)。 |
args |
变量 | Workflow 工具 args 的原样透传;须传真实 JSON 值,不要传字符串化 JSON。 |
budget |
{ total, spent(), remaining() } |
total 为本 turn 的 token 目标(无则 null);
spent() 为主循环与所有 workflow 共享的输出 token;
触顶后再调 agent() 抛错。
|
硬限(超出直接抛错,不静默截断):
| 常量 | 值 | 含义 |
|---|---|---|
DEFAULT_MAX_CONCURRENCY |
3 | 每 run 默认并发 agent 数 |
MAX_CONCURRENCY_CAP |
16 | maxConcurrency 入参的上限(钳到 [1, 16]) |
MAX_TOTAL_AGENTS |
1000 | 单个 workflow 生命周期内总 agent 数上限 |
MAX_ITEMS_PER_CALL |
4096 | 单次 parallel / pipeline 的 items 上限 |
脚本执行模型与确定性约束:
| 规则 | 说明 |
|---|---|
| 不是 ESM 模块 |
脚本是 new AsyncFunction 的函数体;agent / parallel /
pipeline / phase / log / workflow /
args / budget 均为注入形参,禁 import。
|
| 禁 TS 语法 |
不要类型注解、interface、enum、as、泛型。
即便文件是 .ts 也原样报语法错。
|
| 唯一 export |
只允许一处 export const meta = {...},其余 export 与
export default 都要去掉;顶层 return 返回结果。
|
| meta 纯字面量 |
meta 在加载期求值,必须是纯字面量(无变量 / 函数调用 / 展开 / 模板插值),
否则抛 ScriptError。必填 name / description。
|
| 确定性 |
Date.now() / Math.random() / 无参 new Date()
被 shim 屏蔽并抛错,以保证 journal 可重放。时间戳 / 随机种子经 args 传入。
|
目录与持久化:
| 路径 | 内容 |
|---|---|
.claude/workflows/<name>.ts|js|mjs |
命名 workflow 脚本(扫描顺序 .ts → .js → .mjs) |
.claude/workflow-runs/<runId>/journal.jsonl |
按执行顺序记录的每个 agent() 的 { key, seq, result },用于 resume 重放 |
.claude/workflow-runs/<runId>/state.json |
终态 RunProgress 快照(含 returnValue / error),原子写(tmp + rename),面板重启后可展示历史 |
仅在 run 进入终态(completed / failed / killed)时才写 state.json;
.claude/workflow-runs/ 保留最近 50 个 run(KEEP_MAX_RUNS),
打开面板时 hydrate 最新 20 个(LOAD_PERSISTED_LIMIT)。持久化失败只记日志,不阻断
workflow 完成。
常用命令与参数
Workflow 工具输入字段(模型侧发起 workflow 时使用):
| 字段 | 类型 / 取值 | 说明 |
|---|---|---|
script |
string(可选) | 内联脚本字符串 |
name |
string(可选) | 命名 workflow,解析到 .claude/workflows/<name>.ts|js|mjs |
scriptPath |
string(可选) | 已存在脚本的绝对路径(须位于 cwd 内) |
args |
任意 JSON 值(可选) | 透传给脚本的 args,传真实对象 / 数组,不要传字符串化 JSON |
resumeFromRunId |
string(可选) | 从既有 run 重放 journal |
description |
string(可选) | 本次调用描述(3–5 词) |
title |
string(可选) | 进度视图标题(缺省回退 workflow) |
maxConcurrency |
int,1–16(可选) | 每 run 并发上限,缺省 3 |
斜杠命令:
| 命令 | 作用 |
|---|---|
/workflows |
打开全屏监控面板(三区焦点模型:顶部 run tab + 左 phase 侧栏 + 右 agent 列表) |
/<name> |
.claude/workflows/ 脚本自动生成;调用即要求模型以
name="<name>" 发起 Workflow
|
/workflows 面板键位:
| 键 | 作用 |
|---|---|
Tab / Shift+Tab | 切换顶部 run tab(正 / 反) |
← / → | 在 phases 与 agents 两列间切换焦点 |
↑ / ↓ | 当前焦点列内移动(phase 改筛选,agent 移光标) |
x | kill 当前选中的单个 agent(需二次确认) |
K | kill 整个 run(需二次确认) |
r | resume 当前 run |
n | 新建提示 |
q / Esc | 退出面板 |
每个 run 一个 tab(状态点 + 名称 + #runId短码)。左栏合并
meta.phases 声明的 pending 阶段(○)与实际阶段
(● running / ✓ done),并含固定的 All 项;
右栏按选中 phase 过滤 agent,行尾标注 running / object /
text / dead。
实战示例
一个「发现 → 改写 → 验证」的迁移类 workflow:改写阶段用
isolation: 'worktree' 隔离并行写,验证阶段读取原 item 定位文件。
export const meta = {
name: 'migrate-api',
description: '发现 legacyFetch 调用点,逐个改写并验证',
phases: [
{ title: 'Discover', detail: 'grep 调用点' },
{ title: 'Transform', detail: '每处一个 agent(worktree 隔离)' },
{ title: 'Verify', detail: '逐处验证改写' },
],
}
const SITES_SCHEMA = {
type: 'object',
properties: {
sites: {
type: 'array',
items: {
type: 'object',
properties: {
file: { type: 'string' },
line: { type: 'number' },
},
required: ['file', 'line'],
},
},
},
required: ['sites'],
}
const VERDICT_SCHEMA = {
type: 'object',
properties: { ok: { type: 'boolean' } },
required: ['ok'],
}
phase('Discover')
const found = await agent(
'列出仓库中所有对 legacyFetch 的调用点(file + line)',
{ label: 'discover:sites', schema: SITES_SCHEMA }
)
// Discover 之后需要全部调用点才能 fan-out,故此处显式屏障后的 pipeline 逐项处理
const results = await pipeline(
found ? found.sites : [],
site => agent(`把 ${site.file}:${site.line} 的 legacyFetch 改写为新 API`, {
label: `transform:${site.file}`,
phase: 'Transform',
isolation: 'worktree',
}),
(_patch, site) => agent(`验证 ${site.file} 的改写是否正确`, {
label: `verify:${site.file}`,
phase: 'Verify',
schema: VERDICT_SCHEMA,
})
)
const verified = results.filter(Boolean).filter(v => v.ok)
log(`已验证 ${verified.length} 处改写`)
return { verified }
迭代与 resume:脚本改动后,用同一路径加 resumeFromRunId 重新发起,
未改动前缀里的 agent() 会命中 journal 直接返回缓存,只重跑首个发散点之后的部分。
# 首次运行后,工具结果里会给出 runId
Workflow({ scriptPath: ".claude/workflows/migrate-api.mjs" })
# 改脚本后从该 run 重放
Workflow({
scriptPath: ".claude/workflows/migrate-api.mjs",
resumeFromRunId: "<runId>",
})
# 也可在 /workflows 面板中对选中的 run 按 r 续跑
用预算控制深度:当用户在 turn 里给出 token 目标(如 +500k)时,
budget.total 有值,可据此动态扩缩 fan-out。
// 无预算目标时 total 为 null,勿裸写 while(会一路跑到 1000 agent 硬顶)
// 需要动态循环时始终以 budget.total 为守卫
const found = []
while (budget.total && budget.remaining() > 50000) {
const r = await agent('继续寻找遗漏的调用点', { schema: SITES_SCHEMA })
found.push(...(r ? r.sites : []))
log(`已收集 ${found.length} 处`)
}
常见问题排错
脚本语法错误或 meta 非法时,parseScript 在进后台前就报错,
工具结果直接返回 Error: script validation failed,不产生 run。
常见原因即 import、TS 语法、多个 export、缺 meta。
子 agent 被用户 skip、或在重试耗尽后终态死亡(kind:'dead')时会返回
null,但不会杀掉整个 workflow。parallel /
pipeline 里的单项失败同样只把该项置 null。
处理结果前统一 .filter(Boolean)。
parallel 是屏障,wall-clock 等于最慢项;pipeline 无屏障,
快链不会等慢链。只有阶段 N 确实需要上一阶段全部结果时(跨项去重、总数为零提前退出、
提示词要引用「其它发现」)才用屏障。默认选 pipeline。
journal 按执行顺序比对 agent() 的 key(prompt + 去掉展示字段后的
opts 哈希)。命中「最长未变前缀」,首个发散点之后现场重跑。脚本源码 hash 变化
(改动脚本)会导致整体不命中;此外若脚本绕过 shim 用非确定性来源制造差异,也会导致
resume 命中错误缓存——请勿用 globalThis.Date 之类的旁路。
spent() 达到 total 后,后续 agent() 直接抛错
(脚本可 try/catch)。预算池在主循环与所有 workflow 间共享;无 total 时
remaining() 为 Infinity。
只有进入终态(completed / failed / killed)的 run 才会写 state.json;
进程在 run 结束前被强杀(SIGKILL / 断电)会导致该 run 无磁盘快照。若文件存在但面板仍看不到,
检查 .claude/workflow-runs/<runId>/state.json 是否损坏或
schemaVersion 不符(当前为 1)。持久化失败只记日志、不影响 workflow 本身。
默认 DEFAULT_MAX_CONCURRENCY=3 是为避免一次扇出十几个 agent。可以传更多
item 给 parallel / pipeline(会排队跑完),但同一时刻只有配置数量的
agent 在跑;需要更高并发时经 maxConcurrency(1–16)覆盖,且工具描述要求先与用户
确认。