帮助用户创建符合 mini_agent 最新版规范的 workflow(.agent/workflows/<name>/workflow.yaml,可携带私有 agents/skills/prompts;支持顶层 defaults 默认配置继承、可复用 step 片段 include、max_total_tokens 用量护栏、mode: autonomous 全自动执行模式、插件自定义 step 类型、foreach/wait/merge 批处理与汇聚类型、tool_call 占位符入参、result_file 字段级占位符)。当用户说"帮我创建一个workflow"、"做一个工作流"、"写一个xxx流水线"、"把这个workflow转成文件夹模式"、"做一个全自动/挂后台跑的workflow"、"对一批数据逐个处理"、"把几个step的结果合并一下"时使用。
Install
npx skillscat add onewaymyway/mini-agent/workflow-generator Install via the SkillsCat registry.
Workflow Generator(文件夹模式)
用于创建符合本项目 WorkflowStore/WorkflowDef/WorkflowStep
(src/mini_agent/workflow/schema.py、src/mini_agent/workflow/store.py)
解析规范的 workflow。优先按"文件夹模式"创建——除非用户明确要求单文件、
或工作流足够简单(≤2 步、不需要私有 agent/skill/prompt 文件)。
完整设计背景见 next_doc/workflow_directory_mode_design.md,可运行的完整
示例见 .agent/workflows/doc_change_review/(本 skill 生成的内容应与它
风格一致)。批处理/汇聚/占位符相关的最新规范来自 P12-P15 四轮迭代(next_doc/workflow_mechanism_improvement_plan_p12.md ~ _p15.md),完整
细节见 docs/workflow-guide.md,本 skill 只摘录生成时需要的规则。
两种模式,如何选择
| 单文件模式 | 文件夹模式 | |
|---|---|---|
| 路径 | .agent/workflows/<name>.yaml |
.agent/workflows/<name>/workflow.yaml |
| 适用场景 | 简单、prompt 内嵌几行文本就够、不需要专属 agent/skill | 复杂流水线、prompt 较长、需要工作流私有的角色 agent 或 skill |
| 私有资源 | 不支持 | 支持 agents/、skills/、prompts/ |
WorkflowStore.load(name) 优先找文件夹模式,找不到再退回单文件模式,
两者可以在 .agent/workflows/ 下共存,不冲突。
动手写之前先看看有没有现成模板
除了从零写 YAML,还可以用 list_workflow_templates() 看内置模板
(code_review/research_report/multi_perspective_debate 等,定义在src/mini_agent/workflow/templates/*.yaml),需求和某个模板高度相似时用create_workflow_from_template(template_name, new_name) 直接复制一份、
换个名字保存,比手写更稳(模板本身已经过校验),后续再用patch_workflow_step(workflow-debugger skill)微调具体字段。只有需求和
现有模板差异明显、或用户要求"自己设计流程"时才走下面的手写流程。
另一条生成路径:从已完成的 session 反向生成
本 skill 覆盖"用户用自然语言描述一个流程,帮他从零写 YAML"这条路径。如果
用户想要的是"把之前某次 session 里实际做成的一件事沉淀成 workflow"(比如
"把刚才那次修 bug 的过程整理成一个 workflow"),不要在这里手写 YAML——
这是 summarize_session_for_workflow → build_workflow_from_summary 两个
Agent 工具(或 CLI /workflow from-session <session_id>)的场景,设计见next_doc/session_to_workflow_design.md,使用说明见docs/workflow-guide.md"从历史 Session 生成 Workflow"一节。区分方式:
用户给的是"一段流程描述"→ 本 skill;用户指的是"之前发生过的一次具体任务"
→ 那两个工具。该路径受 agent_config.json 里workflow.session_to_workflow_enabled 开关控制(默认开启),关闭后这两个
工具和对应 CLI 子命令会返回"功能已关闭"提示。
再另一条边界:测试、调试、修改已有 workflow
本 skill 只覆盖"从零生成/保存一个新 workflow"。如果用户是针对已经存在
的 workflow 说"跑一下试试"、"这步失败了帮我看看"、"改一下这个 step 的
prompt/timeout"、"这个改动会不会影响后面的 step,先测一下",这些是
workflow-debugger skill 的场景(沙箱单步测试 test_workflow_step、
断点续跑 resume_workflow_run、单步修改 patch_workflow_step、执行统计get_workflow_stats 等),不要在这里手工排查或重新走一遍生成流程。
默认按文件夹模式创建:即使当前只有 2-3 步,文件夹模式也不吃亏
(agents//skills/ 目录为空即可,不强制使用),而且用户后续加步骤、
加私有资源时不需要再迁移。只有用户明确说"就要单文件"、"简单点不要建
文件夹"时才退回单文件模式(直接写 .agent/workflows/<name>.yaml,字段
和文件夹模式下的 workflow.yaml 完全一样,只是没有 agents//skills//prompts/ 子目录、也不能用 prompt_file/本地 role/本地 skill_agent)。
目录结构(文件夹模式)
.agent/workflows/<name>/
workflow.yaml 必需,主入口
agents/ 可选,工作流私有 agent profile(同 .agent/agents/*.md 格式)
<role>.md
skills/ 可选,工作流私有 skill(同 .claude/skills 目录格式)
<skill-name>/
SKILL.md
prompts/ 可选,抽出来的 prompt 模板文件
<step_id>.mdagents/、skills/、prompts/都是可选的;用不到就不创建,或创建成
空目录也没关系(AgentProfileLoader/SkillLoader会自动跳过空目录)。agents/*.md的写法和全局.agent/agents/*.md完全一样(可以参考.claude/skills/agent-generator/SKILL.md),区别只是它只在这一个
workflow 内生效、且同名时优先于全局/项目级同名 profile。skills/<name>/SKILL.md的写法和.claude/skills/<name>/SKILL.md
完全一样,区别同样是只在这一个 workflow 内生效(配合type: skill_agent使用,见下文)。
workflow.yaml 字段规范
顶层字段:
| 字段 | 必填 | 说明 |
|---|---|---|
name |
是 | 工作流唯一标识,建议与文件夹名一致 |
description |
建议 | 一句话说明这个工作流做什么、什么场景下用 |
version |
否 | 字符串,缺省 "1.0" |
steps |
是 | step 列表,见下 |
max_total_duration |
否 | 整体超时(秒),缺省走全局配置 |
max_total_tokens |
否 | 整体 token 用量护栏,超过则看护线程主动取消该次执行,缺省走全局配置(只统计 agent/skill_agent 类型 step) |
defaults |
否 | 一个 dict,给 model/timeout/max_turns/retry_on_error/allow_parallel 这 5 个 step 字段提供统一默认值,减少重复。见下方"善用 defaults" |
mode |
否 | interactive(默认)或 autonomous。用户明确表示这个 workflow 要"全自动跑完、中途不能等人"时必须设为 autonomous,见下方"全自动执行模式"一节;保存时会校验,混入阻塞点会直接报错,不是等运行时才发现卡住 |
单个 step 的字段:
| 字段 | 必填 | 说明 |
|---|---|---|
id |
是 | 步骤唯一标识,其他 step 通过 {id.output} / {id.score} 引用它的结果 |
name |
建议 | 可读名称 |
type |
建议显式写 | 见下方"Step 类型"表;不写时按 role 是否为空自动推断为 agent/role_agent(向后兼容旧写法,新建议一律显式写 type)。也可以是插件通过 register_step_executor() 注册的自定义类型(不在下表里,见"自定义/插件 Step 类型"一节)。P13/P14 新增 foreach/wait/merge 三种内置类型,见下方专门一节 |
include |
与 prompt 二选一 |
引用 .agent/workflow_snippets/<n>.yaml 里的一段可复用 step,见下方"善用可复用 step 片段(include)"。填了这个之后该 step 条目本身只需要 id(作为命名空间前缀)+ depends_on(挂接点),prompt/type 等字段不需要填、填了也会被忽略 |
prompt |
二选一 | 内嵌 prompt 文本,支持 {参数名}、{其他step_id.output}、{其他step_id.score} 占位符 |
prompt_file |
二选一 | 相对 workflow 所在目录的相对路径,如 prompts/analyze.md;与 prompt 都填时 prompt_file 优先;文件夹模式下一律优先用这个而不是内嵌 prompt——内联 prompt 超过 5 行时 validate() 会给出 warning(不阻断保存,但生成新 workflow 时应主动避免) |
role |
role_agent 专用 |
角色 agent 名,优先匹配本工作流 agents/<role>.md,没有则退回全局 .agent/agents/ |
skill_name |
skill_agent 专用 |
skill 名,优先匹配本工作流 skills/<skill_name>/SKILL.md,没有则退回全局 skills 目录 |
depends_on |
否 | 依赖的 step id 列表,决定拓扑执行顺序;同一"层"(无相互依赖)的 step 默认并发执行 |
condition |
否 | 形如 "evaluate.score >= 6" / "review.output != ''" 的表达式,不满足则跳过该 step(SKIPPED)。注意(P12):如果表达式本身写错(引用了不存在的 step、或此刻类型不对导致 AttributeError/TypeError),求值会抛异常,这种情况判的是 NEEDS_FIX 而不是 SKIPPED——生成时如果不确定某个字段这时是否已就绪,先用 preview_workflow/test_workflow_step 验证 |
allow_parallel |
否 | 不写(null)时按"本字段 → 顶层 defaults.allow_parallel → 硬编码兜底 true"三层继承;step 有隐式副作用(读写同一外部状态)时显式设为 false 强制串行 |
max_turns |
否 | 该 step 允许的最大 LLM 轮数;不写时同样三层继承,硬编码兜底 10 |
model |
否 | 覆盖该 step 用的模型;不写时三层继承(defaults.model → 全局配置) |
timeout |
否 | 该 step 超时(秒);不写时三层继承,硬编码兜底为不限制 |
retry_on_gate_fail |
否 | evaluator 质检门不达标时重跑前序步骤的最大次数,仅对 role_type: evaluator 的角色有意义。不参与 defaults 继承,缺省就是 0,与其它几个"三层继承"字段是两套独立机制 |
retry_on_error |
否 | 普通异常(超时/工具报错)重试次数;不写时三层继承,硬编码兜底 0 |
escalate_after_n_same_failures |
否 | 同一 step 在 retry_on_error 重试循环里连续(中间没成功)出现同一个 error_type 达到这个次数时,看护线程提前判定"大概率不是瞬时故障",直接标记 needs_fix、跳过剩余重试预算;不写时三层继承(defaults → 全局默认 2),是与 max_turns 等同类的"三层继承"字段之一,resume_workflow_run(step_overrides=...) 也支持临时覆盖它,见新增的 workflow-debugger skill |
require_approval |
否 | 是否需要人工审批门放行,需配合 run_workflow(background=True) |
workflow_name |
sub_workflow 专用 |
引用的另一个已保存工作流名称,不能引用自身 |
tool_name / tool_args |
tool_call 专用 |
直接调用某个已注册工具,不启动整个 Agent。tool_args(P12 起)支持占位符——值是字符串时可以写 {step_id.output}/{step_id.result_file:a.b[0].c} 等,嵌套 dict/list 里的字符串叶子节点同样会被替换,纯字面量值不受影响;引用的 step 同样要出现在 depends_on 里,validate() 会检查 |
input_prompt |
human_input 专用 |
展示给人类的提示语,缺省用 prompt 本身 |
input_key |
human_input 专用 |
若启动 run_workflow(inputs={...}) 时能通过该 key 找到值,直接使用、不阻塞等待;mode: autonomous 的 workflow 里 human_input 步骤必须设置这个字段,否则保存时校验失败。见下方"全自动执行模式"一节 |
script |
script 专用 |
要执行的 shell 命令,受 cfg.workflow.script_step_enabled 开关保护,默认关闭 |
script_path |
python_step 专用 |
相对 workflow 目录的脚本路径(如 steps/03_filter.py),受 cfg.workflow.python_step_enabled 开关保护,默认关闭;脚本须暴露 def run(ctx: PyStepContext) -> str|dict |
params |
否,python_step 常用 |
透传给 python_step 脚本的自定义参数字面量(dict),脚本内通过 ctx.params 读取 |
output_file |
否,通用于所有 type | 该 step 执行完成后,输出统一落盘到当前 workflow session 的 output/<output_file>,不管是哪种 type 产生的输出;下游 step 可用 {该step.output_file} 占位符引用落盘文件的绝对路径 |
result_file / result_file_required_keys |
否,skill_agent/script 通用(P15 起) |
期望执行方主动产出的结构化结果文件名 + 必须包含的顶层字段。skill_agent 通过 prompt 注入路径文字告知 Agent;script(P15 起)通过环境变量 WORKFLOW_RESULT_FILE_PATH 注入子进程 env,脚本自行写文件。下游可用 {该step.result_file}(路径)或 {该step.result_file:a.b[0].c}(P12,直接取字段值)引用 |
items |
foreach 专用 |
要遍历的列表:字面量列表,或单个占位符字符串(如 "{search.result_file:questions}",此时取解析出的原始 Python list) |
foreach_step |
foreach 专用 |
内层每个元素要执行的 step 定义子集(须含 type),内层 prompt 支持 {item}/{item_index},不能是 foreach(禁止嵌套) |
foreach_max_concurrency |
否,foreach 专用 |
内层并发度,默认 1(串行),与外层 allow_parallel 是独立的并发维度 |
foreach_stop_on_error |
否,foreach 专用 |
默认 false:单元素失败记入聚合结果、不影响整体;true:第一个失败即整体抛异常交给 retry_on_error/NEEDS_FIX |
wait_seconds |
wait 专用 |
要等待的秒数(正数),可被 pause_workflow_run/cancel_workflow_run 打断 |
merge_sources |
merge 专用 |
要汇聚的上游 step id 列表,顺序即聚合顺序,须非空、无重复,且都要出现在 depends_on 里 |
merge_strategy |
否,merge 专用 |
concat_text(默认,拼接 output 文本)/ json_array(组成 JSON 数组)/ json_merge(按顺序 dict.update,须为 JSON object) |
merge_separator |
否,merge 专用 |
concat_text 策略下的拼接分隔符,默认 "\n\n" |
merge_use_result_file |
否,merge 专用 |
true 时 json_array/json_merge 从各来源的 result_file 读取;false(默认)读 output 文本 |
写法提示:
max_turns/model/timeout/retry_on_error/allow_parallel/escalate_after_n_same_failures这 6 个字段现在语义是"不写=继承"而不是
"不写=固定默认值"——生成 workflow
时,如果多个 step 都要用同一个非默认值(比如整个流水线都想用model: gpt-4.1-mini、max_turns: 6),优先写进顶层defaults,
不要在每个 step 里重复写一遍;只有某个 step 需要跟其它 step 不一样时才
在该 step 上单独覆盖。
Step 类型(type)
type |
语义 |
|---|---|
agent(默认) |
主 Agent 执行。文件夹模式下会自动带上本工作流的 agents//skills/,因此这一步内部通过 spawn_named_agent 也能看到本地 agent,技能触发/skill_activate 也能看到本地 skill |
role_agent |
用指定 role 的角色 agent 执行(优先本地 agents/<role>.md,找不到退回全局) |
skill_agent |
临时启动一个只强制挂载 skill_name 对应 skill(不走关键词触发判断)的最小 Agent 执行 prompt(优先本地 skills/,找不到退回全局) |
sub_workflow |
调用另一个已保存的工作流(workflow_name)。注意:子工作流不继承父工作流的本地资源包,子工作流按自己的 source_dir 独立解析 |
tool_call |
直接调用某个已注册工具,不启动 Agent,适合纯确定性操作 |
human_input |
暂停等待人工输入,配合 /workflow input |
script |
执行 shell 命令(默认关闭,需要显式开启配置项) |
python_step |
在独立子进程里跑一段外置 .py 脚本(script_path),不启动 Agent,适合"给定输入产出结构化 JSON"这类确定性数据加工(默认关闭,需要显式开启 python_step_enabled)。真正需要临场应变(如浏览器交互)的步骤仍应使用 agent/skill_agent,见下方"python_step:脚本外置与批量处理"一节 |
foreach(P13) |
对一个列表逐元素执行同一份 foreach_step 定义(可控并发度),结果聚合成 JSON 数组。"先搜出一批候选、再逐个 enrich"这类场景应该用这个类型,而不是在 python_step 里手写循环调 ctx.run_agent_turn()——那样会把编排层的逻辑硬编码进脚本,YAML 完全看不出这里其实是批处理 |
wait(P13) |
等待指定秒数,可被 pause/cancel 信号打断。用于限速节流、等一段固定时间;不要用 python_step 里 time.sleep 代替,那会跟子进程超时/watchdog 硬超时打架 |
merge(P14) |
把多个上游 step 的结果按策略汇聚成一个 step 的输出(拼接文本 / 组数组 / 合并对象)。多分支/多并发结果需要汇总时用这个类型,而不是在某个 step 的 prompt 里手写 {a.output}{b.output} 拼接 |
选型强制规则:python_step 优先,agent 是兜底方案
每生成一个 step 前都要先过一遍这个判断,不是可选的风格偏好:
- 这一步的本质是"对一个列表的每个元素重复执行同一份逻辑,再把结果聚合
成一个列表"吗(比如"先搜出一批候选、再逐个 enrich/打分/提取")?是的话
必须用type: foreach(见下方"批处理与汇聚"一节),不要在python_step脚本里手写for循环调ctx.run_agent_turn()——那样会把
本该是编排层的批处理逻辑硬编码进脚本,YAML 完全看不出这里其实是批处理,
调试时也没法单独用test_workflow_step验证某一个元素的处理逻辑。 - 这一步的功能能不能用
python_step+ctx.llm/ctx.llm.ask_json解决
(确定性加工、或"给模型一段明确输入、要求它给出结构化判断"这类单次/
分批调用)?能的话必须用python_step,即使这一步涉及"要不要收
录这条数据""打几分"这类看起来需要判断力的任务——只要能把判断要求写清楚
成一次ask_json调用,就不需要升级成agent。 - 不能的话,这一步是否可以约束在"强制挂载单一 skill、按固定套路多轮
调用"的范围内?可以的话用skill_agent,不要用无约束的agent。 - 只有步骤确实需要临场应变(工具选择、页面结构、下一步动作都要运行时
才能确定,比如真实浏览器交互、开放式调研)时,才生成type: agent/role_agent——agent是前两种方案都覆盖不了时才用的兜底选项,
不是"不确定用什么类型就先写 agent"的默认落点。
生成 workflow 时如果发现自己因为"图省事、不想让用户额外去开python_step_enabled 开关"而把一个本该是 python_step 的确定性加工步骤
写成了 agent,这是错误的取舍——应该按规则生成 python_step,并在结果里
提示用户开启开关,而不是为了"少一步配置"牺牲步骤的确定性和稳定性。
skill_agent(或需要结构化产出的 agent/role_agent):务必配 result_file
skill_agent 的产出是一段自由文本对话,如果下游是 python_step(或任何
需要拿到合法 JSON 的场景),不要指望靠 {step_id.output} 占位符或ctx.input_output() 解析对话原文——模型经常在 JSON 前后夹杂解释文字,
解析很脆弱。生成这类 step 时按下面的写法:
- id: search_zhihu
type: skill_agent
skill_name: browser-cdp
prompt_file: prompts/02_search_zhihu.md
output_file: search_results.json # 对话原文存档,供人工排查
result_file: search_results_data.json # 下游真正消费的结构化产物
result_file_required_keys: [questions] # 必须包含的顶层字段
max_turns: 25
timeout: 900- 声明了
result_file后,runner 会自动在 prompt 末尾追加"必须用文件写入
工具把结果写到这个绝对路径"的指令并在结束后校验;校验失败会自动
resume/重开 agent 补救(最多 3+3 次),不需要自己在 prompt 里手写这段
指令、也不需要在python_step脚本里自己兜底解析脏 JSON。 - 下游
python_step通过ctx.input_json("search_zhihu")/ctx.input_output("search_zhihu")取值时,只要上游声明了result_file
且校验通过,会优先读文件而不是对话原文——这也是为什么result_file_required_keys要如实列出下游脚本真正会用到的顶层字段,
避免"文件写了但少了个字段,下游脚本才发现"这种情况。 - 一定要在 prompt 里额外加一句"写完文件并自检通过后立即收尾,不要再做
其它操作":result_file只在这一轮 agent 自然结束后才会被校验,agent
如果写完文件还继续浏览/反复确认,即使结果早就产出了,这一步也要等它
自己收尾才会往下走,实测会明显拖慢整体执行时间。不写这句提示是目前
生成的 workflow 里最容易漏掉、也最容易导致"文件已经生成但迟迟不结束"
的一个点。
自定义/插件 Step 类型
以上是内置的 11 种类型(agent/role_agent/sub_workflow/tool_call/human_input/script/skill_agent/python_step/foreach/wait/merge)。项目还可能通过 myplugins/*.py 里的register_step_executor() 注册了自定义类型(比如示例插件myplugins/example_http_step.py 注册的 type: http)。生成 workflow
前,如果不确定项目里有没有这类插件、支持哪些自定义类型,可以查一下:
from mini_agent.workflow.executors import get_registered_types
print(get_registered_types()) # 内置 11 种(含 P13/P14 的 foreach/wait/merge)+ 插件注册的自定义类型用户明确要求某个自定义类型(如"用 http 类型调一下这个接口")时才使用;
不确定该类型需要哪些专属字段时,去看对应插件源码里的StepExecutor.validate_step() 实现,或直接问用户。不要凭空编造一个不存在
的 type——WorkflowDef.validate() 会在保存时因"type 非法"直接报错。
python_step:脚本外置与批量处理
完整规范见 next_doc/workflow_authoring_guide.md,参考实现见.agent/workflows/zhihu_content_publish/(4 个 step、4 个 prompt 文件、
2 个 python_step 脚本)。生成含 python_step 的 workflow 时遵循:
- 脚本代码一律外置到
steps/*.py,workflow.yaml只写script_path: steps/xx.py,不要把脚本内容内嵌进 YAML。 - 脚本入口函数签名固定:
def run(ctx: PyStepContext) -> str | dict:。ctx提供ctx.llm.ask()/ctx.llm.ask_json()(LLM 调用)、ctx.run_agent_turn()(临时起最小 Agent 处理需要判断力的子任务)、ctx.params(workflow.yaml里该 step 的params字面量)、ctx.inputs/ctx.input_output()/ctx.input_json()(读上游 step 结果,
默认只包含该 stepdepends_on里声明过的上游,需要读取某个历史
step 必须先把它加进depends_on)、ctx.load_prompt_file()(读prompts/*.md)、ctx.write_output()(往 output_dir 落盘中间产物)。 - 默认关闭:生成含
python_step的 workflow 后,要提示用户在agent_config.json里显式开启{"workflow": {"python_step_enabled": true}},否则运行时会被直接拦截。 - 多条候选数据的判断类场景走批量而不是逐条:比如"从 N 条候选里筛选
符合要求的",脚本内部分批调用ctx.llm.ask_json()(配合独立的prompts/xx_batch.md模板,要求模型逐条给出id+判断+理由的 JSON
数组),而不是对每条数据单独起一次 LLM 调用——省 token、省延迟。批量
调用返回的判断数量明显少于批次数量(漏判)时,应该拆成更小的子批重试
而不是直接丢弃,可参考.agent/workflows/zhihu_content_publish/steps/03_filter.py的BATCH_SIZE/MISS_RATIO_THRESHOLD/MIN_SUB_BATCH写法。 search_zhihu/enrich_questions这类真实浏览器交互步骤仍用skill_agent,不要为了"统一用 python_step"而把需要临场应变的步骤也
硬写成脚本——页面结构会变、要应对弹窗/滚动加载,脚本硬编码的稳定性
反而更差。
output_file 契约对 python_step 同样适用(且是通用契约,不限于这一种
type):声明了 output_file 之后,脚本 return 的内容会被 runner 自动
落盘到 session output/ 目录,脚本本身不需要自己拼路径写文件;下游 step
可以用 {该step.output_file} 占位符拿到落盘文件的绝对路径,提示agent/role_agent 类型的 step 直接读文件而不是把整段 JSON 塞进 prompt。
- id: analyze_doc
type: python_step
script_path: steps/01_analyze_doc.py
output_file: doc_analysis.json
timeout: 120
- id: filter_questions
type: python_step
script_path: steps/03_filter.py
depends_on: [analyze_doc, search_zhihu] # ctx.inputs 只会包含这两个 step
output_file: filtered_questions.json
timeout: 300批处理与汇聚:foreach / wait / merge(P13/P14)
三种类型都以纯 StepExecutor 插件形式实现,不改变外层拓扑调度语义——从
外部看它们跟其它 step 类型没有区别(一份输入、一段输出,output_file/
评分提取/NEEDS_FIX 判定都通用)。完整机制细节见docs/workflow-guide.md"批处理与汇聚"一节,这里只给生成时要遵循的规则。
foreach:对列表逐元素批处理
- id: enrich_each_question
type: foreach
depends_on: [search_zhihu]
items: "{search_zhihu.result_file:questions}" # 引用上游 result_file 字段,解析为原始 list
foreach_max_concurrency: 3 # 默认 1(串行),需要并发时显式调大
foreach_stop_on_error: false # 默认单元素失败不影响其它元素
foreach_step:
type: skill_agent
skill_name: browser-cdp
prompt: "打开问题详情页并提取正文:{item}(第 {item_index} 条)"
max_turns: 15生成规则:
items只能是字面量列表,或单个占位符字符串(不能是拼接了别的文本
的模板);引用别的 step 的result_file字段时用{step_id.result_file:field_path}语法(同 P12 的result_file字段
访问),会拿到解析出的原始 Python list,不是文本。foreach_step必须显式写type,且不能是foreach(禁止嵌套,validate()会直接拒绝)——遇到"批处理里还要批处理"的需求,说明这一步
本身该拆成两个 workflow 或换个粒度设计,不要硬套嵌套。- 内层 prompt 只有
{item}/{item_index}两个占位符可用,不能引用
外层的其它 step(内层没有"上游 step 结果"的概念);确实需要外层上下文,
写进外层resolved_prompt之外的固定字段(如foreach_step.params)。 foreach_max_concurrency默认1(串行),只有明确要提速、且各元素
之间没有隐式副作用冲突时才调大;这是独立于allow_parallel的并发维度,
不要混淆。- 需要"某个元素失败就整体停下"时设
foreach_stop_on_error: true(配合retry_on_error使用);否则保持默认false,让失败元素单独记录在聚合
结果里、不拖累其它元素。
wait:可中断的等待
- id: rate_limit_pause
type: wait
wait_seconds: 30只用于限速节流、等一段固定时间;wait_seconds 必须是正数。不要为了
"等一下"而在 python_step 脚本里写 time.sleep——那会跟子进程超时/
watchdog 硬超时的语义打架,且不能被 pause_workflow_run/cancel_workflow_run 提前打断。
merge:把多分支/多并发结果汇聚成一等公民 step
- id: final_report
type: merge
depends_on: [summary_a, summary_b, enrich_each_question]
merge_sources: [summary_a, summary_b, enrich_each_question] # 顺序即聚合顺序
merge_strategy: concat_text # concat_text(默认)/ json_array / json_merge
merge_separator: "\n\n---\n\n"
merge_use_result_file: false生成规则:
merge_sources里的每个 id 必须同时出现在该 step 的depends_on里
(跟 prompt 占位符同一套校验,写漏会在save_workflow阶段报 error)。- 单纯拼接文本(原来靠 prompt 手写
{a.output}{b.output}的场景)用默认concat_text;需要下游拿到结构化数组用json_array;需要把多个 JSON
object 合并成一个用json_merge(注意后面的来源会覆盖前面同名 key,
设计合并顺序时留意这一点)。 foreach产出一份 JSON 数组后,常见的下一步就是用merge
(json_array/json_merge)跟另一个 step 的结果合并成最终输出——这是
两者最自然的组合方式,生成"批处理 + 汇总"类流水线时优先考虑这个组合,
而不是让某个agent/python_step手写拼接逻辑。
善用 defaults(多个 step 共享同一非默认配置时)
如果整个流水线的多个 step 都要用同一个非默认的 model/max_turns/timeout/retry_on_error/allow_parallel,写进顶层 defaults,不要
在每个 step 上重复:
name: data_pipeline
defaults:
model: gpt-4.1-mini
max_turns: 6
steps:
- id: fetch
type: agent
prompt: "..."
# 不写 model/max_turns → 继承 defaults 里的值
- id: analyze
type: agent
prompt: "..."
max_turns: 20 # 这一步比较复杂,单独覆盖成 20,其它仍继承 defaults.model只有单个 step 需要偏离多数 step 的配置时,才在该 step 上单独写这几个
字段——不要把每个 step 都显式写一遍相同的值,那样起不到"改一处生效
全局"的作用,也让 YAML 更啰嗦。
全自动执行模式(mode: autonomous)
用户如果表达了"这个 workflow 要全程自动跑完,中途不能停下来等人""所有
参数在一开始就给全,别问我""要挂后台跑,没人盯着"这类诉求,生成时要做
两件事,缺一不可:
- 顶层写
mode: autonomous:这不是可选的装饰,而是一道保存期的
保险——设了之后,如果steps里混入了会真正阻塞等待人工的写法
(没有input_key的human_input、或require_approval: true),save_workflow/patch_workflow_step会直接校验失败并报出具体是
哪个 step 的问题,而不是等运行到后台才因为没人应答而卡到超时。 - 每个
human_inputstep 必须配input_key:这个 key 对应运行时run_workflow(inputs={...})里的某个字段名,命中就直接取值使用,不
进入阻塞等待。也就是说,autonomous模式下human_input的语义从
"临场问人"变成了"从启动参数里取一个命名字段"——写prompt/input_prompt时仍然要写清楚这个字段该填什么,因为它同时也是运行时
报错提示(require_all_inputs_upfront=true时缺参数会报出这个提示)
和给人类阅读者的说明。
name: nightly_release_check
mode: autonomous
steps:
- id: intake
type: human_input
input_key: release_tag # 必须写,否则保存时报错
input_prompt: "本次要检查的 release tag,如 v1.4.0"
prompt: "release_tag" # 同样会展示给人看,写法不强制但建议和 input_prompt 呼应
- id: run_checks
type: agent
depends_on: [intake]
prompt: |
对 release {intake.output} 跑一遍发布前检查清单,输出结果。不要在 mode: autonomous 的 workflow 里写 require_approval: true
——这个字段没有类似 input_key 的"预置值"逃生舱,只要开着就一定会阻塞
等待人工审批,跟"全自动"的诉求直接矛盾,保存时会被拒绝。如果流程里确实
需要一个人工把关的节点,说明用户描述的其实不是"全自动"场景,应该回去
跟用户确认清楚,而不是硬塞一个 autonomous 顶层字段掩盖矛盾。
如果用户没有明确表达"全自动/挂后台/别问我"这类诉求,不要主动加mode: autonomous——默认的 interactive 已经能覆盖"有 human_input/
审批门、但允许运行时临场交互"的场景,强行改成 autonomous 反而会让
原本合理的 human_input/require_approval 写法在保存时报错。
善用可复用 step 片段(include)
如果这次要生成的 workflow 里有一段 step 组合(比如"打分 → 生成报告"这类
质检套路)明显可能被其它 workflow 复用,或者用户之前已经在.agent/workflow_snippets/ 下存过同类片段,优先复用而不是重新写一遍:
- 先检查有没有现成片段可用:
from pathlib import Path from mini_agent.workflow.store import WorkflowStore store = WorkflowStore(project_root=Path(".")) print(store.list_snippets()) # [{'name': ..., 'step_count': ..., 'steps': [...]}] - 有现成的且语义匹配,直接在
steps里用include引用:
工作流里其它 step 想引用这段片段的"整体输出",直接写- id: qc # 这个 id 是片段展开后所有 step 的命名空间前缀 include: quality_check # 片段名,不带 .yaml 后缀 depends_on: [analyze] # 挂到片段"入口" step 上depends_on: [qc]/{qc.output}即可(会自动指向片段展开后的
最后一个 step),不需要知道片段内部具体的 step id。 - 没有现成片段、但这段 step 组合确实通用(用户明确表示"以后还要用"),
可以先用store.save_snippet(name, steps)存一份,再在 workflow 里include引用;如果只是这一个 workflow 专用,不要为了"显得工程化"
强行拆成片段,直接内嵌在steps里更直观。 - 不要把
include和该条目自己的prompt/type等字段混着填,include存在时其它字段(除了id/depends_on)会被忽略;也不要
凭空引用一个不存在的片段名,store.load_snippet()找不到文件时会
直接抛错。
Prompt 占位符
{参数名}:运行时由调用方通过run_workflow(inputs={...})传入(如{old_path}),无法静态校验,运行到该 step 且未提供对应 input 时会替换为空字符串。{step_id.output}:引用某个前序 step 的输出文本。{step_id.score}:引用某个前序 step(通常是role_type: evaluator的评估 step)产出的分数。{step_id.output_file}:引用某个前序 stepoutput_file落盘文件的绝对路径字符串(不是内容),适合上游产出较大 JSON、下游只需要提示 Agent 去读文件的场景。validate()会静态扫描这三类占位符,{step_id.xxx}引用了不存在的 step id、或.xxx不是output/score/output_file时会报错——写完 prompt 后要检查占位符拼写。- 引用某个 step 的输出/分数/落盘文件必须同时在
depends_on里声明该 step——这一点现在是强制校验(不再是弱一致性检查):validate()会检查占位符引用的 step_id 是否在当前 step 的depends_on(直接或传递)范围内,写漏会直接报 error,而不是等到运行期才因为该 step 还没跑完而报错。生成 workflow 后务必用wf.validate()确认没有这类报错。 condition表达式里除了{step_id 对应的裸名}.output/.score/.passed这类属性访问外,还有一个始终可见、不受depends_on约束的命名空间inputs.xxx,指向run_workflow(inputs={...})传入的外部参数,如"inputs.env == 'prod'";validate()的静态一致性检查会跳过inputs.*引用,只对引用了其它 step 却没声明depends_on(直接或传递)的情况报错,生成含condition的 step 时留意这一区别。
创建流程(生成 workflow 时遵循)
- 确认需求,向用户澄清(不确定就问,不要瞎猜):
- 这个流水线要完成什么目标,分成几个逻辑阶段
- 每个阶段该由谁执行:按"选型强制规则"(见上文)先判断能不能用
python_step(配合ctx.llm/ctx.llm.ask_json)解决;不能的话再看
能不能约束成skill_agent(固定挂载某个技能,是否要用工作流私有
skill);纯确定性操作用tool_call;只有确实需要临场应变(工具选择/
页面结构等运行时才能确定)才落到agent(是否要用工作流私有 agent,
即role_agent)——agent/role_agent是兜底选项,不是默认答案;
还可能是某个插件注册的自定义类型 - 阶段之间的依赖关系(
depends_on)、是否有需要人工确认的关卡
(human_input/require_approval)、是否有质检门(role_type: evaluatorcondition: "xxx.score >= N")
- 运行时需要哪些外部输入参数(如文件路径),会被哪些 step 的 prompt 引用
- 这个流程是否要全自动跑完、中途不能停下来等人(比如挂后台批处理、
定时任务),是的话顶层要写mode: autonomous,且所有human_input
step 都要配input_key(对应到"运行时需要哪些外部输入参数"里问到的
那些参数名),不能再用require_approval: true,见下方"全自动执行
模式"一节 - 是否有明显重复、可能被其它 workflow 复用的 step 组合(考虑
include);多个 step 是否会共用同一个非默认的model/max_turns/timeout/retry_on_error/allow_parallel
(考虑顶层defaults);是否需要限制单次执行的总 token 用量
(max_total_tokens,通常只在用户明确担心成本失控时才主动问)
- 选择模式:默认文件夹模式(见上方"如何选择"),按用户要求调整。
- 检查可复用片段:
WorkflowStore.list_snippets()看有没有现成的include片段可以直接用,避免重复造轮子(见上方"善用可复用 step
片段")。 - 创建目录结构:
.agent/workflows/<name>/,按需创建agents/、skills/、prompts/。 - 写
prompts/*.md(如果用文件夹模式且 prompt 较长):每个文件只放
一个 step 的 prompt 正文,占位符规则同上。简短的 prompt(一两句话)
可以直接内嵌prompt字段,不强制拆文件。 - 写
agents/<role>.md(如果某个 step 需要专属角色):格式参考.claude/skills/agent-generator/SKILL.md(frontmatter: name/description/
tools/inputs,正文用{参数名}/{context}占位符)。这一份只服务于当前
workflow,description 里可以写清楚"这是 xxx workflow 私有的 agent"。 - 写
skills/<skill-name>/SKILL.md(如果某个 step 需要强制挂载某项
能力):格式参考.claude/skills/reminder-generator/SKILL.md等现有
skill(frontmatter: name/description/triggers,正文是技能使用说明)。
配合type: skill_agent+ 对应skill_name使用。 - 写
workflow.yaml:按上方字段表填写,steps里体现第 1 步确认的
拓扑结构;多个 step 共享的非默认配置提到顶层defaults,明确要复用的
step 组合用include引用。 - 自检(写完后必须做,不要跳过):
- 逐个检查
agent/role_agent类型的 step:能不能改写成python_step(配合ctx.llm.ask_json)或收紧成skill_agent?改不了
的话明确这是"需要临场应变"(见"选型强制规则"),不是图省事的默认写法 - 每个
id唯一、depends_on引用的 id 都存在(include条目的id
算作其它 step 引用它的入口 id,不需要等于片段内部任何一个 id) prompt/prompt_file/include三选一至少有一个非空(human_input
例外)prompt_file指向的文件路径确实存在(相对 workflow 目录)include引用的片段名在.agent/workflow_snippets/下确实存在type专属字段都填了(sub_workflow→workflow_name、tool_call→tool_name、script→script、skill_agent→skill_name、python_step→script_path、foreach→items+foreach_step
(含type)、wait→wait_seconds(正数)、merge→merge_sources(非空无重复));用了插件自定义类型的话,该类型专属的
必填字段(看插件的validate_step())也要填foreach_step.type不是foreach(禁止嵌套);foreach_max_concurrency
若填写须>= 1merge_sources/tool_args里引用的 step id(含占位符{step_id.xxx}
形式)都出现在该 step 的depends_on(直接或传递)范围内python_step的script_path指向的文件确实存在(相对 workflow
目录),且脚本内暴露了def run(ctx) -> str|dict入口函数;用了python_step的话记得提示用户显式开启python_step_enabledsub_workflow没有引用自身mode: autonomous的 workflow:每个human_inputstep 都配了input_key,且没有任何 step 写require_approval: true(这两点validate()会校验,但生成时自己先过一遍能提前发现问题)- prompt 里的
{step_id.output}/{step_id.score}占位符拼写正确、
引用的 step 确实存在(include片段展开后新增的前缀__原id
形式的 id 也要留意,不要在片段外直接用未加前缀的原始 id 引用) - 尽量用代码验证而不是纯人工检查:
from pathlib import Path from mini_agent.workflow.store import WorkflowStore wf = WorkflowStore(project_root=Path(".")).load("<name>") errors = wf.validate() print(errors) # 空列表才算过关;有 include 的话 load() 已经完成展开, # 这里看到的是展开后的真实校验结果
- 逐个检查
- 提示用户如何验证/运行:
/workflow show <name>查看解析结果- 正式运行前建议先
preview_workflow(name, inputs)做一次 dry-run:
不会真的执行,但会给出并发分批结果、prompt 占位符替换预览、condition静态求值情况和unresolved_placeholders清单——比直接跑
一遍正式执行更省成本,尤其适合刚写完、还不确定inputs是否传全时 /workflow run <name> '{"参数名": "值"}'运行(有需要外部输入的话)- 用户要求"跑完的产出文件放到指定目录"时,用
run_workflow(name, inputs, output_export_dir="<绝对路径>"):跑到
终态后会自动把本次output/目录下的文件复制过去,不用手动 cp
(复制失败不影响 workflow 本身的执行结果,只在返回信息里提示) - 不想进交互 REPL、只想在 shell/cron/systemd 里直接跑,把前缀换成
mini-agent workflow ...即可(子命令名和参数一致),如mini-agent workflow run <name> '{"参数名":"值"}' --background --project <path>;--background在这条路径下会 spawn 一个独立 OS
子进程,父进程立刻返回、子进程即使触发它的 shell 已退出也会跑完——
这条路径本身的排查/调试属于 workflow-debugger skill,这里只需要知道
它存在、什么时候该推荐给用户(用户说"写个脚本/加个定时任务跑这个
workflow"时) mode: autonomous的 workflow,建议提示用户可以用run_workflow(name, inputs='{...}', require_all_inputs_upfront=true)
启动——所有input_key对应的值都要在inputs里给全,缺了会在
启动前直接报错列出缺哪些字段,而不是跑到一半才卡住;需要这次运行
强制不并发时可以加force_serial=true- 已有单文件工作流想升级为文件夹模式,用
/workflow to-dir <name>
(自动建agents//skills/prompts空目录,原 YAML 移入workflow.yaml) - 生成/保存完之后,如果用户想先验证某个 step 的 prompt 措辞是否合适、
或者运行后某个 step 失败(状态failed/needs_fix)需要排查、
修改后要不要重跑等——不属于本 skill 覆盖的范围,也不需要重新走一遍
本 skill 的生成流程,改用 workflow-debugger skill
(test_workflow_step沙箱测试单步、get_workflow_run_status/get_workflow_stats排查、patch_workflow_step改定义、resume_workflow_run续跑)
常见坑
- 默认写
type: agent是最常见的错误取舍:只要一个阶段的描述看起来是
"解析/过滤/打分/重排结构化数据",先假设它应该是python_step,而不是
因为不确定就顺手落到agent;生成完之后回头检查一遍steps里的agent/role_agent,每一个都应该能说清楚"为什么这一步不能用python_step/skill_agent"(见上文"选型强制规则"),说不清楚就是生成
错了,应该改写。 prompt_file路径是相对 workflow 目录的相对路径(如prompts/analyze.md),
不是相对项目根目录,也不要写成绝对路径。prompt和prompt_file都填时prompt_file优先——文件夹模式下如果
两个都写了,prompt字段的内容会被完全忽略,不会合并。allow_parallel/max_turns/model/timeout/retry_on_error不写
≠ 一定是旧的硬编码默认值:现在是"本字段 → 顶层defaults→ 硬编码
兜底"三层继承,如果这个 workflow 写了defaults,某个 step 不写这几个
字段时实际生效的是defaults里的值,不是表格里写的硬编码兜底值——生成
时如果想让某个 step 明确不跟随defaults、强制用某个值,必须在该
step 上显式写出来,不能靠"不写"。include展开是加载期行为,保存时看到的还是未展开的include:声明:save_workflow/WorkflowStore.save()落盘的 YAML 里仍然是include: <片段名>,只有load()(或/workflow show/run_workflow)
才会展开成真实 step;直接读磁盘上的 YAML 文件人工检查时,不要误以为
片段没生效。- 子工作流不继承父工作流的本地资源包:
type: sub_workflow引用的另一个
workflow,如果需要用到私有 agent/skill,必须在它自己的目录下也放一份,
不能指望"父工作流有就行"。 role/skill_name的本地优先级:本工作流agents//skills/目录下
同名资源优先于全局/项目级同名资源,这是"覆盖"而不是"合并成一个"——如果
本地和全局都有reviewer.md,实际生效的是本地这一份。skill_agent和"关键词触发"不是一回事:type: skill_agent是强制
挂载,不管 prompt 里有没有命中该 skill 的triggers关键词都会生效;如果
只是希望"主 Agent 在需要时自动触发某个 skill",用普通type: agentstep
就够了,不需要skill_agent。mode: autonomous不是"随手加的保险栓":只有用户明确表达了全自动/
挂后台诉求时才加;加了之后忘记给human_input配input_key,或者
留了个require_approval: true,save_workflow会直接拒绝保存——这是
故意设计成"保存期报错"而不是"运行期才发现",生成时按上面的自检清单过
一遍就能避免。反过来,不需要全自动时也不要为了"看起来更完善"顺手加mode: autonomous,那样会让原本合理的human_input/require_approval
写法保存不了。skill_agent的产出要喂给下游python_step时忘了配result_file:
没有result_file时下游只能拿到对话原文,靠正则/脆弱解析拿 JSON 迟早
会炸;只要下游需要结构化数据,就应该配result_file+result_file_required_keys,见上面"skill_agent:务必配result_file"
一节。- 把批处理逻辑写进
python_step手写循环,而不是用foreach:只要发现
一段脚本里在for循环调用ctx.run_agent_turn()/重复调 LLM 处理列表里
每个元素,就应该改成type: foreach+foreach_step,让 YAML 能直接
看出这里是批处理,也方便用test_workflow_step单独验证一个元素的处理
逻辑。 foreach_step写成嵌套foreach:validate()会直接拒绝,遇到
"批处理里还要批处理"的需求应该重新设计粒度,不要硬套。merge_sources忘了同步写进depends_on:跟 prompt/tool_args
占位符一样,这是保存期强制校验,写漏会直接报 error。- 多分支结果汇总还在 prompt 里手写
{a.output}{b.output}拼接:这类
需求现在应该用type: merge,让"这里是汇聚节点"在 YAML 里显式可见,
调试时也能单独测这一步的汇聚逻辑。 condition引用了还没跑完/类型不对的字段:现在求值异常会被判定为NEEDS_FIX而不是SKIPPED(P12),生成时不确定某个.output/.score
字段此刻是否已就绪,先用preview_workflow验证,不要凭感觉写。tool_call的tool_args想传上游结构化字段却整段转成文本塞给
prompt:tool_args的字符串值(含嵌套 dict/list)现在支持占位符
(P12),应该直接写{step_id.output}/{step_id.result_file:a.b.c},
不需要退化成"把整个 prompt 当第一个参数"的旧写法。- 不确定字段语义时,直接看
.agent/workflows/doc_change_review/这个完整
可跑的例子,或者读src/mini_agent/workflow/schema.py顶部的 docstring
和docs/workflow-guide.md"文件夹模式 Workflow"一节。
示例:文档变更审查工作流(参考本仓库已有实例)
.agent/workflows/doc_change_review/ 是一个可以直接运行的完整例子,
四个 step 分别覆盖了 agent(主 Agent + prompt_file)、skill_agent
(调用本地 changelog-diff skill)、role_agent(调用本地 reviewer
agent)、以及最后再用 agent 汇总生成报告并带 condition。生成新
workflow 时,结构上可以直接照这个例子的骨架改。
示例:全自动 workflow(mode: autonomous,供对比)
如果用户明确要求"要挂后台跑、中途别停下来问我":
# .agent/workflows/nightly_release_check/workflow.yaml
name: nightly_release_check
description: 全自动的发布前检查:给定 release tag,跑完检查清单并生成报告
version: "1.0"
mode: autonomous
steps:
- id: intake
name: 接收参数
type: human_input
input_key: release_tag
input_prompt: "本次要检查的 release tag,如 v1.4.0"
prompt: "release_tag"
- id: run_checks
name: 执行检查清单
type: agent
depends_on: [intake]
prompt: |
对 release {intake.output} 跑一遍发布前检查清单(依赖版本、
变更日志、回归测试结果),逐项给出通过/不通过的结论。
- id: report
name: 生成报告
type: agent
depends_on: [run_checks]
prompt: |
基于以下检查结果,生成一份简明的发布检查报告:
{run_checks.output}启动方式:run_workflow("nightly_release_check", '{"release_tag": "v1.4.0"}', background=true, require_all_inputs_upfront=true)——intake 这个human_input step 会直接从 inputs.release_tag 取值,不会阻塞等待。
示例:最小两步骤 workflow(单文件模式,供对比)
如果用户明确要求"简单点,就一两步",可以退回单文件模式:
# .agent/workflows/quick_summary.yaml
name: quick_summary
description: 对一段文本做摘要 + 一句话点评
version: "1.0"
steps:
- id: summarize
name: 摘要
type: agent
prompt: |
请用 3 句话概括以下内容:
{text}
- id: comment
name: 点评
type: agent
prompt: |
基于以下摘要,给一句简短的点评(褒贬皆可,说明理由):
{summarize.output}
depends_on:
- summarize