migrating-workflow-v4-to-v5
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMigrating Workflow SDK 4.x to 5.0
将 Workflow SDK 从 4.x 迁移到 5.0
Workflow SDK 5.0 keeps the programming model from 4.x. / , , , hooks, webhooks, streams, , retries, and the event log are unchanged, so most application code compiles as-is.
"use workflow""use step"start()getRun()sleep()The breaking changes are concentrated in three places:
- Runtime entrypoints (,
workflow/api) — two exports removed.workflow/runtime - The interface — only relevant if the app implements a custom World or calls
Worlddirectly.getWorld() - Build integrations — subpaths, and private compiler subpaths that were never public.
@workflow/nest
Do not rewrite workflow or step bodies. If you find yourself restructuring business logic, you have gone outside this migration.
Workflow SDK 5.0 保留了 4.x 的编程模型。 / 、、、hooks、webhooks、streams、、重试以及事件日志均未改变,因此大多数应用代码可以原样编译。
"use workflow""use step"start()getRun()sleep()破坏性变更集中在三个方面:
- 运行时入口(、
workflow/api)——移除了两个导出。workflow/runtime - 接口——仅当应用实现了自定义 World 或直接调用
World时才相关。getWorld() - 构建集成——子路径,以及从未公开的编译器私有子路径。
@workflow/nest
不要重写 workflow 或 step 的函数体。如果你发现自己在重构业务逻辑,那已经超出了本次迁移的范围。
Intake
信息收集
Before editing, establish:
- Which packages are installed. Read for
package.jsonand everyworkflowdependency.@workflow/* - Whether the app touches the runtime. Grep for ,
getWorld,createWorld,getWorldHandlers,writeToStream,readFromStream,closeStream,listStreamsByRunId,getStreamChunks,world.steps,listByCorrelationId,runStep,stepEntrypoint,internal/private.core/private - Whether the app implements a custom World. Grep for ,
implements World,: World,createLocalWorld,createVercelWorld.startWorkflowWorld - Which framework integration is in use. ,
@workflow/next,@workflow/nest,@workflow/nitro,@workflow/sveltekit,@workflow/vite,@workflow/nuxt, or the CLI.@workflow/astro - Whether is used. Grep for
hook.getConflict().getConflict - Whether hook metadata is read outside a workflow. Grep for and
getHookByToken, then forresumeHookon their results..metadata - Whether the app calls the compiler directly. Grep for ,
mode: 'client',transformSync. Only custom build integrations do this.swc-plugin-workflow - Whether is used. Grep for
experimental_setAttributes.experimental_setAttributes
Report anything in 2–8 that the app does not use as "not applicable" rather than silently skipping it.
在编辑之前,先确认:
-
安装了哪些包。 读取中的
package.json和所有workflow依赖。@workflow/* -
应用是否触及运行时。 搜索、
getWorld、createWorld、getWorldHandlers、writeToStream、readFromStream、closeStream、listStreamsByRunId、getStreamChunks、world.steps、listByCorrelationId、runStep、stepEntrypoint、internal/private。core/private -
应用是否实现了自定义 World。 搜索、
implements World、: World、createLocalWorld、createVercelWorld。startWorkflowWorld -
使用的是哪个框架集成。、
@workflow/next、@workflow/nest、@workflow/nitro、@workflow/sveltekit、@workflow/vite、@workflow/nuxt或 CLI。@workflow/astro -
是否使用了。 搜索
hook.getConflict()。getConflict -
是否在 workflow 外部读取 hook 元数据。 搜索和
getHookByToken,然后检查它们结果上的resumeHook。.metadata -
应用是否直接调用编译器。 搜索、
mode: 'client'、transformSync。只有自定义构建集成才会这样做。swc-plugin-workflow -
是否使用了。 搜索
experimental_setAttributes。experimental_setAttributes
对于 2–8 中应用未使用的内容,请报告为“不适用”,而不是静默跳过。
Step 1 — bump the dependencies
步骤 1 — 升级依赖
Move every and dependency to . They are released together and must not be mixed across majors — a 4.x against a 5.x will fail at build time.
workflow@workflow/*^5.0.0@workflow/nextworkflowjson
{
"dependencies": {
"workflow": "^5.0.0",
"@workflow/next": "^5.0.0"
}
}Then reinstall and rebuild so the compiler regenerates the workflow/step bundles and the generated routes under . Never hand-edit generated output.
.well-known/workflow/v1/Node requirements are unchanged: .
^18 || ^20 || ^22 || ^24将所有 和 依赖升级到 。它们是一起发布的,不能跨大版本混用——4.x 的 搭配 5.x 的 会在构建时失败。
workflow@workflow/*^5.0.0@workflow/nextworkflowjson
{
"dependencies": {
"workflow": "^5.0.0",
"@workflow/next": "^5.0.0"
}
}然后重新安装并重新构建,让编译器重新生成 workflow/step bundles 以及 下的生成路由。切勿手动编辑生成输出。
.well-known/workflow/v1/Node 版本要求不变:。
^18 || ^20 || ^22 || ^24Step 2 — apply the mechanical rewrites
步骤 2 — 应用机械重写
Apply each rule only where the pattern actually appears.
仅在模式实际出现的位置应用每条规则。
getWorld()
and createWorld()
are async
getWorld()createWorld()getWorld()
和 createWorld()
现在是异步的
getWorld()createWorld()ts
// v4
const world = getWorld();
// v5
const world = await getWorld();This also applies to . Awaiting was already correct in 4.x, so this edit is safe to make before the dependency bump. Propagate up the call chain rather than wrapping in chains.
getWorldHandlers()async.then()ts
// v4
const world = getWorld();
// v5
const world = await getWorld();这也适用于 。在 4.x 中 已经是正确的,因此这个修改可以在依赖升级之前安全进行。在调用链上向上传播 ,而不是用 链包裹。
getWorldHandlers()awaitasync.then()createLocalWorld()
and createVercelWorld()
removed
createLocalWorld()createVercelWorld()createLocalWorld()
和 createVercelWorld()
已移除
createLocalWorld()createVercelWorld()First-party World packages now expose a single factory. The arguments are unchanged — this is a rename only.
createWorld()ts
// v4
import { createLocalWorld } from '@workflow/world-local';
const world = createLocalWorld({ dataDir });
// v5
import { createWorld } from '@workflow/world-local';
const world = createWorld({ dataDir });The same applies to from .
createVercelWorld@workflow/world-vercel第一方 World 包现在暴露单一的 工厂。参数不变——这仅是重命名。
createWorld()ts
// v4
import { createLocalWorld } from '@workflow/world-local';
const world = createLocalWorld({ dataDir });
// v5
import { createWorld } from '@workflow/world-local';
const world = createWorld({ dataDir });@workflow/world-vercelcreateVercelWorldrunStep
removed from workflow/api
runStepworkflow/apirunStep
已从 workflow/api
移除
runStepworkflow/apiCall the step function directly. The compiler routes the call through the step runtime.
ts
// v4
import { runStep } from 'workflow/api';
const result = await runStep(chargeCard, [orderId]);
// v5
const result = await chargeCard(orderId);直接调用 step 函数。编译器会将调用路由到 step 运行时。
ts
// v4
import { runStep } from 'workflow/api';
const result = await runStep(chargeCard, [orderId]);
// v5
const result = await chargeCard(orderId);stepEntrypoint
removed from workflow/runtime
stepEntrypointworkflow/runtimestepEntrypoint
已从 workflow/runtime
移除
stepEntrypointworkflow/runtimeFramework integrations generate step routes themselves — delete hand-written step routes that existed only to call . For a custom host, serve the handlers from instead.
stepEntrypointgetWorldHandlers()框架集成会自行生成 step 路由——删除那些仅为调用 而存在的手写 step 路由。对于自定义 host,请改为从 提供 handlers。
stepEntrypointgetWorldHandlers()workflow/internal/private
and @workflow/core/private
removed
workflow/internal/private@workflow/core/privateworkflow/internal/private
和 @workflow/core/private
已移除
workflow/internal/private@workflow/core/privateThese subpaths were never public API. Remove the imports; if generated build output still references them, it is stale — reinstall and rebuild rather than restoring the imports.
这些子路径从来不是公共 API。删除这些导入;如果生成的构建输出仍引用它们,说明输出已过期——重新安装并重新构建,而不是恢复导入。
Stream methods moved to world.streams.*
with runId
first
world.streams.*runIdStream 方法移至 world.streams.*
,且 runId
放在第一位
world.streams.*runId| v4 | v5 |
|---|---|
| |
| |
| |
| |
| |
| |
The argument order flipped, so a rename alone silently passes a stream name where a run ID is expected. Swap the arguments at every call site. had no parameter at all — requires one, so thread the owning run ID through to the call.
readFromStreamrunIdstreams.getApplication code that uses inside a workflow or reads is unaffected; this rule is only for direct access.
getWritable()run.readableWorld| v4 | v5 |
|---|---|
| |
| |
| |
| |
| |
| |
参数顺序颠倒了,因此仅重命名会在期望 run ID 的位置静默传入 stream 名称。在每个调用点交换参数。 原本根本没有 参数—— 需要一个,因此需要将所属 run ID 传递到调用处。
readFromStreamrunIdstreams.get在 workflow 内使用 或读取 的应用代码不受影响;此规则仅适用于直接访问 。
getWritable()run.readableWorldworld.steps.get()
requires a runId
world.steps.get()runIdworld.steps.get()
需要 runId
world.steps.get()runIdThe first parameter was and is now . Pass the run ID that owns the step.
string | undefinedstringts
// v4
const step = await world.steps.get(undefined, stepId);
// v5
const step = await world.steps.get(runId, stepId);第一个参数原本是 ,现在是 。传入拥有该 step 的 run ID。
string | undefinedstringts
// v4
const step = await world.steps.get(undefined, stepId);
// v5
const step = await world.steps.get(runId, stepId);events.listByCorrelationId()
requires a runId
events.listByCorrelationId()runIdevents.listByCorrelationId()
需要 runId
events.listByCorrelationId()runIdA correlation ID identifies a step, hook or wait within its run, not across runs. The lookup is scoped to one run, so pass the run that owns the ID. The same applies to .
analytics.events.listByCorrelationId()ts
// v4
const events = await world.events.listByCorrelationId({ correlationId });
// v5
const events = await world.events.listByCorrelationId({ runId, correlationId });correlation ID 标识的是某个 run 内的 step、hook 或 wait,而不是跨 run。查询被限定在单个 run 内,因此传入拥有该 ID 的 run。 同理。
analytics.events.listByCorrelationId()ts
// v4
const events = await world.events.listByCorrelationId({ correlationId });
// v5
const events = await world.events.listByCorrelationId({ runId, correlationId });hook.getConflict()
resolves with a Run
hook.getConflict()Runhook.getConflict()
解析为一个 Run
hook.getConflict()RunThe resolved value is now the conflicting run handle rather than . still reads the same, so existing code keeps working — but the round trip through a step to fetch the run can be deleted.
{ runId }conflict.runIdts
// v4
const conflict = await hook.getConflict();
if (conflict) {
const run = await fetchRun(conflict.runId); // "use step" wrapper around getRun()
return { dedupedTo: await run.returnValue };
}
// v5
const conflict = await hook.getConflict();
if (conflict) {
return { dedupedTo: await conflict.returnValue };
}await conflict.statusawait conflict.cancel()if (conflict)getConflict()null解析值现在是冲突的 run handle,而不是 。 仍然可以读取同样的值,因此现有代码可以继续工作——但通过 step 往返获取 run 的代码可以删除。
{ runId }conflict.runIdts
// v4
const conflict = await hook.getConflict();
if (conflict) {
const run = await fetchRun(conflict.runId); // "use step" wrapper around getRun()
return { dedupedTo: await run.returnValue };
}
// v5
const conflict = await hook.getConflict();
if (conflict) {
return { dedupedTo: await conflict.returnValue };
}同一个 handle 上可以使用 和 。不要移除 空值检查——当 token 被正常认领时, 仍会解析为 。
await conflict.statusawait conflict.cancel()if (conflict)getConflict()nullhook.metadata
is a Promise
hook.metadatahook.metadata
是一个 Promise
hook.metadataOn the hook returned by and , is now a getter that returns a memoized Promise, the same shape as . Looking a hook up by token is a single read; hydrating the metadata can add network round trips, and those are paid only by code that awaits it.
getHookByToken()resumeHook()metadatarun.returnValuets
// v4
const hook = await getHookByToken(token);
if (hook.metadata?.allowedUserId !== userId) {
return new Response('Forbidden', { status: 403 });
}
// v5
const hook = await getHookByToken(token);
const metadata = (await hook.metadata) as { allowedUserId?: string } | undefined;
if (metadata?.allowedUserId !== userId) {
return new Response('Forbidden', { status: 403 });
}Awaiting it on a hook that stored no metadata resolves with no I/O, so the needs no guard. and no longer carry , so a handler that forwarded the whole hook object must forward the awaited value explicitly. , , and the other fields are unchanged, and still returns the raw record. Under TypeScript the old shape fails the build; in plain JavaScript reads off a Promise, so a check like the one above silently rejects every request.
undefinedawait{ ...hook }JSON.stringify(hook)metadatahook.runIdhook.tokenworld.hooks.getByToken()Hookhook.metadata.allowedUserIdundefined在 和 返回的 hook 上, 现在是一个 getter,返回一个带记忆化的 Promise,形状与 相同。按 token 查找 hook 是一次读取;填充 metadata 可能会增加网络往返,而这些开销只会由 它的代码承担。
getHookByToken()resumeHook()metadatarun.returnValueawaitts
// v4
const hook = await getHookByToken(token);
if (hook.metadata?.allowedUserId !== userId) {
return new Response('Forbidden', { status: 403 });
}
// v5
const hook = await getHookByToken(token);
const metadata = (await hook.metadata) as { allowedUserId?: string } | undefined;
if (metadata?.allowedUserId !== userId) {
return new Response('Forbidden', { status: 403 });
}在未存储 metadata 的 hook 上 它会无 I/O 地解析为 ,因此 不需要防护。 和 不再包含 ,因此转发整个 hook 对象的 handler 必须显式转发 await 后的值。、 以及其他字段不变, 仍然返回原始的 记录。在 TypeScript 下,旧形状会导致构建失败;在纯 JavaScript 中, 会从 Promise 上读取到 ,因此像上面这样的检查会静默拒绝所有请求。
awaitundefinedawait{ ...hook }JSON.stringify(hook)metadatahook.runIdhook.tokenworld.hooks.getByToken()Hookhook.metadata.allowedUserIdundefinedexperimental_setAttributes
renamed to setAttributes
experimental_setAttributessetAttributesexperimental_setAttributes
重命名为 setAttributes
experimental_setAttributessetAttributests
// v4
import { experimental_setAttributes } from 'workflow';
// v5
import { setAttributes } from 'workflow';The old name was removed in v5, so this rewrite is required. is likewise now . The option on is unchanged.
ExperimentalSetAttributesOptionsSetAttributesOptionsattributesstart()ts
// v4
import { experimental_setAttributes } from 'workflow';
// v5
import { setAttributes } from 'workflow';旧名称已在 v5 中移除,因此这个重写是必需的。 同样变成了 。 上的 选项不变。
ExperimentalSetAttributesOptionsSetAttributesOptionsstart()attributesmode: 'client'
removed from the SWC transform
mode: 'client'SWC transform 中移除了 mode: 'client'
mode: 'client'Only relevant to a custom build integration that calls the compiler itself. The mode merged into , which now absorbs hoisted variable references and dead-code elimination. Pass .
clientstepmode: 'step'仅与直接调用编译器的自定义构建集成相关。 模式已合并到 中,后者现在会吸收提升的变量引用并执行死代码消除。请传入 。
clientstepmode: 'step'NestLocalBuilder
moved out of the @workflow/nest
root
NestLocalBuilder@workflow/nestNestLocalBuilder
已移出 @workflow/nest
根路径
NestLocalBuilder@workflow/nestts
// v4
import { NestLocalBuilder } from '@workflow/nest';
// v5
import { NestLocalBuilder } from 'workflow/nest/builder';NestVercelBuilderworkflow/nest/vercel-builderWorkflowModule@workflow/nestts
// v4
import { NestLocalBuilder } from '@workflow/nest';
// v5
import { NestLocalBuilder } from 'workflow/nest/builder';NestVercelBuilderworkflow/nest/vercel-builderWorkflowModule@workflow/nestStep 3 — flag the behavior changes that need a decision
步骤 3 — 标出需要决策的行为变更
These are not code edits. Report each one that applies, and do not "fix" them silently.
- Tracing defaults to . Each workflow and step invocation is its own trace root with span links to the enqueue site and the run origin, instead of one trace per run. If the app has dashboards, saved queries, or alerts keyed on a single trace ID per run, either move them to the
linkedattribute or setworkflow.run.idto restore the 4.x shape.WORKFLOW_TRACE_MODE=continuous - The event-creation precondition guard is gone. no longer exists; remove it from the app's environment and deployment config if it is set. No World in the SDK rejects a write for a stale snapshot any more — a replay that is behind learns what it missed from the write it makes next.
WORKFLOW_PRECONDITION_GUARD - Turbo mode is on by default. The first invocation of a run backgrounds and skips the initial event-log load.
run_starteddisables it.WORKFLOW_TURBO=0 - Errors keep their type. now preserves the original class identity and cause chain. Code that pattern-matched on
WorkflowRunFailedError.causebecause the class was flattened in 4.x can useerror.message— but flag it rather than rewriting error handling unprompted.instanceof - Event IDs are slot numbers, not ULIDs. An event ID is now its 1-based position in the run's log (), unique only within a run and carrying no timestamp. Report any app code that uses an event ID as a global key (pair it with the
evnt_00000000000000000000000042) or decodes a time out of one (readrunIdoff the event). Other entity IDs are unchanged.createdAt - A per-run event limit is enforced. The World supplies the ceiling (25,000 events on the Local and Vercel Worlds) and a run that reaches it fails with . Flag any workflow with an unbounded loop; the fix is a child run per batch, which is a design change, not a migration edit.
MAX_EVENTS_EXCEEDED - Stream writes flush the leading chunk immediately. The flush window default went from 10ms to 0. An app that relied on the window to coalesce a burst of tiny chunks can set or
streamFlushIntervalMs.WORKFLOW_STREAM_FLUSH_INTERVAL_MS - Generated step, workflow and webhook bundles are ESM (the VM-executed workflow bundle stays CJS). Only matters for a host that post-processes build output.
- Duplicate step or workflow IDs now fail the build. In 4.x, two identically named non-exported functions across workspace files collided last-write-wins. If the build fails on this, rename one of them — do not suppress the check.
- rows written before the upgrade. Failed runs stored by 4.x read back with
world-postgres, because the payload lives in the legacyerror: undefinedtext column rather thanerror. There is no data migration; recent-history dashboards may show blank errors for pre-upgrade failures.errorJson - stream chunks moved to
world-local. Files in the old flat layout are not read back and stale files are left in place — local development state, so deleting the data directory is fine.streams/chunks/<streamName>/ - The workflow sandbox is stricter about nondeterminism. ,
WeakRef,FinalizationRegistry, and asyncAtomics.waitAsynccompilation are no longer available inside workflow functions, andWebAssemblycomputes synchronously (same results, deterministic timing). Grepcrypto.subtle.digestfiles for these APIs; the fix is moving that code into a step, which is a design change — flag it, do not restructure unprompted."use workflow" - In-flight runs do not migrate. Runs created on a 4.x deployment keep executing on that deployment. Let them finish where they started; do not add code to "drain" or re-target them.
这些不是代码修改。逐项报告适用的内容,不要静默“修复”它们。
- Tracing 默认使用 。 每次 workflow 和 step 调用都是独立的 trace root,并通过 span link 连接到入队位置和 run 来源,而不是每个 run 一个 trace。如果应用有按单个 trace ID 关联的仪表盘、保存的查询或告警,要么将它们迁移到
linked属性,要么设置workflow.run.id以恢复 4.x 的形状。WORKFLOW_TRACE_MODE=continuous - 事件创建的前置条件防护已移除。 不再存在;如果应用环境中设置了它,请将其从环境和部署配置中移除。SDK 中的任何 World 都不再拒绝针对过期快照的写入——落后的重放会从它下一次写入中学到错过的内容。
WORKFLOW_PRECONDITION_GUARD - Turbo 模式默认开启。 run 的首次调用会将 放到后台,并跳过初始事件日志加载。
run_started可禁用它。WORKFLOW_TURBO=0 - 错误会保留其类型。 现在保留原始类身份和 cause 链。在 4.x 中由于类被扁平化而基于
WorkflowRunFailedError.cause做模式匹配的代码,可以使用error.message——但请标出,而不是未经提示就重写错误处理。instanceof - 事件 ID 是槽位号,而不是 ULID。 事件 ID 现在是它在 run 日志中的从 1 开始的位置(),仅在单个 run 内唯一,且不携带时间戳。如果应用代码将事件 ID 用作全局键(请将其与
evnt_00000000000000000000000042配对)或从中解码时间(请从事件上读取runId),请报告。其他实体 ID 不变。createdAt - 强制实施每个 run 的事件上限。 World 提供上限(Local 和 Vercel Worlds 为 25,000 个事件),达到上限的 run 会以 失败。标出任何包含无界循环的 workflow;修复方法是为每个批次使用子 run,这是设计变更,而不是迁移编辑。
MAX_EVENTS_EXCEEDED - Stream 写入会立即刷出首个 chunk。 刷出窗口默认值从 10ms 变为 0。依赖该窗口来合并一小批微小 chunk 的应用可以设置 或
streamFlushIntervalMs。WORKFLOW_STREAM_FLUSH_INTERVAL_MS - 生成的 step、workflow 和 webhook bundles 是 ESM(由 VM 执行的 workflow bundle 仍是 CJS)。仅对会后处理构建输出的 host 有影响。
- 重复的 step 或 workflow ID 现在会导致构建失败。 在 4.x 中,不同工作区文件中两个同名且未导出的函数会以“最后写入者胜出”的方式冲突。如果构建因此失败,请重命名其中一个——不要抑制该检查。
- 升级前写入的 行。 4.x 存储的失败 run 读回来时
world-postgres,因为负载位于旧的error: undefined文本列而不是error中。没有数据迁移;最近历史仪表盘可能会对升级前的失败显示空白错误。errorJson - 的 stream chunks 已移动 到
world-local。旧扁平布局中的文件不会被读回,过期文件会留在原处——这是本地开发状态,因此删除数据目录没有问题。streams/chunks/<streamName>/ - workflow 沙箱对非确定性的限制更严格。 、
WeakRef、FinalizationRegistry和异步Atomics.waitAsync编译不再可用于 workflow 函数内部,WebAssembly现在同步计算(结果相同,时序确定)。在crypto.subtle.digest文件中搜索这些 API;修复方法是将该代码移入 step,这是设计变更——请标出,不要未经提示就重构。"use workflow" - 进行中的 run 不会迁移。 在 4.x 部署上创建的 run 会继续在该部署上执行。让它们在原处完成;不要添加“排空”或重新定向它们的代码。
Step 4 — custom World
implementations
World步骤 4 — 自定义 World
实现
WorldOnly if the app implements itself. Use the skill for this part (); it carries the full World migration, including the event ID allocation work summarized below. What follows is enough to scope the job and to know when it has not been done.
Worldmigrating-world-v4-to-v5npx skills add https://github.com/vercel/workflow --skill migrating-world-v4-to-v5Beyond the stream and step signatures above, the interface gained:
- as a namespace (see the table in step 2).
streams - — metadata-only listings for runs, steps, events, hooks, waits, and attributes.
analytics - attribute support on runs, including .
experimentalSetAttributes - capability advertisement, so the runtime can gate optimizations. Unadvertised capabilities fail closed, which means an incomplete World stays correct but slower — advertise a capability only once it is genuinely implemented. Event ID allocation is not among them: it is a requirement, not a capability. A World that rejected stale writes behind the old flag can delete that code, since the flag is gone and nothing replaced it.
preconditionGuard - an optional per-run event ceiling returned on run reads, which the runtime enforces.
- optional and a
createRunId()on queue options, for worlds that place run state regionally.region
Four contract changes affect existing implementations. The first is required and is not visible from the type signatures:
-
Event IDs are slots, and allocating them is mandatory. An event ID is no longer a ULID the World mints freely: it isfollowed by the event's 1-based position in that run's log, zero-padded to 26 characters (
evnt_fromslotToEventId()formats one). Positions must be unique and dense with no holes, so the race has to be settled in the store — a unique constraint on@workflow/worldor a conditional write — rather than by reading the maximum in process and adding one.(runId, eventId)params carryevents.create()to place the write; when that position is taken the World must not reject the create, it advances to the next free position, commits there, and returns the events occupying the skipped positions on the success response. Take the position at the commit, in the same operation that appends the event, since that is what stops an event landing behind a position a reader has already passed. There is no capability to declare and no fallback: the runtime reads a position out of every ID it loads and fails the run when it cannot, so a World whose IDs are not positions type-checks, starts runs, and fails on the first replay witheventCount.Event id is not slot-numberedWarn the user that runs already in their store cannot be replayed by the new code — a ULID-numbered run is not readable as positions and the runtime refuses it, so those runs must be drained on 4.x first, unless the platform pins a run to its creating deployment (Vercel does, which resolves it). Point them at's@workflow/world-testing, which catches a wrong ID scheme in the conformance suite instead of at replay time, and at the "Upgrading a World to v5" guide for the full rules.numbers events by position -
Suspension and dispatch. The asymmetricreturn contract for waits is gone. Waits are ordinary queue continuations carrying
{ timeoutSeconds }, and wait plus step dispatch is unified into one parallel batch per suspension. A World that special-cased the old wait return needs rewriting against the current interface.delaySeconds -
Step queue topics are retired. Thequeue kind no longer exists: queued steps travel on the workflow topic (carrying
'step'/stepIdin the payload) and execute in the combined flow handler. A World that provisioned or routed separatestepNametopics can drop them.__wkf_step_* -
World resolution happens at build time. Worlds are statically injected into host bundles rather than selected dynamically at runtime, and first-party World packages expose afactory. A custom or community World must be resolvable by the build; verify the app still boots against it rather than assuming a runtime lookup.
createWorld()
Optional methods may be omitted; the runtime falls back. Point the user at the skill and the "Upgrading a World to v5" guide, which carry the full interface delta and the contract changes above, rather than inventing method bodies.
migrating-world-v4-to-v5仅当应用自己实现 时才需要。这部分请使用 skill();它包含完整的 World 迁移,包括下面总结的事件 ID 分配工作。以下内容足以界定工作范围,并判断迁移是否尚未完成。
Worldmigrating-world-v4-to-v5npx skills add https://github.com/vercel/workflow --skill migrating-world-v4-to-v5除了上面的 stream 和 step 签名外,接口还新增了:
- 作为命名空间(见步骤 2 中的表格)。
streams - —— 仅返回元数据的 runs、steps、events、hooks、waits 和 attributes 列表。
analytics - run 上的 attribute 支持,包括 。
experimentalSetAttributes - 能力通告,使运行时可以对优化进行门控。未通告的能力会以“失败关闭”方式处理,这意味着不完整的 World 仍然正确但更慢——只有在真正实现某个能力后才通告它。事件 ID 分配不在其中:它是一个要求,而不是能力。过去在旧的 标志后拒绝过期写入的 World 可以删除该代码,因为该标志已不存在,也没有任何东西取代它。
preconditionGuard - 在 run 读取时返回的可选每个 run 事件上限,由运行时强制实施。
- 可选的 和 queue 选项上的
createRunId(),供按区域放置 run 状态的 worlds 使用。region
四个契约变更会影响现有实现。第一个是必须的,并且从类型签名中看不出来:
-
事件 ID 是槽位,并且必须分配它们。 事件 ID 不再是 World 随意生成的 ULID:它是后跟该事件在该 run 日志中的从 1 开始的位置,零填充到 26 个字符(
evnt_中的@workflow/world可以格式化一个)。位置必须唯一且密集、不能有空洞,因此竞争必须在存储中解决——在slotToEventId()上加唯一约束,或使用条件写入——而不是在进程中读取最大值再加一。(runId, eventId)参数携带events.create()来放置写入;当该位置已被占用时,World 不能拒绝创建,而要推进到下一个空闲位置,在那里提交,并在成功响应中返回占据被跳过位置的 events。在提交时获取位置,与追加事件的同一个操作中完成,因为这样才能阻止事件落在读取者已经越过的位置之后。没有可声明的能力,也没有回退:运行时会从它加载的每个 ID 中读取位置,无法读取时会让 run 失败,因此 ID 不是位置的 World 能通过类型检查、能启动 run,但会在第一次重放时以eventCount失败。Event id is not slot-numbered警告用户,其存储中已有的 run 无法被新代码重放——带 ULID 编号的 run 不能按位置读取,运行时会拒绝它,因此这些 run 必须先在 4.x 上排空,除非平台将 run 固定到创建它的部署(Vercel 会这样做,这解决了问题)。请指向的@workflow/world-testing,它会在一致性测试套件中捕获错误的 ID 方案,而不是在重放时,并指向 “Upgrading a World to v5” 指南以了解完整规则。numbers events by position -
挂起与分发。 等待的异步返回契约已移除。等待变成了携带
{ timeoutSeconds }的普通队列延续,等待和 step 分发被统一为每次挂起一个并行批次。特殊处理旧等待返回的 World 需要针对当前接口重写。delaySeconds -
Step 队列主题已退役。队列类型不再存在:排队的 step 在 workflow 主题上传输(在负载中携带
'step'/stepId),并在合并的 flow handler 中执行。曾经预置或路由独立stepName主题的 World 可以删除它们。__wkf_step_* -
World 解析发生在构建时。 Worlds 被静态注入 host bundle,而不是在运行时动态选择;第一方 World 包暴露工厂。自定义或社区 World 必须能被构建解析;请验证应用仍然可以启动并依赖它,而不是假设运行时查找。
createWorld()
可选方法可以省略;运行时会回退。请让用户参考 skill 和 “Upgrading a World to v5” 指南,它们包含完整的接口差异和上述契约变更,而不是自行发明方法体。
migrating-world-v4-to-v5Required output shape
要求的输出结构
Return the migration in this structure:
md
undefined按以下结构返回迁移结果:
md
undefinedSummary
Summary
Dependency Changes
Dependency Changes
Code Changes
Code Changes
Behavior Changes To Review
Behavior Changes To Review
Verification
Verification
Open Questions
Open Questions
- `## Code Changes` lists one entry per rule applied, with the file paths touched. Rules that did not apply are listed once as not applicable — do not omit them silently.
- `## Behavior Changes To Review` carries the step 3 items that apply to this app, each with the concrete follow-up (which dashboard, which env var).
- `## Open Questions` carries anything that could not be decided from the code, especially a custom World that needs interface work.
- `## Code Changes` 列出应用到的每条规则的一个条目,并包含涉及的文件路径。不适用的规则应一次性列为“不适用”——不要静默省略。
- `## Behavior Changes To Review` 包含适用于该应用的步骤 3 中的项目,每项都带有具体的后续行动(哪个仪表盘、哪个环境变量)。
- `## Open Questions` 包含无法从代码确定的内容,尤其是需要接口工作的自定义 World。Verification
验证
Run, in order, and report actual output:
- Install and build. The build must regenerate workflow/step bundles without errors.
- Typecheck. The async change and the
getWorld()signature surface here.steps.get - The app's test suite.
- Start the app and execute one real run end to end, confirming the first step runs and the run reaches a terminal state.
Fail the migration if any of these are true:
- and
workflowversions span both 4.x and 5.x@workflow/* - /
getWorld()/createWorld()is called withoutgetWorldHandlers()await - or
runStepis still importedstepEntrypoint - or
createLocalWorldis still importedcreateVercelWorld - or
workflow/internal/privateis still imported@workflow/core/private - a call kept the v4 argument order (name before runId)
world.streams.* - was called without a
streams.getrunId - was called with
world.steps.getas its first argumentundefined - is imported from
NestLocalBuilderinstead of@workflow/nestworkflow/nest/builder - a compiler call still passes
mode: 'client' - is read off a
metadataorgetHookByToken()result withoutresumeHook()await - a behavior change from step 3 was silently "fixed" instead of reported
- workflow or step bodies were restructured beyond the rules above
- generated output under was hand-edited
.well-known/workflow/v1/ - the build, typecheck, or test results were not actually run and reported
按顺序运行并报告实际输出:
- 安装并构建。构建必须无错误地重新生成 workflow/step bundles。
- 类型检查。异步 变更和
getWorld()签名会在这里暴露。steps.get - 应用的测试套件。
- 启动应用并端到端执行一次真实 run,确认第一个 step 运行且 run 达到终态。
如果以下任一情况为真,则迁移失败:
- 和
workflow版本同时包含 4.x 和 5.x@workflow/* - /
getWorld()/createWorld()被调用时没有getWorldHandlers()await - 仍导入了 或
runStepstepEntrypoint - 仍导入了 或
createLocalWorldcreateVercelWorld - 仍导入了 或
workflow/internal/private@workflow/core/private - 调用保留了 v4 参数顺序(name 在 runId 之前)
world.streams.* - 被调用时没有传
streams.getrunId - 被调用时第一个参数是
world.steps.getundefined - 是从
NestLocalBuilder而不是@workflow/nest导入的workflow/nest/builder - 编译器调用仍传入了
mode: 'client' - 在 或
getHookByToken()的结果上读取resumeHook()时没有metadataawait - 步骤 3 中的行为变更被静默“修复”而不是报告
- workflow 或 step 函数体被重构,超出了上述规则
- 下的生成输出被手动编辑
.well-known/workflow/v1/ - 构建、类型检查或测试结果没有实际运行和报告
Reference
参考
- What's new in v5: https://workflow.dev/docs/whats-new
- Configuration and runtime tuning: https://workflow.dev/docs/configuration
- Upgrading a World to v5: https://workflow.dev/worlds/upgrading-to-v5
- Building a World: https://workflow.dev/worlds/building-a-world
- v4 documentation: https://workflow.dev/v4/docs
- v5 新特性:https://workflow.dev/docs/whats-new
- 配置与运行时调优:https://workflow.dev/docs/configuration
- 将 World 升级到 v5:https://workflow.dev/worlds/upgrading-to-v5
- 构建 World:https://workflow.dev/worlds/building-a-world
- v4 文档:https://workflow.dev/v4/docs