关于 workflow
面向 Vercel Workflow SDK 开发的编码技能,指导 Agent 查阅与已安装版本对应的文档,并介绍持久化工作流、步骤及 WorkflowAgent 的使用模式。
主要功能
- 01搜索对应版本的 SDK 文档
- 02将编排与步骤逻辑分离
- 03使用 WorkflowAgent 构建持久化 Agent
- 04处理工作流钩子与运行限制
适用场景
- 01针对已安装版本开发 Workflow SDK
- 02将可重试的副作用组织为步骤
- 03实现可等待外部事件的工作流
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vercel/ workflow --skill workflow安装后在 Claude Code 中输入/workflow确认已加载。
详细介绍
重要:始终使用正确的 workflow 文档
你对 workflow 的了解已经过时。
以下 workflow 文档与已安装的 Workflow SDK 版本一致。
开始任何与 workflow 相关的任务前,请遵循以下说明:
搜索 node_modules/workflow/docs/ 中打包的文档:
- 查找文档:
glob "node_modules/workflow/docs/**/*.mdx" - 搜索内容:
grep "your query" node_modules/workflow/docs/
node_modules/workflow/docs/ 中的文档结构:
getting-started/- 框架设置(next.mdx、express.mdx、hono.mdx 等)foundations/- 核心概念(workflows-and-steps.mdx、hooks.mdx、streaming.mdx 等)api-reference/workflow/- API 文档(sleep.mdx、create-hook.mdx、fatal-error.mdx 等)api-reference/workflow-api/- 客户端 API(start.mdx、get-run.mdx、resume-hook.mdx 等)api-reference/workflow-runtime/- 运行时 API(get-world.mdx)和world/World SDK(storage.mdx、streams.mdx、queue.mdx)api-reference/workflow-observability/- Hydration 和名称解析工具(hydrate-resource-io.mdx、parse-workflow-name.mdx 等)ai/:AI SDK 集成文档errors/- 错误代码文档worlds/- 各个 World 的行为和限制(vercel.mdx、local.mdx、postgres.mdx)。其他页面会链接到/worlds/<name>。
相关软件包也包含打包的文档:
@ai-sdk/workflow:node_modules/ai/docs/- WorkflowAgent 和 AI SDK 集成@workflow/ai:node_modules/@workflow/ai/docs/- 适用于现有应用的弃用 DurableAgent API@workflow/core:node_modules/@workflow/core/docs/- 核心运行时(foundations、how-it-works)@workflow/next:node_modules/@workflow/next/docs/- Next.js 集成
如有疑问,请更新到最新版本的 Workflow SDK。
官方资源
快速参考
指令:
"use workflow"; // First line - makes async function durable
"use step"; // First line - makes function a cached, retryable unit
必要导入:
// Workflow primitives
import { sleep, fetch, createHook, createWebhook, getWritable } from "workflow";
import { FatalError, RetryableError } from "workflow";
import { getWorkflowMetadata, getStepMetadata } from "workflow";
// API operations
import { start, getRun, resumeHook, resumeWebhook } from "workflow/api";
// Observability & data hydration
import { hydrateResourceIO, observabilityRevivers, parseStepName, parseWorkflowName } from "workflow/observability";
// Framework integrations
import { withWorkflow } from "workflow/next";
import { workflow } from "workflow/vite";
import { workflow } from "workflow/astro";
// Or use modules: ["workflow/nitro"] for Nitro/Nuxt
// AI agent (Workflow 5)
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
优先使用步骤函数,避免沙盒错误
"use workflow" 函数在沙盒 VM 中运行。"use step" 函数拥有完整的 Node.js 访问权限。将逻辑放入步骤中,仅使用工作流函数进行编排。
// Steps have full Node.js and npm access
async function fetchUserData(userId: string) {
"use step";
const response = await fetch(`https://api.example.com/users/${userId}`);
return response.json();
}
async function processWithAI(data: any) {
"use step";
// AI SDK works in steps without workarounds
return await generateText({
model: "spacexai/grok-4.6",
prompt: `Process: ${JSON.stringify(data)}`,
});
}
// Workflow orchestrates steps - no sandbox issues
export async function dataProcessingWorkflow(userId: string) {
"use workflow";
const data = await fetchUserData(userId);
const processed = await processWithAI(data);
return { success: true, processed };
}
**优点:**步骤会自动重试,结果会持久化以供重放,并且不受沙盒限制。
工作流沙盒限制
当你需要直接在工作流函数中(而不是在步骤中)编写逻辑时,以下限制适用:
| 限制 | 解决方法 |
|---|---|
不支持 fetch() | 从 "workflow" 导入 fetch,然后设置 globalThis.fetch = fetch |
不支持 setTimeout/setInterval | 使用 "workflow" 中的 sleep("5s") |
| 不支持 Node.js 模块(fs、crypto 等) | 将逻辑移至步骤函数 |
示例——在工作流上下文中使用 fetch:
import { fetch } from "workflow";
export async function myWorkflow() {
"use workflow";
globalThis.fetch = fetch; // Required for AI SDK and HTTP libraries
// Now generateText() and other libraries work
}
**注意:**普通的 "provider/model" 字符串会使用 Vercel AI Gateway。除非用户明确需要仅限特定提供商的功能,否则不要构造直接的提供商实例。
WorkflowAgent:Workflow 5 中的 AI Agent
使用 AI SDK 的 WorkflowAgent 在 Workflow 5 中构建持久化 Agent。它取代了 @workflow/ai 中已弃用的 DurableAgent API,并为模型调用和由步骤支持的工具创建检查点。
import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
import { isStepCount, tool } from "ai";
import { getWritable } from "workflow";
import { z } from "zod";
async function lookupData({ query }: { query: string }) {
"use step";
// Step functions have full Node.js access
return `Results for "${query}"`;
}
export async function myAgentWorkflow(userMessage: string) {
"use workflow";
const agent = new WorkflowAgent({
model: "spacexai/grok-4.6",
instructions: "You are a helpful assistant.",
tools: {
lookupData: tool({
description: "Search for information",
inputSchema: z.object({ query: z.string() }),
execute: lookupData,
}),
},
});
const result = await agent.stream({
messages: [{ role: "user", content: userMessage }],
writable: getWritable<ModelCallStreamPart>(),
stopWhen: isStepCount(10),
});
return result.messages;
}
要点:
- 普通的
"provider/model"字符串会通过 Vercel AI Gateway 路由;Workflow 示例中的默认模型是spacexai/grok-4.6 getWritable<ModelCallStreamPart>()会流式传输持久化模型调用的输出;在 HTTP 路由中使用createModelCallToUIChunkTransform()将其转换- 需要访问 Node.js/npm 的工具
execute函数应使用"use step" - 使用工作流原语(
sleep()、createHook())的工具execute函数不应使用"use step",因为它们在工作流层级运行 stopWhen用于限制模型调用次数;默认情况下,模型停止调用工具时就会停止- 多轮对话:将
result.messages与新的用户消息传递给后续的agent.stream()调用
如需了解更多详情,请查阅已安装的 AI SDK 包中的 WorkflowAgent 文档,或访问 https://ai-sdk.dev/v7/docs/agents/workflow-agent。
启动工作流和子工作流
使用 start() 从 API 路由启动工作流。在 Workflow 5 中,也可以直接从工作流函数调用 start() 来生成子运行;它由步骤支持,并会在父级事件日志中记录一个确定性边界。
import { start } from "workflow/api";
// From an API route; works directly
export async function POST() {
const run = await start(myWorkflow, [arg1, arg2]);
return Response.json({ runId: run.runId });
}
// No-args workflow
const run = await start(noArgWorkflow);
从 Workflow 5 工作流内部启动子工作流:
import { start } from "workflow/api";
export async function parentWorkflow() {
"use workflow";
const childRun = await start(childWorkflow, ["some data"]);
await sleep("1h");
return { childRunId: childRun.runId };
}
start() 创建子运行后就会返回,不会等待其完成。只有当父级需要等待子运行时,才使用 childRun.returnValue;在工作流中访问 Run 的每个属性或调用其每个方法,都会被视为一个步骤。
运行规模与并发:了解何时拆分
有三个需要把握的限制,而且三者都有上限。不要把你记得的任何数值当作权威数据——当前值发布在工作流运行限制页面上,该页面是唯一应引用的数据来源。
每次运行的事件数。 单次运行的事件日志有上限,超过上限后运行会以 MAX_EVENTS_EXCEEDED 失败。事件不等同于步骤:一个首次成功的步骤会记录三个事件(step_created、step_started、step_completed),重试时还会多记录一到两个事件,钩子、休眠和 Webhook 也各自会记录事件。应在远低于上限时就拆分为子工作流——定价页面建议在事件数超过几千条时进行拆分,因为运行失败之前,重放速度就会显著下降。
每次运行的步骤数。 步骤数有独立于事件数的上限,因此即使长串行链路保持狭窄,也有数量限制。如果链路中的步骤可能达到五位数,应将多个项目合并到一个步骤中处理。
并发。 大量扇出不会被拒绝,而是会受到限流:每次运行每秒创建事件的数量受到限制,因此对几千个项目执行扁平的 Promise.all 时,大部分时间都会花在退避等待上。应改为分批或合并处理——将列表分成多个块处理,或在每个步骤中处理多个项目,从而减少并发运行的单元数量。为每个项目生成一个子运行,并不会自动缩小扇出范围;这样做可以限制每个子运行的日志规模并隔离故障,出于这些原因值得采用,但不能替代分块处理。
你无法自行提高这些上限——WORKFLOW_MAX_EVENTS_OVERRIDE 只能降低上限,而在 Vercel World 上,上限由服务方管理。不过,Vercel 会应请求提高每次运行的事件数和步骤数限制,因此如果确实需要运行大型工作流,除了设计问题,也应联系支持团队。
const BATCH = 100;
async function processItem(item: string) {
"use step";
return item.toUpperCase();
}
// One step per item, all in flight at once, all in one log
export async function processAll(items: string[]) {
"use workflow";
await Promise.all(items.map((item) => processItem(item)));
}
// Chunked, so only BATCH steps are in flight at a time
export async function processBatched(items: string[]) {
"use workflow";
for (let i = 0; i < items.length; i += BATCH) {
await Promise.allSettled(items.slice(i, i + BATCH).map((item) => processItem(item)));
}
}
// Bundled, so one step covers many items and the log stays short
async function processChunk(chunk: string[]) {
"use step";
return chunk.map((item) => item.toUpperCase());
}
export async function processBundled(items: string[]) {
"use workflow";
for (let i = 0; i < items.length; i += BATCH) {
await processChunk(items.slice(i, i + BATCH));
}
}
processAll 是应避免用于大规模处理的模式。processBatched 能限制并发,但仍会为每个项目记录事件。processBundled 则两者都能限制,因为一个步骤会处理 BATCH 个项目——这是三种模式中唯一一种会随着 BATCH 增大而减少事件数的模式。
Hooks:通过外部事件暂停和恢复
Hooks 可让工作流等待外部数据。在工作流中使用 createHook(),并从 API 路由调用 resumeHook()。确定性令牌仅适用于 createHook() + resumeHook()(服务端)。createWebhook() 始终会生成随机令牌,因此不要向 createWebhook() 传递 token 选项。
单个事件
import { createHook } from "workflow";
export async function approvalWorkflow() {
"use workflow";
const hook = createHook<{ approved: boolean }>({
token: "approval-123", // deterministic token for external systems
});
const result = await hook; // Workflow suspends here
return result.approved;
}
多个事件(可迭代 Hooks)
Hooks 实现了 AsyncIterable。使用 for await...of 接收多个事件:
import { createHook } from "workflow";
export async function chatWorkflow(channelId: string) {
"use workflow";
const hook = createHook<{ text: string; done?: boolean }>({
token: `chat-${channelId}`,
});
for await (const event of hook) {
await processMessage(event.text);
if (event.done) break;
}
}
每次调用 resumeHook(token, payload) 都会向循环传递下一个值。
从 API 路由恢复
import { resumeHook } from "workflow/api";
export async function POST(req: Request) {
const { token, data } = await req.json();
await resumeHook(token, data);
return new Response("ok");
}
错误处理
对于永久性故障(不重试),使用 FatalError;对于暂时性故障,使用 RetryableError:
import { FatalError, RetryableError } from "workflow";
if (res.status === 429) {
throw new RetryableError("Rate limited", { retryAfter: "5m" });
}
if (res.status >= 400 && res.status < 500) {
throw new FatalError(`Client error: ${res.status}`);
}
序列化
传入或传出工作流和步骤的所有数据都必须可序列化。
支持的内置类型: string、number、boolean、null、undefined、bigint、普通对象、数组、Date、RegExp、URL、URLSearchParams、Map、Set、Headers、ArrayBuffer、类型化数组、Request、Response、ReadableStream、WritableStream。
不支持: 函数、Symbol、WeakMap/WeakSet。传递数据,而不是回调。
自定义类序列化
类实例可以通过实现 @workflow/serde 协议,在工作流/步骤边界之间序列化。当类具有带有 "use step" 的实例方法,或你希望在步骤之间传递类实例时,这一点至关重要。
安装: @workflow/serde 必须是包含该类的包的依赖项。
模式: 使用计算属性语法在类体内添加两个静态方法:
import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from "@workflow/serde";
export class Point {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
// Serialize: return plain data (must be devalue-compatible types only)
static [WORKFLOW_SERIALIZE](instance: Point) {
return { x: instance.x, y: instance.y };
}
// Deserialize: reconstruct from plain data
static [WORKFLOW_DESERIALIZE](data: { x: number; y: number }) {
return new Point(data.x, data.y);
}
async computeDistance(other: Point) {
"use step";
return Math.sqrt((this.x - other.x) ** 2 + (this.y - other.y) ** 2);
}
}
关键规则:
- 在类体内定义 serde 方法,作为使用计算属性语法的静态方法(
static [WORKFLOW_SERIALIZE](...))。SWC 插件会扫描类并检测这些方法。不要在类外部赋值(例如,(MyClass as any)[WORKFLOW_SERIALIZE] = ...)——编译器无法检测到这种写法。 - Serde 方法只能返回 devalue 兼容的类型(普通对象、数组、原始类型、Date、Map、Set、Uint8Array 等)。不能包含函数、类实例或 Node.js 专用对象。
- 为依赖 Node.js 的实例方法添加
"use step"。 SWC 插件会从工作流包中剥离"use step"方法体。这样可以将 Node.js 导入项(fs、crypto、child_process 等)排除在工作流沙盒之外。包含 serde 方法的类框架仍会保留在工作流包中;只有步骤方法体会被移除。 - 不要手动注册类。 SWC 插件会自动生成注册代码(一个设置
classId并将类添加到全局注册表的 IIFE)。手动调用registerSerializationClass()没有必要,而且容易出错。 - 不要使用动态导入来绕过沙盒限制。 如果类方法需要 Node.js API,正确的解决方案是使用
"use step",而不是/* @vite-ignore */ import(...)。
适合使用 serde 的场景: 纯数据类、领域模型、配置对象,以及可以为依赖 Node.js 的方法添加 "use step" 的类。
应避免使用 serde 的场景: 如果一个类在本质上无法与 Node.js API 分离(每个方法都需要 fs、net 等),且无法以类框架的形式存在于工作流沙盒中,则应将其完全保留在步骤函数中,并在边界之间传递普通数据对象。
验证 serde 合规性
使用以下工具验证类是否设置正确:
workflow transform <file> --check-serde-- 显示文件的 SWC 转换输出,并检查 serde 类是否合规(工作流包中不再包含 Node.js 导入)。workflow validate-- 扫描所有工作流文件并报告 serde 合规性问题。使用--json获取机器可读的输出。- SWC Playground --
workbench/swc-playground中的 Web 操场在检测到 serde 模式时会显示 Serde Analysis 面板。 - 构建时警告 -- 构建器会在工作流包中的 serde 类仍有 Node.js 内置模块导入时自动发出警告。
流式传输
使用 getWritable() 从工作流流式传输数据。getWritable() 可在工作流和步骤上下文中调用,但不能直接在工作流函数中与流交互(调用 getWriter()、write()、close())。必须将流传递给步骤函数以执行实际 I/O,也可以由步骤自行调用 getWritable()。
在工作流中获取流,并将其传递给步骤:
import { getWritable } from "workflow";
export async function myWorkflow() {
"use workflow";
const writable = getWritable();
await writeData(writable, "hello world");
}
async function writeData(writable: WritableStream, chunk: string) {
"use step";
const writer = writable.getWriter();
try {
await writer.write(chunk);
} finally {
writer.releaseLock();
}
}
直接在步骤中调用 getWritable()(无需传递流):
import { getWritable } from "workflow";
async function streamData(chunk: string) {
"use step";
const writer = getWritable().getWriter();
try {
await writer.write(chunk);
} finally {
writer.releaseLock();
}
}
命名空间流
使用 getWritable({ namespace: 'name' }) 为不同类型的数据创建多个独立流。这适用于分离日志与主要输出、不同日志级别、Agent 输出、指标或任何不同的数据通道。长时间运行的工作流可从命名空间流中受益,因为你可以只重放重要事件(例如最终结果),同时将详细日志保存在单独的流中。
示例:分离日志级别和 Agent 输出:
import { getWritable } from "workflow";
type LogEntry = { level: "debug" | "info" | "warn" | "error"; message: string; timestamp: number };
type AgentOutput = { type: "thought" | "action" | "result"; content: string };
async function logDebug(message: string) {
"use step";
const writer = getWritable<LogEntry>({ namespace: "logs:debug" }).getWriter();
try {
await writer.write({ level: "debug", message, timestamp: Date.now() });
} finally {
writer.releaseLock();
}
}
async function logInfo(message: string) {
"use step";
const writer = getWritable<LogEntry>({ namespace: "logs:info" }).getWriter();
try {
await writer.write({ level: "info", message, timestamp: Date.now() });
} finally {
writer.releaseLock();
}
}
async function emitAgentThought(thought: string) {
"use step";
const writer = getWritable<AgentOutput>({ namespace: "agent:thoughts" }).getWriter();
try {
await writer.write({ type: "thought", content: thought });
} finally {
writer.releaseLock();
}
}
async function emitAgentResult(result: string) {
"use step";
// Important results go to the default stream for replay
const writer = getWritable<AgentOutput>().getWriter();
try {
await writer.write({ type: "result", content: result });
} finally {
writer.releaseLock();
}
}
export async function agentWorkflow(task: string) {
"use workflow";
await logInfo(`Starting task: ${task}`);
await logDebug("Initializing agent context");
await emitAgentThought("Analyzing the task requirements...");
// ... agent processing ...
await emitAgentResult("Task completed successfully");
await logInfo("Workflow finished");
}
使用命名空间流:
import { start, getRun } from "workflow/api";
import { agentWorkflow } from "./workflows/agent";
export async function POST(request: Request) {
const run = await start(agentWorkflow, ["process data"]);
// Access specific streams by namespace
const results = run.getReadable({ namespace: undefined }); // Default stream (important results)
const infoLogs = run.getReadable({ namespace: "logs:info" });
const debugLogs = run.getReadable({ namespace: "logs:debug" });
const thoughts = run.getReadable({ namespace: "agent:thoughts" });
// Return only important results for most clients
return new Response(results, { headers: { "Content-Type": "application/json" } });
}
// Resume from a specific point (useful for long sessions)
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const runId = searchParams.get("runId")!;
const startIndex = parseInt(searchParams.get("startIndex") || "0", 10);
const run = getRun(runId);
// Resume only the important stream, skip verbose debug logs
const stream = run.getReadable({ startIndex });
return new Response(stream);
}
对于长时间运行的会话(50 分钟以上),命名空间流有助于提升重放性能。将详细/调试输出放在单独的命名空间中,这样你就可以只重放重要事件。
调试
# Check workflow endpoints are reachable
npx workflow health
npx workflow health --port 3001 # Non-default port
# Visual dashboard for runs
npx workflow web
npx workflow web <run_id>
# CLI inspection (use --json for machine-readable output, --help for full usage)
npx workflow inspect runs
npx workflow inspect run <run_id>
# For Vercel-deployed projects, specify backend and project
npx workflow inspect runs --backend vercel --project <project-name> --team <team-slug>
npx workflow inspect run <run_id> --backend vercel --project <project-name> --team <team-slug>
# Open Vercel dashboard in browser for a specific run
npx workflow inspect run <run_id> --web
npx workflow web <run_id> --backend vercel --project <project-name> --team <team-slug>
# Cancel a running workflow
npx workflow cancel <run_id>
npx workflow cancel <run_id> --backend vercel --project <project-name> --team <team-slug>
# --env defaults to "production"; use --env preview for preview deployments
深度链接到某次运行(分享 URL,无需浏览器)
使用 --url 打印仪表板深度链接并退出。不会打开浏览器,也
不会启动本地服务。当你需要向用户提供可点击的链接(PR 评论、Slack
消息、调试摘要),而不是打开 UI 时,这是合适的工具。(--web 会打开仪表板;--url 只会打印链接。)
# Vercel run: prints the Vercel dashboard URL for the run
npx workflow inspect run <run_id> --backend vercel --project <project> --team <team> --url
npx workflow web <run_id> --backend vercel --project <project> --team <team> --env preview --url
# Local run: prints the local web UI deep link
npx workflow inspect run <run_id> --url
# Machine-readable: --url --json prints { "url": "..." } to stdout
npx workflow inspect run <run_id> --backend vercel --url --json
生成的 URL 格式:
- Vercel:
https://vercel.com/<team-slug>/<project-slug>/workflows/runs/<run_id>?environment=<production|preview>(--env用于选择环境;默认为production。解析团队 slug 需要先通过vercel login登录,并关联项目。) - 本地:
http://localhost:<port>?resource=run&id=<run_id>(端口默认为3456;运行npx workflow web服务期间,此链接有效)。
stdout 仅包含 URL(或 JSON 对象)。其他所有输出都会发送到
stderr,因此你可以直接捕获它,例如:URL=$(npx workflow web <run_id> --backend vercel --url)。
调试提示:
- 对任何命令使用
--json(-j)以获取机器可读的输出 - 使用
--web在浏览器中打开 Vercel Observability 仪表板,或使用--url打印深度链接 - 对任何命令使用
--help查看完整用法详情 - 只导入实际使用的工作流 API。未使用的导入可能导致 500 错误。
测试工作流
Workflow SDK 提供了 Vitest 插件,可在进程内测试工作流,无需运行服务。
单元测试步骤: 步骤是函数;没有编译器时,"use step" 不执行任何操作。直接测试这些函数:
import { describe, it, expect } from "vitest";
import { createUser } from "./user-signup";
describe("createUser step", () => {
it("should create a user", async () => {
const user = await createUser("[email protected]");
expect(user.email).toBe("[email protected]");
});
});
集成测试: 对使用 sleep()、钩子、Webhook 或重试的工作流,使用 @workflow/vitest。将它与 workflow 一起安装,并确保两者的主版本号相同:npm i -D @workflow/vitest。如果插件的 @workflow/core 主版本号与应用不一致,它会使运行失败。
// vitest.integration.config.ts
import { defineConfig } from "vitest/config";
import { workflow } from "@workflow/vitest";
export default defineConfig({
plugins: [workflow()],
test: {
include: ["**/*.integration.test.ts"],
testTimeout: 60_000,
},
});
// approval.integration.test.ts
import { describe, it, expect } from "vitest";
import { start, getRun, resumeHook } from "workflow/api";
import { waitForHook, waitForSleep } from "@workflow/vitest";
import { approvalWorkflow } from "./approval";
describe("approvalWorkflow", () => {
it("should publish when approved", async () => {
const run = await start(approvalWorkflow, ["doc-123"]);
// Wait for the hook, then resume it
await waitForHook(run, { token: "approval:doc-123" });
await resumeHook("approval:doc-123", { approved: true, reviewer: "alice" });
// Wait for sleep, then wake it up
const sleepId = await waitForSleep(run);
await getRun(run.runId).wakeUp({ correlationIds: [sleepId] });
const result = await run.returnValue;
expect(result).toEqual({ status: "published", reviewer: "alice" });
});
});
测试 Webhook: 使用带有 Request 对象的 resumeWebhook()。无需 HTTP 服务:
import { start, resumeWebhook } from "workflow/api";
import { waitForHook } from "@workflow/vitest";
const run = await start(ingestWorkflow, ["ep-1"]);
const hook = await waitForHook(run); // Discovers the random webhook token
await resumeWebhook(hook.token, new Request("https://example.com/webhook", {
method: "POST",
body: JSON.stringify({ event: "order.created" }),
}));
主要 API:
start():触发工作流run.returnValue:等待工作流完成waitForHook(run, { token? })/waitForSleep(run):等待工作流到达暂停点resumeHook(token, data)/resumeWebhook(token, request):恢复已暂停的工作流getRun(runId).wakeUp({ correlationIds }):跳过sleep()调用getWorkflowRef(name)/listWorkflowRefs():当测试无法导入函数时,从测试构建的清单中查找工作流(切勿手动编写workflow//...ID)
最佳实践:
- 将单元测试(不使用插件)和集成测试(使用
workflow()插件)放在不同的配置中 - 安装
@workflow/vitest时,确保其主版本号与workflow相同,并同时升级两者 - 使用基于测试数据的确定性钩子令牌,以便更轻松地恢复执行
- 设置较宽裕的
testTimeout值,因为工作流的运行时间可能长于典型的单元测试 vi.mock()无法作用于工作流主体(它们在 VM 中运行);只有当生成的 bundle 通过 Vitest 模块运行器加载时,vi.mock()才能作用于步骤代码。项目本地模块会打包进步骤 bundle,因此应模拟 npm 叶子依赖、注入依赖,或对步骤进行单元测试
可观测性与 World SDK
使用 await getWorld() 构建可观测性仪表板、管理面板,并检查工作流状态。getWorld() 是异步的,返回 Promise<World>(基于动态导入/环境的设置)。
主要导入:
import { getWorld } from "workflow/runtime";
import { hydrateResourceIO, observabilityRevivers, parseStepName, parseWorkflowName } from "workflow/observability";
主要文档(搜索 node_modules/workflow/docs/ 获取完整详情):
api-reference/workflow-runtime/world/storage.mdx:事件、运行、步骤和钩子(事件是事实来源;其他内容是物化视图)api-reference/workflow-observability/:水合与名称解析
World SDK 方法签名
⚠️ 分页参数嵌套在 { pagination: { cursor } } 中,而不是直接使用 { cursor }。
const world = await getWorld();
// Runs
const { data, cursor } = await world.runs.list({ pagination: { cursor }, resolveData: 'all' | 'none' });
const run = await world.runs.get(runId, { resolveData: 'all' | 'none' });
// Cancel via event creation (no cancel() method on runs)
await world.events.create(runId, { eventType: 'run_cancelled' });
// Steps: runId is top-level, NOT inside pagination
const { data, cursor } = await world.steps.list({ runId, pagination: { cursor }, resolveData: 'all' | 'none' });
const step = await world.steps.get(runId, stepId, { resolveData: 'all' | 'none' });
// Events
const { data, cursor } = await world.events.list({ runId, pagination: { cursor } });
await world.events.create(runId, { eventType: 'run_cancelled' });
// Hooks
const hook = await world.hooks.get(hookId);
const hook = await world.hooks.getByToken(token);
// Streams (methods on world.streams)
await world.streams.write(runId, name, chunk);
await world.streams.writeMulti?.(runId, name, chunks);
const readable = await world.streams.get(runId, name, startIndex);
await world.streams.close(runId, name);
const streamNames = await world.streams.list(runId);
const chunks = await world.streams.getChunks(runId, name, { limit, cursor });
const info = await world.streams.getInfo(runId, name);
// Queue (methods live directly on world as internal SDK infrastructure)
await world.queue(queueName, payload, opts);
const deploymentId = await world.getDeploymentId();
resolveData 参数
控制是否在响应中包含输入/输出数据。可接受 'all'(默认值)或 'none'。
重要: 即使设置为 'all',数据仍会经过 devalue 序列化。必须调用 hydrateResourceIO() 才能获取可用的 JS 值。
- 对于状态轮询、进度仪表板和运行列表,使用
'none' - 需要检查实际步骤 I/O 数据时,使用
'all'(或省略此参数),然后务必进行水合
// Lightweight status check with no I/O loaded
const run = await world.runs.get(runId, { resolveData: 'none' });
console.log(run.status); // 'running' | 'completed' | 'failed' | 'cancelled'
// Full inspection: resolveData includes data, hydrateResourceIO deserializes it
const step = await world.steps.get(runId, stepId); // defaults to 'all'
const hydrated = hydrateResourceIO(step, observabilityRevivers);
常见错误:设置
resolveData: 'all'后检查step.input !== undefined,并认为数据已可直接使用。数据确实存在,但仍处于序列化状态,因此务必先进行水合。
数据水合(devalue 格式)
步骤 I/O 使用 devalue 序列化,并带有 4 字节格式前缀(devl)。未经水合时,input/output 是类似 Uint8Array、带有数字键的对象:
{"0":100,"1":101,"2":118,"3":108,...} 中包含的值未经水合后无法使用。
使用 I/O 数据前务必先进行水合:
import { hydrateResourceIO, observabilityRevivers } from "workflow/observability";
const { data: steps } = await world.steps.list({ runId, resolveData: 'all' });
const hydrated = steps.map(s => hydrateResourceIO(s, observabilityRevivers));
// hydrated[0].input → [123, 2] (actual function arguments)
// hydrated[0].output → 125 (actual return value)
hydrateResourceIO 同时适用于 Step 和 WorkflowRun 对象。对于加密工作流,请使用 getEncryptionKeyForRun() + hydrateResourceIOWithKey()。
名称解析
parseWorkflowName()、parseStepName() 和 parseClassName() 返回 { shortName: string, moduleSpecifier: string } | null。务必使用可选链:
const parsed = parseWorkflowName("workflow//./src/workflows/order//processOrder");
// parsed?.shortName → "processOrder"
// parsed?.moduleSpecifier → "./src/workflows/order"
// ⚠️ Returns null if format doesn't match
事件类型
事件是仅追加的数据事实来源。Run/Step/Hook 是物化视图。
| 类别 | 类型 |
|---|---|
| Run | run_created、run_started、run_completed、run_failed、run_cancelled |
| Step | step_created、step_started、step_completed、step_failed、step_retrying |
| Hook | hook_created、hook_received、hook_disposed、hook_conflict |
| Wait | wait_created、wait_completed |
错误处理模式
针对不同故障模式的三种错误策略:
| 错误类型 | 适用场景 | 行为 |
|---|---|---|
FatalError | 永久性故障(输入无效、身份验证被拒绝) | 立即终止工作流,不重试 |
RetryableError | 暂时性故障(速率限制、超时) | 重试,可选择设置 retryAfter 延迟 |
Promise.allSettled | 关键性各不相同的并行步骤 | 即使部分步骤失败,也会继续执行 |
import { FatalError, RetryableError } from "workflow";
// Permanent failure, so the workflow terminates
throw new FatalError("Invalid input: missing required field");
// Transient failure, so it will retry
throw new RetryableError("API rate limited", { retryAfter: "5m" });
// Mixed criticality parallel execution
const results = await Promise.allSettled([
criticalStep(data), // Must succeed
optionalStep(data), // OK to fail
enrichmentStep(data), // OK to fail
]);
const [critical, optional, enrichment] = results;
if (critical.status === "rejected") throw new FatalError(critical.reason);