版本说明
- Spec 版本:3.18(文档版本)
- Workflow JSON 格式版本:过渡期兼容
3.16、3.17与3.18;当前已发布的 VLCode-Lite JS 引擎仍以"version": "3.16"为主- 兼容性规则(3.18 延续):规范升级先于所有运行时升级。只要 workflow 未使用更高版本独占能力,现有
3.16workflow 应继续可运行;运行时应优先按 capability 判断,而不是只按版本字符串拒绝加载- 3.18 相比 3.17 的主要差异:
- 明确 DocCenter 引用优先级:workflow 中
registry.docs与 stepin.docs仍以保留 path 号表达语义,但运行时必须优先使用用户配置的 Doc ID 覆盖,再退回 path→docId 目录解析- 明确 官方文档与用户文档解耦:官方核心 spec / workflow docs 通过稳定
Doc ID固定;path 仅作为逻辑别名与默认槽位- 补充 IDE Settings 配置位约束:
VL Syntax、Theme与 workflow docs 可由用户替换其 Doc ID;Meta Spec与Workflow Spec可由 IDE/平台锁定- 新增
Tool_*标准扩展剖面:workflow 可直接调用本地/宿主工具,支持tool/toolName、input/in、timeout、allowError等字段- 新增
Subflow_*显示/运行别名:在 editor 与运行时中可作为WorkflowRun的语义化别名使用,用于突出“子工作流”而不引入新的底层执行原语- 新增 工具运行时事件:
tool_start/tool_message/tool_done/tool_error,要求向外层窗口、SSE 消费方、日志系统完整暴露- 新增 稳定运行标识
clientRunToken:在真实runID尚未分配前,即可对并行 run、pause/resume、rerun 做稳定归属- 明确 checkpoint 续跑 / rerunFromStep 的 fork、loop、overrides 语义,允许从复杂分支中的指定节点向下继续执行
- 补充 workflow-of-workflows 编排模式:父 workflow 可通过工具或宿主能力同步/异步调度子 workflow,并透传结构化事件
对 registry.docs、step in.docs、以及任何 workflow 内部文档引用,运行时必须按以下顺序解析:
Doc IDDoc IDpath -> latest docId 的回退解析约束:
"1", "2", "141" 等值,语义上是 保留 path 槽位Doc IDDoc ID 时,运行时不得再用本地旧文档或 path fallback 覆盖Workflow 是 VL 平台中的一种通用流程编排机制,用于以结构化、可执行的方式描述和运行多步骤流程。它既可以用于开发阶段的流程约束,也可以作为应用运行时的一部分,对外提供能力。
Workflow 本身是一种程序化定义的流程,可被前端、后端服务或工具环境触发执行,并根据使用场景的不同,呈现为不同的承载形式与生命周期。
在 VL 平台中,Workflow 主要覆盖以下四类用途:
| 场景 | 承载位置 | 使用对象 | 是否项目资产 | 是否参与编译 | 调用方式 | 主要作用 |
|---|---|---|---|---|---|---|
| IDE 内 agent flow(无服务调用) | Process/ | 开发者自用 | 是 | 否 | IDE 内调用 | 约束 IDE 内的开发流程,支持基于项目上下文的 Agent 执行 |
| 业务 workflow(可以调用服务) | Workflows/ | 应用用户 | 是 | 是 | 前端调用 / 服务内调用 | 编排后端流程,作为应用能力对外提供 API 或自动化服务 |
| 审批流(可以调用服务) | Workflows/ | 应用用户 | 是 | 是 | 前端调用 / 服务内调用 | 支持长流程执行,允许 pause / resume 与人工介入 |
| 本地自用 workflow | LocalWorkflows/(建议) | 开发者自用 | 否 | 否 | 本地工具调用 | 作为用户本地或个人使用的自动化工具 |
当 workflow 用于 IDE 内 agent flow(典型承载路径 Process/)时,必须满足:
steps 中不得出现 Service_* 节点;出现即编译/加载报错registry.services 必须为空数组(或在实现允许时省略)Workflows/)不受此限制Workflow 的整体设计目标是:
Workflow 在 VL 平台中的运行模型遵循 "执行无状态,状态外置" 的原则。
从定义层面看,Workflow 本身不包含持久化状态,也不描述状态如何存储或演进。
同一个 Workflow 定义可以被多次、并行地执行,而不会因历史运行而产生语义差异。
在运行层面,Workflow 可以在执行时选择性地挂载一个外部状态空间,用于承载执行过程中所需的上下文或产物,例如文件、运行中间结果或流程进度信息。
该状态空间具有以下特征:
当未挂载状态空间时,Workflow 仍可正常执行,其行为仅依赖运行参数与即时计算结果。
当挂载状态空间时,Workflow 可以在该空间中读写数据,但仍保持执行引擎本身的无状态特性。
通过将状态与执行解耦,VL Workflow 既能够支持短时、一次性的流程执行,也能够支撑需要持续上下文的复杂场景,而无需引入重量级的内置状态机机制。
Workflow 支持在执行过程中产生多步输出(Multi-step Output),用于对外暴露流程的中间进度、阶段性结果或执行事件。
多步输出的核心目标是: 在不阻塞流程执行的前提下,使外部系统能够感知并响应 Workflow 的执行过程。
Workflow 的多步输出具有以下特性:
children 并行分支或 Loop_* mode:"parallel"),输出事件的顺序取决于各节点的实际完成时间,不做确定性顺序保证多步输出的消费方式不限定于前端场景,可根据运行环境选择不同的承载方式:
Workflow 本身不关心多步输出的最终消费方,也不依赖具体的传输机制。
无论输出被前端直接展示,还是被后端服务订阅处理,其语义均保持一致。
通过多步输出机制,Workflow 可以自然支持:
控制流由两部分组成:
用于描述"节点之间如何连接、并发以及是否执行":
next:串行后继节点(显式继续信号)children:并行子分支入口列表(并行扇出 + 汇合)if:节点执行条件(条件不满足时跳过节点本体及其 children 子树)规则取向:
next 必须显式写出,支持特殊语义如"RETURN"和"BREAK"Stop_*节点,其他节点必须有 next ,否则spec编译报错if 为 false 时:
children 子树next说明:
next / children / if 都是所有节点可拥有的通用属性,不属于某个特定节点类型。用于表达结构化控制语义:
branch:条件分支(选择性执行某一条分支)loop:循环(对集合数据或条件执行重复步骤,支持数组遍历模式与 while 条件模式)说明:
branch/loop是"节点类型(kind)",它们本身也是 steps 中的一种节点。
不设置显式的"开始节点"。入口规则与终止语义如下:
入口节点 = steps 中不被任何其他节点的 next、children、cases 引用的节点。
children 扇出)不设显式 Start 节点的原因:入口点可由结构自动推断,额外的 Start 节点只会增加图上的冗余方块,不携带任何执行语义。
workflow 存在三种互斥的结束状态,其语义明确且不可混用:
| 结束方式 | 触发条件 | 状态 | 是否可恢复 | 调用方含义 |
|---|---|---|---|---|
| 完成(stopped) | 执行到达 Stop_*节点 |
stopped |
否 | 流程正常完成,可获取最终结果 |
| 暂停(paused) | 执行到达 Pause_* 节点 |
paused |
是 | 流程等待外部输入,需要 resume |
| 失败(failed) | 节点执行出错 | failed |
否(需重跑) | 流程异常中断 |
补充说明:
Stop_* 用于表示 workflow 的正常完成,到达即终止,不可恢复。Pause_* 节点用于表示 可恢复的执行中断,不等价于流程结束。failed 表示执行异常,不属于正常控制流的一部分。约束规则:
Stop_* 节点,以保证存在明确的正常完成路径。Stop_* 节点,用于表示不同分支或条件下的提前完成路径。变量用于承载轻量级、结构化的数据,例如参数、标识、状态标志或小规模结果。
变量具有以下特征:
变量主要用于节点之间的数据传递与执行控制,而不是作为持久化产物存在。
$xxx 表示工作流全局变量$vars 在 registry 中声明_xxx 表示局部/循环变量SYSVAR.xxx 引用用户预先配置在系统空间里的通用变量SYSVAR 为只读,常用于密钥、默认配置、偏好参数等Workflow 在运行过程中可以通过 out 将结果写入文件空间。文件用于承载中间产物或结果性输出,例如代码文件、配置文件、文档等,其生命周期和管理方式由**工作空间(Workspace)**决定。
工作空间是 Workflow 运行时可选挂载的外部文件与状态承载空间。当 Workflow 运行时指定了 workspaceId,其所有文件写入行为将发生在该工作空间中;未指定时,系统会为本次运行创建临时文件空间。
工作空间的存在与否、以及具体使用哪个工作空间,均由运行时参数决定,而不是 Workflow 定义的一部分。
一个工作空间在逻辑上分为两个部分:
系统空间用于存储与工作空间自身相关的系统级与流程级元数据,例如:
系统空间由平台自动管理:
Workflow 在运行过程中可以使用系统空间承载必要的运行状态,但不直接暴露其结构与内容。
产物空间用于存储 Workflow 运行过程中产生或修改的业务文件与结果文件,例如:
通过 out 写入的文件路径(以 / 开头),最终都会落入产物空间。
产物空间具有以下特征:
针对产物空间,工作空间本身提供版本管理能力,包括但不限于:
这些能力属于工作空间自身的能力,而不是 Workflow 的职责:
Workflow 的职责仅限于"在当前工作空间状态下生成或修改文件"。
工作空间支持加锁机制,用于控制并发访问与写入冲突。
该机制用于保证:
通过引入工作空间机制,VL Workflow 能够在保持执行模型无状态的前提下,支持复杂的文件生成、修改与长期演进场景。
Workflow 在定义完成后,可以在每一次运行(run)时通过运行时参数对本次执行行为进行控制。运行时参数不属于 Workflow 定义本身,而是每次 run 的附加上下文,用于让同一份 Workflow 在不同场景下以不同方式执行。
运行时参数主要包含以下几类:
params(可选:业务入参)用于向 workflow 传入业务数据。
registry.params 中声明workspaceId(可选:挂载文件空间)用于指定本次运行所使用的外部文件空间。
out 写入的文件路径(以 / 开头)将落到该临时空间;运行结束后按 TTL 自动清理。out 写入的文件路径将落到该 workspaceId 对应的文件空间中,文件持续保留,供后续运行或人工操作使用。
workspaceId只影响本次 run 的文件落点,不改变 Workflow 定义。
nodes(可选:部分执行 / 重跑)用于指定本次运行中实际参与执行的节点列表。
该参数用于支持:
mode(可选:运行模式)用于指定本次运行的整体执行模式。该模式为引擎提供统一的运行策略,用于影响节点的行为(尤其是文件写入相关行为)。
常见模式包括:
create:用于初始化或新增产物patch:用于增量修改regenerate:用于重建/重写validate:仅校验,不产生文件写入运行模式是 run 级别的策略参数,不属于
out的一部分。
params 传入业务数据,workspaceId、nodes、mode 控制执行行为与文件落点out 的一层映射同时支持变量与文件输出:
$... 写入变量/... 写入文件空间(临时或 workspace)通过运行时参数机制,Workflow 能在保持结构稳定的同时,支持新项目生成、增量修改、局部重跑与校验等多种运行方式。
// 示例:Workflow 的运行时参数(runParams)
// 说明:
// - 该示例展示一次"增量修改"的运行方式
// - 同一份 Workflow 定义,在不同 runParams 下可以表现出完全不同的执行行为
{
// 要执行的 workflow 定义
"workflowId": "codegen_apply_v1",
// 本次 workflow run 的运行时参数
"runParams": {
// 可选:业务入参(对应 registry.params 中声明的入参)
"params": {
"userRequest": "帮我生成一个登录页面",
"targetLang": "zh-CN"
},
// 可选:指定外部文件空间
// - 不指定:使用临时文件空间(有 TTL)
// - 指定:文件写入落到该 workspace 中
"workspaceId": "project_workspace_123",
// 可选:仅执行部分节点
// 用于增量执行、失败后重跑、或按规划指令执行
"nodes": [
"blueprint",
"contract",
"file_backend"
],
// 可选:运行模式(run 级别策略)
// - create 初始化 / 新建
// - patch 增量修改
// - regenerate 重建 / 重写
// - validate 仅校验,不写文件
"mode": "patch"
}
}
Workflow 的执行引擎采用 "就绪即执行"(Eager Execution) 的调度策略:
核心规则:当一个节点的所有输入条件(前置依赖)已经全部满足时,该节点即可立即开始执行,无需等待与其无依赖关系的其他节点完成。
这意味着 Workflow 的实际执行顺序不是简单的线性序列,而是由 数据依赖关系 决定的偏序执行图(Partial Order)。多个彼此无依赖的节点可以被引擎同时调度和执行。
在设计和生成 workflow.json 时,应 尽量利用并行性,具体原则如下:
children 扇出,或在 Loop_* 中使用 mode:"parallel")。next 链。只有当节点 B 确实依赖节点 A 的输出(如 B 的 in 引用了 A 写入的 $vars)时,才应将 A → B 设为串行。in 中引用了 A 通过 out 或 Set_* 写入的全局变量($xxx)Write_* 或 out 写入的文件next 指向 B)children 并行扇出Loop_* mode:"parallel"next 串行连接// ❌ 不推荐:无依赖关系却串行排列
{
"id": "LLM_GenFrontend",
"in": { "messages": [{ "role": "user", "content": "=$prd" }] },
"out": { "$frontendCode": "=_result" },
"next": "LLM_GenBackend" // GenBackend 并不依赖 GenFrontend 的输出
}
// ✅ 推荐:使用 children 并行执行无依赖节点
{
"id": "Set_StartGen",
"target": "$status",
"value": "\"generating\"",
"children": ["LLM_GenFrontend", "LLM_GenBackend"],
"next": "Service_Merge"
}
children 中列出的节点天然并行;但即使是通过 next 串联的节点,如果引擎检测到无实际数据依赖,也 允许 提前调度(但这属于引擎优化,不改变语义正确性)steps 数组中的顺序不影响执行顺序,执行顺序完全由 next / children / branch / loop 等控制流结构以及数据依赖关系决定通过就绪即执行策略,Workflow 能在保持语义正确性的同时,最大化执行并行度,显著提升整体吞吐效率。
Workflow 在执行过程中可能遇到各类异常情况,包括但不限于:
当节点执行发生不可恢复的错误时,workflow 进入 failed 状态:
failed 不同于 paused(由 Pause_* 节点触发的可恢复挂起)和 stopped(正常终止)当 workflow 进入 failed 状态时,引擎返回:
status: "failed"failedAt: "<stepId>"(失败节点的 id)error: { code: "<errorCode>", message: "<errorMessage>" }vars: {...}(失败时的全局变量快照,用于调试与重跑)failed 状态下,可通过运行时参数 nodes 指定失败节点及其后续节点进行局部重跑,实现失败恢复。
onError:节点失败时,若配置了 onError,则转入指定失败处理节点;未配置时保持 fail-fastWorkflow 在可视化时表现为流程图(Graph),每个节点对应图上的一个方块,每条 next / children 对应一条连线。为了保持流程图的 可读性与稳定性,在设计和生成 workflow.json 时应遵循以下简洁性原则:
调用型节点(Service_* / API_* / Component_* / LLM_*)自身已具备通过 out 写入变量和文件的能力。 当变量写入或文件写入的内容来源于当前节点的 _result 时,应直接在该节点的 out 中完成,不应 额外创建独立的 Set_* 或 Write_* 节点。
// ❌ 不推荐:多余的 Set 节点,增加了图上的方块和连线
{ "id": "LLM_GenCode", "in": {...}, "out": "$rawResult", "next": "Set_ExtractStatus" }
{ "id": "Set_ExtractStatus", "target": "$status", "value": "=$rawResult.status", "next": "Stop_End" }
// ✅ 推荐:在 out 中直接完成映射,图上只有一个节点
{ "id": "LLM_GenCode", "in": {...}, "out": { "$rawResult": "=_result", "$status": "=_result.status" }, "next": "Stop_End" }
Set_* / Write_* 节点独立状态写入节点仅在以下场景中使用:
_result — 例如需要拼接多个变量、写入常量、或进行纯计算Branch_* 之前设置分支条件变量,或在 Loop_* 之前初始化计数器Write_* 的 mode:"append" 或 mode:"failIfExists",这些是 out 文件写入不支持的Set_* 后接 children 并行启动Set_* 节点的唯一作用是设置变量然后立刻 next 到下一个节点,且该变量可以在前一个调用型节点的 out 中完成,则该 Set_* 节点应被合并Branch_* / Loop_*)本身代表流程结构语义,应始终作为独立节点存在Stop_* 代表明确的终止语义,应始终作为独立节点存在children 扇出,而不是串行排列后依赖引擎优化Set_* 节点作为扇出锚点(设置状态标志),避免为此创建一个空的 Service_* 调用| 场景 | 推荐做法 | 避免做法 |
|---|---|---|
| 调用节点输出写变量 | 在 out中直接映射 |
创建额外 Set_* |
| 调用节点输出写文件 | 在 out中使用 /path写入 |
创建额外 Write_* |
| 纯计算 / 拼接写入 | 使用独立 Set_* |
无法内联,独立节点是正确选择 |
| 特殊写入策略 (append) | 使用独立 Write_* |
out不支持 mode,独立节点是正确选择 |
| 多个无依赖调用 | children并行扇出 |
next串行排列 |
| 条件判断 | 独立 Branch_*节点 |
不应合并到其他节点 |
工作流以一个 JSON 对象表示,顶层结构固定如下:
{
"version": "当前版本号",
"name": "string",
"registry": { },
"steps": [ ]
}
version(必填)"当前版本号"name(必填)stringregistry(必填)objectsteps(必填)array<Step>Step 的结构在后续章节定义registry 与 steps 必须同时存在steps 必须非空next / children / branch / loop 等显式结构(因此数组顺序仅用于阅读与 diff)workflow 的 steps 在结构上构成一个有向图(Directed Graph)。为确保所有节点都有意义且可执行,引擎在加载 workflow 时应执行以下校验:
next、children、cases 引用的节点(见 1.4 C 节)next、children、cases(含 ELSE)的引用链能够到达该节点next、children、cases 中引用的 step id 必须在 steps 中存在Stop_* 校验Stop_* 节点(缺失时引擎发出警告)Stop_* 不允许有 next 或 children(若存在应报错)| 校验项 | 级别 | 说明 |
|---|---|---|
| 入口节点 ≥ 1 | ERROR | 无入口则无法启动 |
| 所有节点可达 | ERROR | 不可达节点是死代码 |
| 引用 id 存在 | ERROR | 断链导致运行时崩溃 |
| id 不重复 | ERROR | 控制流歧义 |
| 至少一个 Stop_* | WARNING | 纯审批流可能无 Stop |
| Stop_* 无 next/children | ERROR | 语义冲突 |
| 检查项 | 规则 | 级别 |
|---|---|---|
| while 与 source 互斥 | 同一 Loop_* 同时声明 while 与 source |
ERROR |
| while 必填项 | while 模式缺少 maxIterations |
ERROR |
| maxIterations 范围 | maxIterations < 1 |
ERROR |
| while mode 约束 | while + mode:"parallel" |
ERROR |
| BREAK 作用域 | BREAK 出现在非 Loop_* children 子树 |
ERROR |
| BREAK 保留字 | BREAK 被用作 stepId |
ERROR |
| LLM model 空值 | LLM_* 节点 model 为空字符串 |
ERROR |
| LLM model 分段 | model 含 / 但任一侧为空 |
ERROR |
registry 用于声明 workflow 运行所需的外部依赖与全局边界,并强制执行"先注册后使用"。workflow 只负责执行逻辑,不内置任何特定业务闭环(如 git commit、审批提交、触发下一个流程等),这些均由 workflow 外部系统完成。
{
"params": [ ... ],
"services": [ ... ],
"apis": [ ... ],
"components": [ ... ],
"vars": [ ... ],
"files": {
"inputs": [ ... ],
"artifacts": [ ... ]
},
"docs": { ... },
"schemas": { ... }
}
params(选填)声明 workflow 接受的运行时入参。入参由调用方在 runParams.params 中传入,workflow 内部以只读变量形式访问。
params 是字符串数组,每个元素声明一个入参名称与类型,可带默认值:
"params": [
"userRequest(STRING)",
"targetLang(STRING)",
"maxRetries(INT) = 3"
]
$ 前缀,与 VL 语法中服务/方法的入参风格对齐runParams.params 中可省略;无默认值的入参为必填,缺失时引擎报错=userRequest),不可出现在 out 左侧或 Set_* 的 target 中registry.vars 中的全局变量名冲突services(必填)声明 workflow 会调用的项目内服务,并在 registry 中明确其入参/出参契约(用于静态校验与 AI 生成约束)。
services 是字符串数组,每个元素是一条服务签名:
"services": [
"PlannerService(prd(STRING), rulesFile(FILE_REF)) RETURN plan(OBJECT)",
"WriteFileService(path(STRING), content(STRING)) RETURN ok(BOOL)",
"ApprovalService(form(OBJECT), policy(STRING)) RETURN decision(STRING), comment(STRING)"
]
ServiceName(...) RETURN ... 为固定格式paramName(Type) 表示,可多个resultName(Type) 表示,可多个ServiceName 必须唯一Service_* 节点的 id 中 Service_ 后缀部分必须匹配 registry.services 中的某个 ServiceNameapis(选填)声明 workflow 会调用的第三方外部 API,并在 registry 中明确其端点、方法与认证方式。
API_* 节点与 Service_* 的区别在于:Service_* 调用的是 VL 项目内部的服务(由项目自身定义和部署),而 API_* 调用的是项目外部的第三方 HTTP 接口(如支付网关、地图服务、天气 API、SaaS 平台 API 等)。
apis 是对象数组,每个元素声明一个第三方 API 端点:
"apis": [
{
"id": "StripeCreateCharge",
"method": "POST",
"url": "https://api.stripe.com/v1/charges",
"auth": "SYSVAR.stripeApiKey",
"headers": { "Content-Type": "application/x-www-form-urlencoded" },
"desc": "Stripe 创建收费"
},
{
"id": "WeatherQuery",
"method": "GET",
"url": "https://api.weatherapi.com/v1/current.json",
"auth": "SYSVAR.weatherKey",
"desc": "查询当前天气"
},
{
"id": "SlackPostMessage",
"method": "POST",
"url": "https://slack.com/api/chat.postMessage",
"auth": "SYSVAR.slackBotToken",
"headers": { "Content-Type": "application/json" },
"desc": "发送 Slack 消息"
}
]
| 字段 | 必填 | 类型 | 说明 |
|---|---|---|---|
| id | 是 | string | API 唯一标识,API_*节点 id 的 API_后缀部分必须匹配此 id |
| method | 是 | string | HTTP 方法:GET / POST / PUT / PATCH / DELETE |
| url | 是 | string | API 端点 URL(可包含路径参数占位符,如 {orderId}) |
| auth | 否 | string | 认证凭据来源(通常引用 SYSVAR.xxx),引擎负责注入到请求头 |
| headers | 否 | object | 固定请求头(键值对),每次调用自动携带 |
| desc | 否 | string | 人类可读的用途说明 |
id 必须唯一API_* 节点的 id 后缀必须匹配 registry.apis 中的某个 idauth 引用的凭据建议存放在 SYSVAR 中(系统级密钥管理),不应明文写入 workflow 定义url 中的路径参数(如 {orderId})在运行时由节点 in.pathParams 替换components(必填)声明 workflow 会调用的系统内置组件能力(例如 MCP、文件能力等)。
"components": ["FileOps", "MCP_Search", "MCP_VectorDB"]
规则:
componentId 必须唯一Component_* 节点引用的 componentId 必须在该列表中vars(必填)声明 workflow 可读写的全局变量集合(VL 风格)。
"vars": ["$keyword(STRING)", "$items([OBJECT])", "$result(OBJECT)", "$count(INT)"]
规则:
$ 开头$xxx 必须在此声明set.target 与节点 out 只能写入已声明的 $xxxfiles(必填)声明 workflow 的文件读写边界(只读输入 + 临时产物)。
"files": {
"inputs": ["Process/PRD.json", "Process/Rules/*"],
"artifacts": ["Process/Artifacts/*"]
}
inputs(只读)规则:
inputs 范围内artifacts(临时可写)规则:
artifacts 范围内docs(语义文档引用)registry.docs 用于声明 workflow 可能引用的语义文档标识。这些文档不以"文件路径"形式暴露给 workflow,而是通过 稳定的 docId 引用。
docs 是 语义级引用表,不是文件系统docs 的作用是:为 LLM / service 提供稳定、可复用的背景知识引用docs 是一个对象,key 为 docId(字符串或数字),value 为该文档的语义说明:
"docs": {
"11": "VL 语法与表达式规则",
"12": "Workflow v2.x 设计约束说明",
"20": "前端组件生成规范"
}
说明:
docId 必须在 workflow 内唯一在 workflow 中,节点只引用 docId,不引用路径或内容。
常见用法(示意):
{
"id": "LLM_Generate",
"in": {
"docs": ["11", "20"],
"messages": [
{ "role": "system", "content": "请严格遵守相关规范。" }
]
}
}
语义:
"11" 表示「VL 语法与表达式规则」"20" 表示「前端组件生成规范」registry.docs 中声明files.inputs 的关系| 对比项 | files.inputs | docs |
|---|---|---|
| 暴露形式 | 文件路径 | docId |
| 是否 IO | 是 | 否 |
| 是否参与计算 | 是 | 否 |
| 主要使用者 | Service / Component | LLM(为主) |
| 关注点 | 数据 | 语义 / 规则 / 规范 |
LLM_* 节点是例外:不需要在 registry 中注册。其模型通过节点 model 字段与运行环境默认规则解析schemas(JSON Schema 复用,选填)registry.schemas 用于集中声明可复用的 JSON Schema,供 LLM_* 节点在 output_config.format.schemaRef 中引用。
"schemas": {
"SpecSchema": { "type": "object", "additionalProperties": false, "properties": { ... } },
"PlanSchema": { "type": "object", "additionalProperties": false, "properties": { ... } }
}
规则:
schemaId(key)在 workflow 内唯一schemaRef 必须能在 registry.schemas 中找到schema 与 schemaRef,建议视为错误(避免歧义)schemaRef 归一化展开为 format.schema本规范采用“本地 SDK 内置”模式:LLM provider 由引擎内置 SDK 调用,不经由统一 LLM_URL 网关。
规则:
registry 中声明模型列表或 API keyLLM_* 节点通过 model 字段声明 provider/model;model 允许以下三种写法:
model:使用运行环境全局默认 LLM_MODEL(格式为 <provider>/<modelId>)model: "<provider>":仅声明 provider,modelId 取运行环境 <PROVIDER>_MODELmodel: "<provider>/<modelId>":节点内完全指定OPENAI_API_KEY、ANTHROPIC_API_KEY)与默认模型值workflow 只声明流程逻辑与输出约束(如 output_config、schemaRef),不声明密钥最小 .env 示例:
LLM_MODEL=anthropic/claude-sonnet-4-5-20250929
ANTHROPIC_API_KEY=sk-ant-xxx
OPENAI_API_KEY=sk-openai-xxx
ANTHROPIC_MODEL=claude-sonnet-4-5-20250929
OPENAI_MODEL=gpt-4.1
steps 中所有节点分为三大类:调用型节点、状态写入节点、控制节点。
用于调用外部能力并产出结果(可流式输出)。
Service_*:调用项目内服务(需在 registry.services 注册并声明签名)API_*:调用第三方外部 HTTP API(需在 registry.apis 注册端点与认证信息)Component_*:调用系统内置组件能力(需在 registry.components 注册)LLM_*:调用大模型推理能力(可流式输出;不需要在 registry 中注册,模型通过节点 model 字段选择)Download_*:从外部来源下载单文件并落盘到 artifacts(流式,不经过 $vars 承载正文)Unzip_*:读取 zip 并逐 entry 解压到 artifacts(流式分发,支持后缀路由)用于把数据写入 workflow 内部状态空间(变量 / 临时文件),作为步骤间数据传递与过程记录手段。
Set_*:写入/更新全局变量($vars)Write_*:写入 workflow 空间内的临时文件(artifacts)用于表达流程结构与执行策略。
Branch_*:条件分支(选择性执行某一条分支)Loop_*:循环节点(对集合执行重复步骤或按条件循环;支持 source 与 while 两种互斥模式)Stop_*:终止节点(明确结束 workflow)Pause_*:暂停节点(挂起 workflow 等待外部 resume,可恢复)| 属性 | 一句话简介 | 适用节点类型 | Service_* | API_* | Component_* | LLM_* | Download_* | Unzip_* | Set_* | Write_* | Branch_* | Loop_* | Stop_* | Pause_* |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| id | 节点唯一标识(同时编码类型,如 Service_xxx) | 全部 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 |
| if | 条件表达式;为 false 时跳过该节点及其 children 子树 | 除Stop_* | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 不适用 | 选填 |
| in | 调用入参对象(由服务/组件/LLM/API 定义) | 调用型节点 | 必填 | 必填 | 必填 | 必填 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
| out | 将本节点输出_result 映射写入 \$vars 或文件 | 调用型节点 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
| model | LLM 选择字段(支持 <provider>/<modelId> / <provider> / 省略) |
LLM_* | 不适用 | 不适用 | 不适用 | 选填 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
| source | 下载来源 / zip 来源 / loop 数据源(与 while 互斥) |
Download/Unzip/Loop | 不适用 | 不适用 | 不适用 | 不适用 | 必填 | 必填 | 不适用 | 不适用 | 不适用 | 条件必填 | 不适用 | 不适用 |
| while | 条件循环表达式(仅 Loop_*,与 source 互斥) |
Loop_* | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 条件必填 | 不适用 | 不适用 |
| maxIterations | 迭代上限(while 时必填,source 时选填) | Loop_* | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 选填 | 不适用 | 不适用 |
| routeByExt | 按后缀路由目录映射 | Download/Unzip | 不适用 | 不适用 | 不适用 | 不适用 | 选填 | 必填 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
| defaultDir | 路由未命中时的兜底目录 | Download/Unzip | 不适用 | 不适用 | 不适用 | 不适用 | 选填 | 选填 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
| overwrite | 解压覆盖策略(默认 true) | Unzip_* | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 选填 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
| target | 状态写入目标(变量路径或文件路径) | 状态写入节点 | 不适用 | 不适用 | 不适用 | 不适用 | 选填 | 不适用 | 必填 | 必填 | 不适用 | 不适用 | 不适用 | 不适用 |
| value | 写入内容/值(表达式) | 状态写入节点 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 必填 | 必填 | 不适用 | 不适用 | 不适用 | 不适用 |
| children | 并行子分支入口列表(join 后再走 next) | 除Stop_* | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 必填 | 不适用 | 不适用 |
| next | 串行后继节点, 除Stop_*外必填 | 除Stop_* | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 必填 | 不适用 | 必填 |
| cases | 分支规则列表 | Branch_* | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 必填 | 不适用 | 不适用 | 不适用 |
| mode | 执行/写入模式 | Loop / Write | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 选填 | 不适用 | 必填 | 不适用 | 不适用 |
| meta | 元信息(展示/追踪,不参与执行语义) | 全部 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 |
| 节点结束后输出自定义消息(流式事件) | 除Stop_* | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 选填 | 不适用 | 不适用 | |
| reason | 暂停等待原因文案(前端/通知/日志展示) | Pause_* | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 选填 |
| resumeResultTarget | resume payload 写入的 \$vars 路径 | Pause_* | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 必填 |
| timeout | 超时配置(sec: 秒数, on: 超时后继节点) | Pause_* | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 | 选填 |
* in 对 Service_* / API_* / Component_* / LLM_*:语法上可选,但调用型节点通常应填写(建议实现侧强制)。
Stop_*:不需要next/children,到达即终止;若出现也应视为非法或忽略(建议非法)。
本节逐个定义step 的属性语义、约束与推荐写法。
idstringnext / children 等引用;同时用前缀表达节点类型,因此 不再需要 kind前缀约束(强制):
id 前缀是引擎识别节点类型的 唯一依据。不符合以下已定义前缀的 id 视为非法,引擎应报错:
Service_xxxAPI_xxxComponent_xxxLLM_xxxDownload_xxxUnzip_xxxSet_xxxWrite_xxxBranch_xxxLoop_xxxStop_xxxPause_xxx示例:
{ "id": "Service_Approval", "next": "Branch_CheckAmount" }
ifstring(表达式)规则:
if 不写 ⇒ 默认执行if 求值为 true ⇒ 执行该节点本体,并按正常规则执行 children,最后进入 nextif 求值为 false ⇒ 跳过该节点本体 + 跳过该节点的所有 children 子树,然后直接进入next示例:
{
"id": "Service_SendNotify",
"if": "=$needNotify == true",
"in": { "msg": "=$text" },
"next": "Stop_End"
}
inobjectLoop_* 为必填;对 Service_* / API_* / Component_* / LLM_* 通常必填in 结构不同。in 是一个键值对象:{ key: value }value 可以是:
$xxxSYSVAR.xxx_item / _index / _result / _meta / _errorif=false 跳过,则该节点的 in 不会被执行/求值Service_* 的 inin 的字段必须匹配 registry.services 中该服务的入参签名示例:
{
"id": "Service_PlannerService",
"in": { "prd": "=$prdText", "rulesFile": "Process/Rules/rules.txt" },
"out": "$plan",
"next": "LLM_Generate"
}
Component_* 的 inin 结构由系统组件定义(不在 registry 重复声明)$vars / SYSVAR / _item / _index示例:
{
"id": "Component_MCP_Search",
"in": { "query": "=$keyword", "topK": 5 },
"out": "$hits"
}
LLM_* 的 model(选填,3.16 更新)LLM_* 节点通过 model 字段选择 provider/model,支持三种写法:
model:使用环境变量 LLM_MODEL(格式 <provider>/<modelId>)model: "<provider>":仅指定 provider,modelId 从环境变量 <PROVIDER>_MODEL 获取model: "<provider>/<modelId>":节点内完全指定校验规则:
model 写了但为空字符串 → 编译错误model 包含 / 但左右任一侧为空(如 /gpt-4.1、openai/)→ 编译错误<PROVIDER>_MODEL → 运行时错误LLM_MODEL → 运行时错误LLM_* 的 inin 结构由 LLM 调用协议定义(如 messages / stream / output_config)output_config):通过 output_config.format.type = "json_schema" 配合 format.schema,可强制 LLM 输出严格符合指定 JSON Schema 的内容。引擎自动将返回的 JSON parse 为对象绑定到 _resultoutput_config 为可选字段。workflow 统一写 in.output_config,引擎按 provider 做参数映射,不要求作者写各厂商原生字段in.stream: true)output_config 字段定义in.output_config 为可选字段,仅适用于 LLM_* 节点。该字段用于声明输出约束;其中 format 是规范定义字段。
跨 provider 兼容规则:
in.output_configopenai/*:引擎映射到 OpenAI 的 response_formatanthropic/*:引擎映射到 Anthropic 对应输入结构(保持 output_config.format 语义)统一字段结构:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
output_config |
object |
否 | 指定 LLM 输出格式约束。不填则为自由文本输出 |
output_config.format |
object |
是(当 output_config 存在时) |
输出格式配置对象 |
output_config.format.type |
string |
是 | 支持 "text"、"json_object"、"json_schema" 三个值。"text" 为默认行为(自由文本),无需显式声明 |
output_config.format.schema |
object |
是(当 type 为 "json_schema" 时) |
标准 JSON Schema 对象,定义输出的结构约束。必须包含 "additionalProperties": false |
output_config.format.schemaRef |
string |
是(当 type 为 "json_schema" 且未提供 schema 时) |
引用 registry.schemas 中已声明的 schemaId。执行前引擎必须展开为 format.schema;若找不到引用应报错 |
json_object 模式新增约束:
type = "json_object" 时,不得提供 schema 或 schemaReftype = "json_object" 且同时提供 schema / schemaRef,引擎应报 bad_requestjson_object 与 json_schema 语义互斥(由 type 单选表达)LLM_* 节点执行时,引擎统一提供三元组上下文:
| 字段 | 语义 | 成功时 | 失败时 |
|---|---|---|---|
_result |
业务内容本体 | 必有 | 不参与正常流程 |
_meta |
调用元信息(usage/model/latency/request_id 等) | 必有 | 可有 |
_error |
失败信息(标准化错误对象) | 无 | 必有 |
其中 _result 规则固定如下:
| 场景 | _result 类型 |
说明 |
|---|---|---|
无 output_config(或 format.type: "text") |
string |
自由文本输出,正文直接存于 _result |
output_config.format.type = "json_object" |
已解析的 JSON 对象/数组 | 引擎 parse 后直接绑定,不做 schema 校验。out 可路径访问,但不保证字段存在性/类型 |
output_config.format.type = "json_schema" |
已解析且通过校验的 JSON 对象/数组 | out 中可直接路径访问(如 =_result.score) |
引擎保证:
type = "json_object" 时,_result 一定是可解析 JSON;parse 失败时 _result = null 且 _error.type = "json_parse_error"json_object 模式不保证字段存在性;下游路径访问前应做防御性判断,或使用 onError 兜底type = "json_schema" 时,_result 一定是合法且符合 schema 的对象(或数组),无需下游再次 parse 或校验stream 的兼容性output_config 与 in.stream: true 可同时使用:
_resultout 映射始终在流式完成后执行,因此 _result/_meta/_error 在 out 求值时均为最终态引擎对 in.output_config 执行统一抽象映射:
in.output_configresponse_formatoutput_config.format 语义)示例(结构化输出 — Anthropic Claude):
{
"id": "LLM_Generate",
"in": {
"stream": true,
"messages": [
{ "role": "system", "content": "Generate a project plan based on the PRD." },
{ "role": "user", "content": "=$prdText" }
],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"title": { "type": "string" },
"phases": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"tasks": { "type": "array", "items": { "type": "string" } }
},
"required": ["name", "tasks"],
"additionalProperties": false
}
}
},
"required": ["title", "phases"],
"additionalProperties": false
}
}
}
},
"out": {
"$planTitle": "=_result.title",
"$planPhases": "=_result.phases"
},
"next": "Service_Validate"
}
示例(Loop 中使用结构化输出):
{
"id": "LLM_ExtractEntity",
"in": {
"messages": [
{ "role": "system", "content": "Extract structured entity information from the given text." },
{ "role": "user", "content": "=_item.rawText" }
],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": { "type": "string" },
"type": { "type": "string", "enum": ["person", "org", "location", "other"] },
"confidence": { "type": "number" }
},
"required": ["name", "type", "confidence"],
"additionalProperties": false
}
}
}
},
"out": {
"$entities[_index]": "=_result"
},
"next": "Write_SaveEntity"
}
示例(自由文本输出,与 3.6 行为一致):
{
"id": "LLM_FreeChat",
"in": {
"stream": true,
"messages": [
{ "role": "user", "content": "=$userQuestion" }
]
},
"out": "$answer",
"next": "Stop_End"
}
示例(json_object:要求合法 JSON,不约束结构):
{
"id": "LLM_Analyze",
"in": {
"messages": [
{ "role": "system", "content": "Analyze and return JSON with your own structure." },
{ "role": "user", "content": "=$inputText" }
],
"output_config": {
"format": { "type": "json_object" }
}
},
"out": { "$analysis": "=_result" },
"next": "Stop_End"
}
不写 output_config 即表示自由文本输出,_result 为文本正文字符串。
三种模式对比:
// 自由文本(默认,不写 output_config)
{ "id": "LLM_Chat", "in": { "messages": [...] } }
// JSON Object 模式(有结构但不约束 schema)
{ "id": "LLM_Analyze", "in": { "messages": [...], "output_config": { "format": { "type": "json_object" } } } }
// JSON Schema 模式(强约束结构)
{ "id": "LLM_Extract", "in": { "messages": [...], "output_config": { "format": { "type": "json_schema", "schema": { ... } } } } }
选型指南:
| 场景 | 推荐模式 |
|---|---|
| 对话、摘要、生成类任务,下游直接展示 | text(默认) |
| 需要 JSON 输出,但结构灵活或由 prompt 约定 | json_object |
需要 JSON 输出,且下游强依赖特定字段(如 =_result.score) |
json_schema |
| 下游需要类型安全的路径访问,且字段稳定 | json_schema |
示例(onError + _meta + _error 联合使用):
{
"id": "LLM_GenSpec",
"in": {
"messages": [{ "role": "user", "content": "..." }],
"output_config": { "format": { "type": "json_schema", "schemaRef": "SpecSchema" } }
},
"out": {
"$spec": "=_result",
"$title": "=_result.title",
"$tokens": "=_meta.usage.total_tokens"
},
"onError": "Set_LogError",
"next": "Stop_End"
}
{ "id": "Set_LogError", "target": "$runState.lastError", "value": "=_error", "next": "Stop_End" }
说明:
onError指向的节点在当前节点失败时执行,此时_error可在该节点的value表达式中读取。成功路径走next,失败路径走onError,两者互斥。
API_* 的 inAPI_* 节点的 in 用于描述 HTTP 请求的参数。引擎根据 registry.apis 中声明的端点信息与节点 in 的内容,构造并发起 HTTP 请求。
in 支持以下字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| body | object / string | 请求体(POST/PUT/PATCH 时使用) |
| query | object | URL 查询参数(键值对) |
| pathParams | object | 路径参数(替换 URL 中的 {xxx}占位符) |
| headers | object | 本次请求额外的请求头(与 registry 中声明的 headers 合并,节点级优先) |
| timeout | number | 本次请求超时时间(毫秒),可选 |
示例:
{
"id": "API_StripeCreateCharge",
"in": {
"body": {
"amount": "=$orderAmount",
"currency": "usd",
"source": "=$paymentToken"
}
},
"out": { "$chargeId": "=_result.id", "$chargeStatus": "=_result.status" },
"next": "Branch_CheckPayment"
}
{
"id": "API_WeatherQuery",
"in": {
"query": { "q": "=$cityName", "key": "=SYSVAR.weatherKey" }
},
"out": "$weather",
"next": "LLM_AnalyzeWeather"
}
{
"id": "API_SlackPostMessage",
"in": {
"body": {
"channel": "=$slackChannel",
"text": "=$notifyMessage"
}
},
"out": "$slackResult",
"next": "Stop_End"
}
执行语义:
registry.apis 中对应 id 的 method / url / auth / headers 构造请求in.pathParams 用于替换 URL 中的路径占位符(如 https://api.example.com/orders/{orderId} → https://api.example.com/orders/12345)in.query 拼接为 URL 查询字符串in.body 作为请求体发送(JSON 序列化或按 Content-Type 处理)in.headers 与 registry 声明的固定 headers 合并(节点级优先)auth 凭据由引擎自动注入到请求头(如 Authorization: Bearer xxx),节点无需手动处理_result 供 out 映射使用failed 状态(引擎可按实现提供更细粒度的错误码判断)outout 用于定义节点执行完成后的输出映射。
out 支持两种写法:
A) 简写形式(string)
"out": "$plan" — 等价于 { "$plan": "=_result" },即将完整 _result 写入指定变量。
适用于节点只需要把完整输出存入一个变量的简单场景。
B) 完整形式(object)
"out": { "$plan.status": "=_result.ok", "/path/file": "=_result" } — 逐字段映射。
每一项都是一个映射规则:key 表示写入目标,value 表示取值来源(通常来自 _result、_item 等运行时上下文)。
$ 开头)当 out 的 key 以 $ 开头时,表示将值写入变量空间。
$draft.content)$generated[_index].name)/ 开头)当 out 的 key 以 / 开头时,表示将值写入文件空间中的某个文件路径。
_item、_index 等)registry.files.artifacts 范围内重要:文件写入由引擎层执行。节点只通过
out声明"写入结果",不直接对文件系统进行写操作。
out 文件写入与 Write_* 节点的使用边界两者功能有重叠,但适用场景不同:
out 文件写入:适用于调用型节点直接将 _result 中的某部分落盘,路径与内容在同一步完成Write_*:适用于需要独立控制写入时机、写入策略(append / failIfExists)或写入内容来自非 _result 的场景(如从 $vars 中取值写入文件)out 的 value 采用路径引用方式,从运行时上下文中取值,常见来源包括:
_result.*:当前节点的执行结果_item.*:loop 场景下当前迭代项_index:loop 场景下当前迭代下标out 只描述"输出映射规则",不描述执行顺序{
"id": "LLM_GenFilesInLoop",
"in": {
"messages": [
{ "role": "system", "content": "Generate file content for the given spec." },
{ "role": "user", "content": "=_item.specText" }
]
},
"out": {
// ===== 变量写入,以$开始 =====
"$generated[_index].name": "=_item.name",
"$generated[_index].tokens": "=_meta.usage.total_tokens",
// ===== 文件写入,以/开始(key 用文件路径,支持 _index / _item 变量插值)=====
// 表示:把本次 _item 对应的文件内容写入到 /vsc 目录下
"/vsc/{_item.path}": "=_result"
// 例:若 _item.path = "files/fileA.ts",
// 则引擎会把 _result 写入 /vsc/files/fileA.ts
},
"next": "API_CustomLogic"
}
Loop_* 的 source 与 whileLoop_* 支持两种互斥模式:
source 模式:数组遍历while 模式:条件循环互斥与约束:
source 与 while 不得同时出现(编译错误)while 模式下 maxIterations 必填且 mode 必须为 "serial"source 模式下 maxIterations 选填(若填写,迭代上限 min(len(source), maxIterations))source 模式source:循环数据源表达式,求值必须是数组{
"id": "Loop_ForItems",
"mode": "parallel",
"source": "=$items",
"children": ["LLM_GenOne", "Write_One"],
"next": "Stop_End"
}
while 模式(3.16 新增)while:每轮 children 执行前求值;true 继续,false 退出maxIterations:循环上限(integer,>=1)_index、_iterDir 可用;_item 不可用if 先于 while 求值:if=false 时整个 Loop 跳过,不执行 while 判定{
"id": "Loop_review",
"while": "=$review.approved != true",
"maxIterations": 5,
"mode": "serial",
"children": ["LLM_Generate"],
"next": "Write_final"
}
Download_*/Unzip_* 路径与来源约束Download_*:
source 必填,可为 URL 字符串或对象target 与 routeByExt 二选一routeByExt 未命中时可用 defaultDir 兜底source 为对象时建议包含:url(必填)/headers/auth/timeout/checksumUnzip_*:
source 必填,必须是 zip 文件路径(表达式字符串)routeByExt 必填,按后缀分流到目标目录defaultDir 选填,overwrite 选填(默认 true)通用约束:
../、绝对路径、盘符跳转)$vars,变量仅保存路径或摘要registry.files.artifacts 范围与 Write_* 的边界与协作:
Write_* 仅负责将 value 写入 target,不负责外部下载与解压Download_* 可直接按后缀分流落盘;如需补丁写入,再接 Write_*Download_* 到 artifacts(建议 .tmp/...),再 Unzip_* 分流;若需二次加工,再接 Write_*target & valuetarget 与 value 是**状态写入节点(State Nodes)**的核心属性,适用于:
Set_*(写全局变量)Write_*(写临时文件)targetstringSet_* / Write_* 必填)Set_*target 必须是全局变量路径:
$var$var.field$arr[_index]$arr[_index].field示例:
{ "id": "Set_Status", "target": "$plan.status", "value": "\"draft\"" }
Write_*target 必须是临时文件路径(字符串或表达式字符串)registry.files.artifacts 范围内示例:
{
"id": "Write_ComponentFile",
"target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
"value": "=$componentCode"
}
valuestring(表达式)Set_*value 求值后写入 target 指向的变量路径示例:
{
"id": "Set_Count",
"target": "$count",
"value": "=$items.length"
}
Write_*value 求值后作为文件内容写入 target 指向的临时文件示例:
{
"id": "Write_PlanJson",
"target": "\"Process/Artifacts/plan.json\"",
"value": "=$plan"
}
说明:若写入 JSON 文件,推荐
value为对象/数组(由引擎序列化),或由调用方显式toJson(...)后写入文本(取决于表达式系统是否内置该函数)。
childrenstring[](step id 列表)Loop_* 为必填)children 的节点时,引擎会并行启动 children 中列出的所有入口节点。children 仅表达控制流并行,不隐含数据传递语义。RETURNchildren 分支从其入口节点开始,沿自身的 next / children / Branch_* / Loop_* 执行完整控制流链。next: "RETURN" 时:
children joinchildren 分支均完成(RETURN) 后,才进入父节点自身的 next。children 的完成顺序不做保证。在任一 children 分支中发生以下情况时,行为如下:
Stop_*
→ 整个 workflow 立即终止(stopped)failedPause_* 节点
→ 整个 workflow 进入 paused,等待 resumeif 的关系if=false:
children 子树均被跳过nextchildren 内部节点的 if:
{
"id": "Service_GenerateParts",
"children": ["LLM_GenA", "LLM_GenB"],
"next": "Service_Merge"
}
{
"id": "LLM_GenA",
"next": "RETURN"
}
{
"id": "LLM_GenB",
"next": "RETURN"
}
nextstringStop_* 节点外)next 只能有一个,表示唯一的后继控制流。Branch_* 或 children。next 的取值必须显式表达控制流语义,合法取值包括:
"<stepId>":线性推进到指定后继节点。"RETURN":结束当前执行分支,返回最近一层父执行上下文(join)。"BREAK":结束当前迭代并退出整个 Loop_*,跳转到 Loop_*.next(仅在 Loop_* children 子树内有效)。"RETURN" 与 "BREAK" 为保留关键字,不得作为 stepId 使用。
BREAK 规则:
Loop_* 的 children 子树;在 Loop 外使用为编译错误source 与 while 两种模式mode:"parallel" 下,触发 BREAK 后未启动迭代不再启动,已启动迭代继续到自然结束,然后进入 Loop_*.next可恢复的暂停不通过
next的魔法值表达,而是通过节点类型Pause_*显式声明(见第十一章)。
Stop_* 节点外,所有节点必须显式声明 next。next 推断 pause、return 或 stop 语义。Stop_* 节点缺失 next,属于 Spec 校验错误。{
"id": "Service_PlannerService",
"next": "LLM_Generate"
}
cases(仅 Branch_*)arrayBranch_*)cases 是一个二维数组,每一项是:
["<whenExpr>", "<stepId>"]
<whenExpr>:条件表达式字符串,或固定字符串 "ELSE"<stepId>:该分支的入口节点 id示例:
"cases": [
["=$amount < 500", "Service_ManagerApprove"],
["ELSE", "Service_ManagerAndDirectorApprove"]
]
<whenExpr> 求值为 true 的分支被选中"ELSE" 必须放最后,作为兜底分支next/children/branch/loop 继续执行)Stop_* 或遇到 next: "RETURN")才算该 case 完成Branch_* 节点自身的 nextmodestringLoop_* 必填;对 Write_* 选填;其他节点不适用Loop_* 的 mode(必填)用于指定循环的执行方式:
"parallel":各次迭代可并行执行"serial":按迭代顺序串行执行while 模式,则 mode 必须为 "serial"示例:
{
"id": "Loop_ForItems",
"mode": "parallel",
"source": "=$items",
"children": ["Service_HandleOne"],
"next": "Stop_End"
}
Write_* 的 mode(选填)用于指定写文件时的写入策略:
"overwrite":覆盖写(默认)"failIfExists":若已存在则报错"append":追加写(新内容拼接到末尾)"prepend":前置写(新内容拼接到开头)约束:
append/prepend 属于读改写(read-modify-write)语义,适用于文本内容(或实现明确支持的字节拼接)append/prepend 均应按新建文件处理append/prepend 不保证顺序;需由上层流程避免并发冲突示例:
{
"id": "Write_Log",
"mode": "append",
"target": "\"Process/Artifacts/log.txt\"",
"value": "=$line"
}
metaobject示例:
{
"id": "LLM_GenerateUI",
"meta": { "title": "Generate UI Map", "tags": ["gen", "ui"], "owner": "agent-ui" },
"in": { "messages": [ { "role": "user", "content": "=$prdText" } ] },
"out": { "$uiMap": "=_result" }
}
printstring(表达式或字面字符串)run_events.type = "step_print" 事件。规则:
print 仅在节点实际执行且成功完成时触发if=false 被跳过,不触发 printprint(仅发 step_error)Stop_* 不允许 printprint 的求值结果应为字符串;若结果非字符串,引擎应按 JSON 序列化为字符串后输出建议:
out / workflow 输出返回)示例:
{
"id": "LLM_CheckVersion",
"in": {
"messages": [
{ "role": "user", "content": "请返回文档版本号" }
]
},
"out": { "$version": "=_result" },
"print": "=\"版本检查完成: \" + $version",
"next": "Stop_End"
}
workflow 中可使用的"数据引用"分为 7 类:
| 写法 | 类型 | 可写 | 声明位置 |
|---|---|---|---|
paramName |
入参(Params) | 否 | registry.params |
$xxx |
全局变量(Global Vars) | 是 | registry.vars |
SYSVAR.xxx |
系统变量(System Vars) | 否 | 系统预配置 |
_item / _index |
循环局部变量(Loop Locals) | 否 | 自动注入 |
_result |
节点输出变量(Step Local Result) | 否 | 自动注入 |
_meta |
节点元信息变量(Step Local Meta) | 否 | 自动注入(LLM 节点) |
_error |
节点错误变量(Step Local Error) | 否 | 自动注入(节点失败时) |
params入参在 registry.params 中声明,不带 $ 前缀:
"params": ["userRequest(STRING)", "targetLang(STRING)", "maxRetries(INT) = 3"]
=userRequest)out 左侧或 Set_* 的 target 中$,与 VL 语法中服务/方法入参的风格对齐$vars全局变量必须在 registry.vars 中声明(VL 风格):
"vars": ["$plans([OBJECT])", "$generated([OBJECT])", "$summary(STRING)"]
$xxxout 写入(字段映射)Set_* 写入(target/value)$vars 是 workflow 的主要数据传递方式(节点连线只表示控制流)当 Loop_* 使用 mode:"parallel" 时:
$vars,但应避免多个迭代写入同一位置导致竞态_index 分槽写入,例如:
$generated[_index] = ...$files[_index].path = ...SYSVAR.xxxSYSVAR.xxx 表示用户在系统空间预先配置的通用变量。
registry.vars 声明_item / _index / _iterDir当进入 Loop_* 的循环体(其 children 子树)时,引擎会为每次迭代提供:
_item:当前迭代元素_index:当前迭代索引(从 0 开始)_iterDir:当前迭代临时目录名,格式 {loopNodeId}_{_index}规则:
_iterDir 建议用于并行迭代下的临时路径隔离(例如 .tmp/{_iterDir}/bundle.zip)mode:"serial" 与 mode:"parallel" 下语义一致_result(调用型节点输出)调用型节点(Service_* / API_* / Component_* / LLM_*)执行完成后,引擎将内容输出绑定为:
_result_result 只在该节点的 out 映射求值阶段可用LLM_* 结构化输出时的 _resultLLM_* 节点中,_result 永远表示业务内容本体:
_result 为字符串正文json_schema 输出:_result 为引擎自动解析并校验后的 JSON 对象/数组即使是文本场景,也不再要求通过 =_result.content 取正文。
示例:
{
"id": "Service_PlannerService",
"in": { "prd": "=$prdText" },
"out": { "$plan": "=_result", "$planId": "=_result.id" }
}
_meta(LLM 节点元信息)_meta 为 LLM_* 节点的元信息对象,承载调用层信息,不承载业务正文。
_meta 只在当前节点 out 求值阶段可用建议至少包含:
provider、model、model_resolvedrequest_id、response_idlatency_ms、finish_reasonusage.input_tokens、usage.output_tokens、usage.total_tokenscost(若启用成本计算)约束:
input_tokens / output_tokens / total_tokensusage.raw取值示例:
=_meta.usage.total_tokens=_meta.model_resolved_error(节点失败信息)节点失败时,引擎将标准错误对象绑定到 _error。
_error 在失败路径中可读(如 onError 分支或失败处理节点)_error 不存在建议至少包含:
type(标准分类)code、message、retryable、status_codeprovider、model、request_idlitellm_exceptiondetails、rawtype 建议使用以下标准枚举:
auth_errorbad_requestrate_limittimeoutcontext_length_exceededcontent_policy_violationservice_unavailableconnection_errorjson_parse_errorschema_validation_errorinternal_errorunknown_error= 前缀)Workflow JSON 中的所有字符串值遵循统一规则:
= 开头:表达式,引擎对 = 后的内容进行求值= 开头:字面字符串,原样使用== 开头:字面字符串,内容为 = 后的部分(用于极少数需要以 = 开头的字面字符串)示例:
"msg": "hello" → 字面字符串 "hello"
"msg": "=$userRequest" → 表达式,求值 userRequest 入参
"msg": "=$text + ' world'" → 表达式,字符串拼接
"msg": "==formula" → 字面字符串 "=formula"
该规则适用于所有 JSON 字符串值位置。引擎不再需要区分"哪些字段是表达式位置"——统一由 = 前缀决定。
以下属性中的字符串值通常需要使用 = 前缀:
if:条件表达式(如 "=$count > 0")in 的字段值(引用变量时需 =,字面值不需要)out 的右侧取值表达式(如 "=_result.id")Set_* 的 value(如 "=$plan.status")Write_* 的 target(当为动态路径时,如 "=\"Process/\" + _item.name")Write_* 的 value(如 "=$componentCode")Loop_* 的 source(如 "=$items")Branch_* 的 cases[*][0](when 表达式,如 "=$amount < 500")print(如 "=\"当前阶段: \" + $phase")以下位置不使用 = 前缀(它们是目标路径或标识符,非求值表达式):
out 的左侧(写入目标路径,如 "$plan"、"$generated[_index].name")out 简写形式(如 "out": "$plan",表示目标变量)Set_* 的 target(写入目标路径,如 "$plan.status")next / children / cases[*][1](步骤 ID)mode、model 等配置值表达式建议至少支持:
userRequest、targetLang(params,无 $ 前缀,只读)$xxx(vars,可读写)SYSVAR.xxx(只读)_item、_index、_result、_meta、_error&&、||、!==、!=、>、>=、<、<=++ - * /(...).field[expr]说明:表达式语言不需要复杂到脚本语言,保持可静态分析与可预测即可。
里"路径"既用于读(表达式),也用于写(out 左侧、Set.target)。
.:永远是字面字段名例如:
$plan.status 表示字段名就是 "status"_meta.usage.total_tokens 表示逐级字段访问点号后面的内容不会被当作变量解析。
[...]:永远是表达式索引方括号内部必须按表达式解析。
$generated[_index]:_index 是变量(表达式求值)$arr[0]:数字常量索引$obj[_item.name]:表达式求值为 key["literal"]如果确实要访问 key 名为 "_index" 的字段(而不是变量),必须写成:
$obj["_index"]这样解析器完全不需要猜测。
out 的路径写入语义(deep-set)当 out 左侧是路径(如 $plan.status)时:
示例:
"out": { "$plan.status": "=_result.success" }
Loop 并行场景推荐按 _index 分槽写入,避免覆盖:
"out": { "$generated[_index]": "=_result" }
或:
"out": { "$generated[_index].name": "=_item.name" }
本章定义workflow 空间内的**临时文件(artifacts)**的用途、路径约束、读写方式与生命周期规则。
Artifacts 仅用于 workflow 内部的数据临时存储与步骤间传递,不等同于业务最终产出文件。
components/ 目录下 N 个文件)registry.files.artifactsworkflow 允许写入的临时文件路径范围必须在 registry.files.artifacts 中声明:
"files": {
"inputs": ["Process/Rules/*", "Process/PRD.json"],
"artifacts": ["Process/Artifacts/*"]
}
写入 artifacts 必须使用状态写入节点:
Write_*Write_* 的 target 表示写入路径(可为表达式字符串):
{
"id": "Write_ComponentFile",
"target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
"value": "=$componentCode"
}
value 表示文件内容(表达式求值结果):
Write_* 支持 mode(可选):
"overwrite":覆盖写(默认)"failIfExists":若文件已存在则报错"append":追加写(适用于日志类文件)"prepend":前置写(将新内容写在文件开头)补充约束:
append/prepend 属于读改写语义(read-modify-write)示例:
{
"id": "Write_Log",
"mode": "append",
"target": "\"Process/Artifacts/run.log\"",
"value": "=$line + \"\\n\""
}
当前版本不强制提供"专门的读文件节点"。Artifacts 的读取建议通过以下方式完成:
Artifacts 文件路径可以作为普通字符串存入 $vars,并作为调用型节点 in 的参数传递:
{ "id": "Service_UseArtifact", "in": { "filePath": "=$artifactPath" } }
Artifacts 的实际读取由以下之一承担:
Component_*(系统内置能力,例如 FileOps.Read)Service_*(自定义的读取服务)当前版本的核心原则是:workflow 只定义编排,不绑定具体 IO 实现细节;因此"读文件"属于能力节点的职责。
$vars$vars 可存"文件引用",而 artifacts 存"文件内容".tmp/ 语义.tmp/ 开头时,视为临时文件路径(例如 .tmp/bundle.zip)runId,只写相对路径.tmp/xxx 解析为 <artifactsRoot>/.tmp/{runId}/xxxworkspaceId + runId.tmp/ 路径时创建 .tmp/{runId}/.tmp/,则不创建 .tmp/{runId}/ 目录_iterDir)Loop_* 并行场景下,建议使用 .tmp/{_iterDir}/... 防止迭代间冲突<artifactsRoot>/.tmp/{runId}/{_iterDir}/...stopped / failed / cancelled)后,引擎应清理 .tmp/{runId}/.tmp/ 仅作为中间态目录,不作为业务可读目录Download_* 必须流式下载,避免将完整正文加载到内存Unzip_* 必须逐 entry 解压,并做 zip-slip 防护routeByExt 路由目录建议与组件工厂目录约定一致(如 ExtComponents/Sections/Services)$vars,仅将路径、摘要、统计信息写入变量当 workflow 存在并行执行(children 并行、Loop_* mode:"parallel")时:
_index 或 _item.name 分片,保证每轮写不同文件示例:
{
"id": "Write_ComponentFile",
"target": "=\"Process/Artifacts/components/\" + _item.name + \".tsx\"",
"value": "=$code"
}
Loop_*)本章定义 Loop_* 节点的完整执行规则,包括数据源求值、并行/串行模式、循环体执行、join 汇合、局部变量作用域,以及与 if/children/next 的交互。
Loop_* 是控制节点,支持 source 与 while 两种互斥模式。
source 模式:
{
"id": "Loop_xxx",
"mode": "parallel | serial",
"source": "<expr>",
"children": ["<stepId>"],
"next": "<stepId>"
}
while 模式:
{
"id": "Loop_xxx",
"mode": "serial",
"while": "<expr>",
"maxIterations": 5,
"children": ["<stepId>"],
"next": "<stepId>"
}
约束:
mode 必填;while 模式必须为 "serial"source 与 while 二选一source 模式:source 必填且求值为数组,maxIterations 选填while 模式:while 与 maxIterations 必填children 必填:循环体入口节点列表(推荐仅 1 个入口;若允许多个则表示并行体)source)执行到 Loop_* 时,引擎首先求值:
items = eval(source)规则:
items 必须是数组null/undefined:
[](空循环)failed 状态当 Loop_* 使用 while 模式时:
whilenext_index++,达到 maxIterations 后强制退出_item 不存在(while 无数据源),_index 与 _iterDir 可用若 items.length == 0:
Loop_* 的 nextLoop_* 必须声明 next;空循环时直接进入该 nextchildren 定义循环体入口节点(stepId 列表)。
推荐约束(更清晰):
Loop_* 的 children 建议限制为 1 个入口节点children(如果允许 Loop_* children 多入口,则它们在每次迭代中并行启动)
每次迭代 i(从 0 开始)都会在循环体子树范围内注入:
_item = items[i]_index = i作用域规则:
_item/_index 仅在该次迭代的循环体子树内可用_item/_index 相互隔离mode)mode: "serial"语义:按顺序逐个迭代执行。
执行流程:
children 入口开始跑完整链)Loop_* next特点:
mode: "parallel"语义:所有迭代可并行执行。
执行流程:
children 入口开始执行完整链Loop_* next特点:
$vars 与 artifacts 同路径写入)while 模式下 mode 必须为 "serial"while + mode:"parallel" 属于编译错误一次迭代从其入口节点开始执行,沿自身的 next / children / Branch_* / Loop_* 推进,并在以下任一情况发生时结束:
next: "RETURN",表示该迭代正常完成,控制流返回到 Loop_* 的 join。next: "BREAK",表示该迭代完成并退出整个 Loop,跳转到 Loop_*.next。Pause_*,workflow 进入 paused 状态,等待 resume;loop 未完成。Stop_*,workflow 立即终止(stopped)。failed 状态。无论 mode 为 serial 还是 parallel,Loop_* 的汇合规则一致:
Loop_* 仅在 所有迭代均正常完成(RETURN) 后,才会进入自身的 next。BREAK:退出循环并进入 Loop_*.next;在 parallel 模式下已启动迭代继续执行至自然结束。Pause_* 节点进入 paused 状态,整个 workflow 进入 paused,loop 不再继续。Stop_*,整个 workflow 立即终止(stopped)。failed 状态。if 的交互ifLoop_* if=false:
Loop_* nextifif 独立求值if=false 会跳过该节点及其子树,但不影响其他迭代$vars / artifacts 的交互建议$vars 写入建议_index 分槽写入,例如:
$generated[_index] = ...Process/Artifacts/components/<n>.tsxLoop 本身不直接产出 _result。循环结果通常通过:
out 写入 $vars[_index]Write_* 写入 artifacts,并把路径写入 $vars[_index]示例(概念):
$files[_index].path = "Process/Artifacts/components/xxx.tsx"Branch_*)本章定义 Branch_* 节点的完整执行规则,包括 case 选择、分支链执行、汇合(join)与 next 的关系,以及与 if/children 的交互。
Branch_* 是控制节点,基本结构如下:
{
"id": "Branch_xxx",
"cases": [
["<whenExpr1>", "<stepId1>"],
["<whenExpr2>", "<stepId2>"],
["ELSE", "<stepIdElse>"]
],
"next": "<stepId>"
}
约束:
cases 必填[whenExpr, stepId]whenExpr 为表达式字符串,或固定字符串 "ELSE""ELSE" 必须存在且必须放最后(推荐强约束)执行到 Branch_* 时,引擎按顺序对 cases 进行判断:
["<whenExpr>", "<stepId>"]:
<whenExpr> 不是 "ELSE":
eval(<whenExpr>)true,该 case 命中,停止继续判断后续 case"ELSE" case每个 case 只能指定 一个入口节点(一个 stepId)。该入口节点代表"该分支路径的起点"。
命中某个 case 后,引擎将从该 case 的入口节点开始执行:
next / children / Branch_* / Loop_* 继续推进重要:Branch 并不是只执行入口节点一次就返回,而是执行"从入口节点开始的一整条分支链"。
分支链从被选中的 case 入口节点开始执行,沿其自身的控制流推进,并在以下任一情况发生时结束:
next: "RETURN",表示该分支正常完成,控制流返回到 Branch_* 的 join。Pause_*,workflow 进入 paused 状态,等待 resume。Stop_*,workflow 立即终止(stopped)。failed 状态。next当且仅当:
next: "RETURN")Branch_* 节点才会进入其自身的 next。
执行规则如下:
Branch_* 的 next必须显式声明。Branch_* next 指向的节点继续执行。异常情况:
Pause_* 节点,workflow 进入 paused 状态,Branch_* 不再继续推进。Stop_*,workflow 立即终止(stopped)。failed 状态。所有未命中的 case 对应的入口节点及其子树完全不执行。
if 的交互if若 Branch_* if=false:
Branch_* nextifBranch 本身是"互斥选择",一次只会选择一个 case。如果某个 case 的入口节点内部包含 children,则该分支链内部仍可产生并行执行,并遵循 children 的 join 规则。
如果实现允许 "ELSE" 缺失(不推荐):
Branch_* next推荐强制要求 ELSE 存在,以保证分支行为可预测。
本章定义 Pause_* 节点的结构、执行语义与配套的 resume 恢复协议,用于在 workflow 中显式表达"等待外部结果再继续"的语义,适配用户输入与审批流等场景。引擎保持通用,不将审批领域规则内置到引擎。
Pause_* 用于在流程中间等待外部系统输入结果。
引擎执行到该节点时进入 paused 状态,仅在收到合法 resume 请求后继续执行 next。
Pause_*≠Stop_*:Stop_*是终止(不可恢复),Pause_*是可恢复的挂起。
Pause_*≠failed:failed是异常中断,Pause_*是设计意图内的等待。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id |
string | 是 | 节点标识,须以 Pause_ 开头 |
reason |
string | 否 | 给前端/通知/日志展示的等待原因文案 |
resumeResultTarget |
string | 是 | resume(payload) 写入的 $vars 目标路径 |
timeout |
object | 否 | 超时配置(见下) |
next |
string | 是 | 恢复成功后的后继节点 |
timeout 结构| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
sec |
number | 是 | 超时秒数(必须 > 0) |
on |
string | 是 | 超时后跳转的节点(通常为 Stop_* 或异常处理链路) |
{
"id": "Pause_UserInput_Address",
"reason": "请补充收货地址",
"resumeResultTarget": "$vars.userInput.address",
"timeout": {
"sec": 86400,
"on": "Stop_Timeout"
},
"next": "Service_CreateOrder"
}
执行到 Pause_* 时,引擎必须:
pausedrunId、pauseNodeId、waitToken(hash)、expireAt、resumeResultTarget)pause_start 事件收到 resume 请求后,引擎必须:
pausedwaitToken 与 pauseNodeId 对应关系payload 写入 resumeResultTargetpause_resumed 事件Pause_* 节点的 next 继续执行若到达 timeout.sec 仍未恢复:
pause_timeout 事件timeout.on 执行timeout,实现可按平台策略保持 paused 或转 failed(需实现侧明确文档化){
"runId": "run_xxx",
"token": "wait_token_xxx",
"payload": {
"approved": true,
"comment": "ok"
},
"requestId": "req_xxx"
}
resume 必须幂等(建议基于 requestId 去重)paused 时应拒绝恢复并返回当前状态nextStep,恢复路径固定为 Pause_* 的 nextpause_start触发:进入 Pause_* 时。
关键字段:runId、nodeId、reason、waitToken、expireAt、resumeResultTarget
pause_resumed触发:resume 成功且已写入 resumeResultTarget 后。
关键字段:runId、nodeId、requestId、resumedAt
pause_timeout触发:等待超时。
关键字段:runId、nodeId、expiredAt、timeoutAction
pause_rejected触发:恢复请求被拒绝(token 无效、状态错误、schema 不通过)。
关键字段:runId、nodeId、requestId、reasonCode
resume推荐结构(每层 2 节点):
Service_StartL1Approval → Pause_L1 → Branch_L1ResultService_StartL2Approval → Pause_L2 → Branch_L2ResultService_StartL3Approval → Pause_L3 → Branch_L3Result说明:
Pause_* 使用独立 resumeResultTarget(如 $vars.approval.l1、$vars.approval.l2、$vars.approval.l3)approval.create),通过 stage 参数区分next: "PAUSE" 魔法值;暂停由 Pause_* 节点显式表达说明
- Workflow 是项目的一部分,不需要用户选择,存储在项目内 /config/workflow.json 中
- 新建项目时自动生成默认 workflow
- IDE 始终运行在已知
tenantId + projectId上下文中- workflow 的执行策略由 Agent 决定,前端不提供配置面板
| 窗口区域 | 主要内容 | 用户操作 | 说明 |
|---|---|---|---|
| 对话窗口 | 用户自然语言输入 | 输入需求 | 所有 workflow 执行的唯一入口 |
| Workflow 运行视图 | 当前项目 workflow 的执行过程 | 无 | 自动展示当前 run 状态 |
| 输出控制台 | 日志 / 节点状态 / 错误 | 查看 | 由后台 run 自动推送 |
| 工作空间视图 | 当前 workspace 文件状态 | commit / rollback | 与 workflow 解耦 |
| 状态提示区 | run 状态 | 查看 | running / finished / failed |
| 接口名称 | 接口用途(使用方) | 入参 | 出参 | 重点说明 |
|---|---|---|---|---|
| LoadProjectContext | 加载项目上下文(IDE) | tenantId, projectId | 项目信息、默认 workspaceId | IDE 初始化 |
| LoadWorkspace | 加载工作空间 | tenantId, projectId, workspaceId | 文件树摘要、workspace 状态 | 判断是否初始状态 |
| AnalyzeIntent | 生成运行入参(Agent) | tenantId, projectId, workspaceId, userInput | runMaps(mode / nodes),notes(用来在会话窗口展示当前修改计划供用户查看,但不会中断流程) | 仅非初始 workspace 调用 |
| RunProjectWorkflow | 运行项目 workflow | tenantId, projectId, workspaceId, userInput, runMaps? | runId + SSE 连接 | 自动建立 SSE,返回执行状态 |
| GetRunStatus | 查询运行状态 | tenantId, projectId, runId | run 状态、错误信息 | 用于断线恢复 |
| CommitWorkspace | 提交产物 | tenantId, projectId, workspaceId | commitId | 与 workflow 无关 |
| RollbackWorkspace | 回滚产物 | tenantId, projectId, workspaceId | 回滚结果 | 与 workflow 无关 |
| 功能点 | 实现逻辑 | 设计约束 |
|---|---|---|
| 对话驱动执行 | 用户输入即触发 workflow | 无显式按钮 |
| 初始 workspace 判断 | LoadWorkspace 后判断是否初始状态 | 前端只判断,不决策 |
| 新项目执行 | 初始 workspace → 直接 run | 不生成 runMaps |
| 修改型执行 | 非初始 workspace → 先 AnalyzeIntent | 由 Agent 决定 mode / nodes |
| 运行参数生成 | AnalyzeIntent 输出 runMaps | 前端不理解语义 |
| 执行启动 | RunProjectWorkflow | 自动 SSE |
| 执行反馈 | 后台推送运行状态 | 前端被动展示 |
| 产物管理 | commit / rollback workspace | workflow 不参与 |
| 错误处理 | run 失败即终止 | 支持后续再次对话 |
| workspace 状态 | 执行流程 |
|---|---|
| 初始状态 | userInput → RunProjectWorkflow |
| 非初始状态 | userInput → AnalyzeIntent → RunProjectWorkflow |
run_events)Workflow 执行过程中,引擎向 run_events 事件流写入结构化消息,用于向调用方(前端、MQ 消费者、日志系统)传递执行进度和阶段结果。
设计原则:
run_events 只描述过程,最终结论以 workflow_runs 为准{
"run_id": "run_xxx",
"seq": 1,
"ts": "2026-02-21T10:00:00.123Z",
"type": "step_start",
"step_id": "LLM_Answer",
"payload": {}
}
| 字段 | 类型 | 说明 |
|---|---|---|
run_id |
string |
本次 workflow 执行唯一 ID |
seq |
integer |
run 内单调递增序号,从 1 开始 |
ts |
string |
ISO 8601 时间戳(毫秒精度) |
type |
string |
事件类型 |
step_id |
`string \ | null` |
payload |
object |
事件负载 |
workflow_startworkflow_doneworkflow_failedworkflow_cancelledstep_startstep_printstep_donestep_errorstep_skippedllm_token(仅当节点 in.stream: true)llm_donefile_start(步骤开始时声明将写入某路径的文件)file_done(文件内容已完整写入对应路径)pause_start(进入 Pause_* 节点,workflow 进入 paused 状态)pause_resumed(resume 成功,已写入 resumeResultTarget,即将继续执行)pause_timeout(等待超时,即将跳转到 timeout.on)pause_rejected(resume 请求被拒绝,token 无效或状态不符)顺序约束:
step_start → llm_token(xN) → llm_done → step_print(0..1) → step_donellm_done 必在所有 llm_token 之后file_start 在 step_start 之后立即批量发出,早于任何 llm_tokenfile_done 在 llm_done 之后、step_print/step_done 之前发出workflow_start{ "params": {} }
workflow_done{ "stop_id": "Stop_End", "duration_ms": 4821 }
workflow_failed{
"failed_step_id": "LLM_Analyze",
"error": { "type": "json_parse_error", "message": "Unexpected token", "retryable": true },
"duration_ms": 1203
}
workflow_cancelled{
"reason": "user_request",
"duration_ms": 800
}
step_start{ "step_type": "LLM_*" }
step_done{ "step_type": "LLM_*", "duration_ms": 2341 }
step_done 的 payload 不包含 _result。
step_print节点执行完成后发出,用于承载节点级自定义打印内容(来自该节点 print 属性)。
{ "message": "版本检查完成: 3.13" }
| 字段 | 类型 | 说明 |
|---|---|---|
message |
string |
print 求值后的输出文本 |
step_error{
"step_type": "LLM_*",
"error": { "type": "rate_limit", "message": "Rate limit exceeded", "retryable": true, "status_code": 429 },
"duration_ms": 312
}
step_skipped{ "step_type": "Set_*", "reason": "if_false" }
file_start步骤开始时批量发出,声明本步骤将写入的文件路径。此时文件内容尚未生成。
{
"path": "src/components/Header.tsx"
}
| 字段 | 类型 | 说明 |
|---|---|---|
path |
string |
文件路径(相对于 workspace 根目录) |
file_done文件内容已完整写入对应路径后发出,在 llm_done 之后、step_done 之前。
{
"path": "src/components/Header.tsx",
"size_bytes": 2048
}
| 字段 | 类型 | 说明 |
|---|---|---|
path |
string |
与对应 file_start 事件中的 path 一致 |
size_bytes |
integer |
写入后文件的字节数 |
llm_token{ "delta": "这是" }
delta 为原始文本增量,不做 JSON parse。
llm_done{
"finish_reason": "stop",
"usage": { "input_tokens": 312, "output_tokens": 428, "total_tokens": 740 },
"model": "claude-sonnet-4-5",
"latency_ms": 2180
}
pause_start{
"nodeId": "Pause_UserInput_Address",
"reason": "请补充收货地址",
"waitToken": "tok_abc123",
"expireAt": "2026-03-03T10:00:00.000Z",
"resumeResultTarget": "$vars.userInput.address"
}
pause_resumed{
"nodeId": "Pause_UserInput_Address",
"requestId": "req_xyz",
"resumedAt": "2026-03-02T11:30:00.000Z"
}
pause_timeout{
"nodeId": "Pause_UserInput_Address",
"expiredAt": "2026-03-03T10:00:00.000Z",
"timeoutAction": "Stop_Timeout"
}
pause_rejected{
"nodeId": "Pause_UserInput_Address",
"requestId": "req_bad",
"reasonCode": "INVALID_TOKEN"
}
step_id + seq 进行重组LLM_* 节点(in.stream: true,无文件写入){ "run_id": "run_abc", "seq": 1, "ts": "...", "type": "workflow_start", "step_id": null, "payload": { "params": {} } }
{ "run_id": "run_abc", "seq": 2, "ts": "...", "type": "step_start", "step_id": "LLM_Answer", "payload": { "step_type": "LLM_*" } }
{ "run_id": "run_abc", "seq": 3, "ts": "...", "type": "llm_token", "step_id": "LLM_Answer", "payload": { "delta": "这是" } }
{ "run_id": "run_abc", "seq": 4, "ts": "...", "type": "llm_token", "step_id": "LLM_Answer", "payload": { "delta": "一个" } }
{ "run_id": "run_abc", "seq": 5, "ts": "...", "type": "llm_token", "step_id": "LLM_Answer", "payload": { "delta": "回答。" } }
{ "run_id": "run_abc", "seq": 6, "ts": "...", "type": "llm_done", "step_id": "LLM_Answer", "payload": { "finish_reason": "stop", "usage": { "input_tokens": 80, "output_tokens": 12, "total_tokens": 92 }, "model": "claude-sonnet-4-5", "latency_ms": 1340 } }
{ "run_id": "run_abc", "seq": 7, "ts": "...", "type": "step_print", "step_id": "LLM_Answer", "payload": { "message": "版本检查完成: 3.13" } }
{ "run_id": "run_abc", "seq": 8, "ts": "...", "type": "step_done", "step_id": "LLM_Answer", "payload": { "step_type": "LLM_*", "duration_ms": 1355 } }
{ "run_id": "run_abc", "seq": 9, "ts": "...", "type": "workflow_done", "step_id": null, "payload": { "stop_id": "Stop_End", "duration_ms": 1400 } }
以一个代码生成 workflow 为例,LLM_GenCode 节点生成两个文件。两个 file_start 在步骤开始时批量发出,两个 file_done 在 llm_done 之后发出:
{ "run_id": "run_xyz", "seq": 1, "ts": "...", "type": "workflow_start", "step_id": null, "payload": { "params": {} } }
{ "run_id": "run_xyz", "seq": 2, "ts": "...", "type": "step_start", "step_id": "LLM_GenCode", "payload": { "step_type": "LLM_*" } }
{ "run_id": "run_xyz", "seq": 3, "ts": "...", "type": "file_start", "step_id": "LLM_GenCode", "payload": { "path": "src/components/Header.tsx" } }
{ "run_id": "run_xyz", "seq": 4, "ts": "...", "type": "file_start", "step_id": "LLM_GenCode", "payload": { "path": "src/components/Header.module.css" } }
{ "run_id": "run_xyz", "seq": 5, "ts": "...", "type": "llm_token", "step_id": "LLM_GenCode", "payload": { "delta": "import React" } }
{ "run_id": "run_xyz", "seq": 6, "ts": "...", "type": "llm_token", "step_id": "LLM_GenCode", "payload": { "delta": " from 'react'..." } }
{ "run_id": "run_xyz", "seq": 7, "ts": "...", "type": "llm_done", "step_id": "LLM_GenCode", "payload": { "finish_reason": "stop", "usage": { "input_tokens": 210, "output_tokens": 560, "total_tokens": 770 }, "model": "claude-sonnet-4-5", "latency_ms": 3100 } }
{ "run_id": "run_xyz", "seq": 8, "ts": "...", "type": "file_done", "step_id": "LLM_GenCode", "payload": { "path": "src/components/Header.tsx", "size_bytes": 1872 } }
{ "run_id": "run_xyz", "seq": 9, "ts": "...", "type": "file_done", "step_id": "LLM_GenCode", "payload": { "path": "src/components/Header.module.css", "size_bytes": 634 } }
{ "run_id": "run_xyz", "seq": 10, "ts": "...", "type": "step_print", "step_id": "LLM_GenCode", "payload": { "message": "代码文件生成完成: 2 个文件" } }
{ "run_id": "run_xyz", "seq": 11, "ts": "...", "type": "step_done", "step_id": "LLM_GenCode", "payload": { "step_type": "LLM_*", "duration_ms": 3280 } }
{ "run_id": "run_xyz", "seq": 12, "ts": "...", "type": "workflow_done", "step_id": null, "payload": { "stop_id": "Stop_End", "duration_ms": 3350 } }
3.17 的目标不是推翻 3.16,而是把过去分散在实现层里的高价值能力收敛成一套可公开引用的规范:
本章允许这些能力以“标准扩展剖面”的形式存在。实现方可以分阶段支持,但一旦声明支持 3.17 扩展剖面,就应满足本章的字段、事件与兼容性要求。
Tool_* 节点Tool_* 用于调用 workflow 宿主环境提供的工具能力,例如文件、检索、编译、浏览器控制、DocCenter、子 workflow 调度等。
Tool_* 属于 3.17 标准扩展剖面。不支持该剖面的运行时,应在加载期或执行期显式报错,而不是静默跳过。
{
"id": "Tool_ReadBlueprint",
"tool": "ReadFile",
"input": { "file_path": "docs/blueprint.md" },
"timeout": 15000,
"allowError": false,
"out": {
"$blueprint": "_result",
"$readMeta": "_toolResult"
},
"next": "LLM_AnalyzeBlueprint"
}
字段约定:
| 字段 | 类型 | 说明 |
|---|---|---|
tool / toolName |
string |
要调用的宿主工具名;两者二选一,推荐 tool |
input / in |
object |
传给工具的输入对象;两者二选一,推荐 input |
timeout |
integer |
可选,毫秒级超时 |
allowError |
boolean |
为 true 时,工具失败不会直接使 workflow failed,而是将错误信息作为结果暴露给后续步骤 |
out |
object |
输出映射,语义与其他节点保持一致 |
Tool_* 执行完成后,至少应向 out 提供以下两个临时值:
_result:工具的主结果;若工具返回 envelope 且存在 result 字段,可优先解包为主结果_toolResult:工具的完整原始返回对象,保留元数据、diff、undoId、usage 等附加字段allowError = false:工具异常应导致当前节点报错,并使 workflow 进入 failedallowError = true:运行时应发出 tool_error,同时允许后续节点读取错误 envelope 并自行决策Subflow_* 语义化别名当 workflow 需要显式表达“此节点会调度一个子 workflow”时,可以使用 Subflow_* 作为 WorkflowRun 的语义化别名。
约束:
Subflow_* 不引入新的底层执行原语WorkflowRun 能力的调用Subflow_* 与普通 Tool_* 在视觉上区分开,便于识别子流程边界workflow_path、params、mode、work_dir、emit_events3.17 在第十三章基础上新增以下事件类型:
tool_starttool_messagetool_donetool_errortool_start{
"nodeId": "Tool_ReadBlueprint",
"name": "ReadFile",
"input": { "file_path": "docs/blueprint.md" }
}
tool_message{
"nodeId": "Tool_ReadBlueprint",
"name": "ReadFile",
"level": "info",
"message": "Reading file",
"data": { "file_path": "docs/blueprint.md" }
}
tool_done{
"nodeId": "Tool_ReadBlueprint",
"name": "ReadFile",
"output": { "result": "...file content..." }
}
tool_error{
"nodeId": "Tool_ReadBlueprint",
"name": "ReadFile",
"error": "ENOENT: file not found"
}
工具相关事件必须满足以下要求:
runID 与 clientRunTokenrunID 是运行时正式分配的执行实例标识;但在部分实现中,它可能直到 workflow_start 之后或首次 checkpoint 之后才稳定。
3.17 引入 clientRunToken 作为前置、稳定、由调用方或前端先生成的运行归属标识。
要求:
clientRunToken 应从 workflow_start 开始就随事件一起抛出Run、rerunFromStep、executeFrom,都应拥有新的 clientRunTokenrunID 被复用,外层 UI 也不应因此把不同 session 混成同一条 run对于同一个 workflow 图上的多次并行运行:
pause / resume / cancel 应按选定 run 生效tool_*、llm_*、checkpoint、node_* 事件都应能按 runID 或 clientRunToken 正确归属3.17 明确要求:基于 checkpoint 的继续执行不仅适用于简单串行流,也应覆盖 fork / loop 等复杂结构。
rerunFromStep / executeFrom当调用方指定某个节点作为继续执行起点时,运行时应:
overrides 覆盖输入变量3.17 认可“workflow 调度 workflow”作为推荐模式之一。
典型形式包括:
Tool_* 调用宿主 WorkflowRun 能力node_* / tool_* / checkpoint 摘要通过 tool_message 或宿主桥接向父级外抛该模式特别适合:
从 3.16 迁移到 3.17 时,建议按以下顺序进行:
"version": "3.16",避免一次性打断旧运行时Tool_*、tool_message、clientRunToken、checkpoint 续跑的实现,显式标注支持 3.17 扩展剖面"version": "3.17"本规范的 canonical copy 发布于 DocCenter Path 3。若本地镜像与文档中心不一致,以文档中心最新版本为准。