migrating-workflow-v4-to-v5 について
Workflow SDK 4.xから5.0への移行を行うアプリケーションチーム向けのガイドです。破壊的変更と対象箇所ごとの修正方法を示し、ワークフローやステップ本体の変更は原則不要としています。
主な機能
- 01依存関係の更新手順
- 02ランタイムAPIの移行ルール
- 03Worldインターフェースの変更説明
- 04ストリームとイベントAPIの修正
- 05フレームワーク連携のインポート更新
活用シーン
- 01アプリの4.xから5.0への移行評価
- 02削除されたランタイムエクスポートの特定
- 03カスタムWorldとストリーム呼び出しの更新
- 04フレームワークやコンパイラー連携の変更確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vercel/ workflow --skill migrating-workflow-v4-to-v5インストール後、Claude Code で/migrating-workflow-v4-to-v5を入力して読み込まれたか確認します。
詳細
Workflow SDK 4.x から 5.0 への移行
Workflow SDK 5.0 でも、4.x のプログラミングモデルは維持されています。"use workflow" / "use step"、start()、getRun()、フック、Webhook、ストリーム、sleep()、リトライ、イベントログに変更はないため、ほとんどのアプリケーションコードはそのままコンパイルできます。
破壊的変更は、主に次の 3 か所に集中しています。
- ランタイムのエントリーポイント(
workflow/api、workflow/runtime)— 2 つのエクスポートが削除されました。 Worldインターフェース — アプリが独自の World を実装する場合、またはgetWorld()を直接呼び出す場合にのみ関係します。- ビルド連携 —
@workflow/nestのサブパスと、公開されたことのない非公開のコンパイラーサブパス。
ワークフローやステップの本体を書き換える必要はありません。ビジネスロジックの構成を変更している場合、この移行の範囲を超えています。
事前確認
編集する前に、次の項目を確認します。
- インストールされているパッケージ。
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、@workflow/astro、または CLI を確認します。 hook.getConflict()が使われているかどうか。getConflictを検索します。- ワークフロー外でフックのメタデータが読み取られているかどうか。
getHookByTokenとresumeHookを検索し、続いてそれらの結果に対する.metadataを検索します。 - アプリがコンパイラーを直接呼び出しているかどうか。
mode: 'client'、transformSync、swc-plugin-workflowを検索します。これらを使うのは、独自のビルド連携のみです。 experimental_setAttributesが使われているかどうか。experimental_setAttributesを検索します。
2〜8 のうち、アプリで使用されていない項目は、黙って省略せず「該当なし」として報告します。
ステップ 1 — 依存関係を更新する
workflow とすべての @workflow/* 依存関係を ^5.0.0 に更新します。これらは同時にリリースされ、メジャーバージョンを混在させてはいけません。4.x の @workflow/next と 5.x の workflow を組み合わせると、ビルド時に失敗します。
{
"dependencies": {
"workflow": "^5.0.0",
"@workflow/next": "^5.0.0"
}
}
その後、再インストールして再ビルドし、コンパイラーがワークフロー/ステップのバンドルと .well-known/workflow/v1/ 配下の生成済みルートを再生成するようにします。生成された出力を手作業で編集してはいけません。
Node の要件に変更はありません。^18 || ^20 || ^22 || ^24。
ステップ 2 — 機械的な書き換えを適用する
実際にパターンが存在する箇所にのみ、各ルールを適用します。
getWorld() と createWorld() は非同期
// v4
const world = getWorld();
// v5
const world = await getWorld();
これは getWorldHandlers() にも当てはまります。4.x でも await を使うのが正しかったため、この変更は依存関係の更新前に安全に行えます。.then() のチェーンで包むのではなく、呼び出し元へ async を伝播させます。
createLocalWorld() と createVercelWorld() の削除
ファーストパーティの World パッケージでは、単一の createWorld() ファクトリーが公開されるようになりました。引数に変更はなく、名前の変更のみです。
// v4
import { createLocalWorld } from '@workflow/world-local';
const world = createLocalWorld({ dataDir });
// v5
import { createWorld } from '@workflow/world-local';
const world = createWorld({ dataDir });
@workflow/world-vercel の createVercelWorld にも同じことが当てはまります。
workflow/api から runStep を削除
ステップ関数を直接呼び出します。コンパイラーがその呼び出しをステップランタイム経由で処理します。
// v4
import { runStep } from 'workflow/api';
const result = await runStep(chargeCard, [orderId]);
// v5
const result = await chargeCard(orderId);
workflow/runtime から stepEntrypoint を削除
フレームワーク連携がステップルートを自動生成します。stepEntrypoint を呼び出すだけの手書きステップルートがある場合は削除します。独自のホストでは、代わりに getWorldHandlers() からハンドラーを提供します。
workflow/internal/private と @workflow/core/private を削除
これらのサブパスは公開 API ではありませんでした。インポートを削除してください。生成済みのビルド出力が引き続き参照している場合は古い出力です。インポートを復元するのではなく、再インストールして再ビルドしてください。
ストリームメソッドは runId を先頭にして world.streams.* に移動しました
| v4 | v5 |
|---|---|
world.writeToStream(name, runId, chunk) | world.streams.write(runId, name, chunk) |
world.writeToStreamMulti(name, runId, chunks) | world.streams.writeMulti(runId, name, chunks) |
world.closeStream(name, runId) | world.streams.close(runId, name) |
world.readFromStream(name, startIndex?) | world.streams.get(runId, name, startIndex?) |
world.getStreamChunks(name, runId, options?) | world.streams.getChunks(runId, name, options?) |
world.listStreamsByRunId(runId) | world.streams.list(runId) |
引数の順序が入れ替わったため、名前だけを変更すると、ストリーム名が実行 ID として誤って渡されます。すべての呼び出し箇所で引数を入れ替えてください。readFromStream には runId 引数がありませんでしたが、streams.get には必須です。呼び出し箇所まで、所有元の実行 ID を渡してください。
ワークフロー内で getWritable() を使うアプリケーションコードや、run.readable を読み取るコードには影響しません。このルールは World に直接アクセスする場合に限られます。
world.steps.get() には runId が必要です
最初のパラメーターは string | undefined でしたが、現在は string です。そのステップを所有する実行 ID を渡してください。
// v4
const step = await world.steps.get(undefined, stepId);
// v5
const step = await world.steps.get(runId, stepId);
events.listByCorrelationId() には runId が必要です
相関 ID は、複数の実行にまたがるものではなく、実行内のステップ、フック、または待機を識別します。検索範囲は単一の実行に限定されるため、その ID を所有する実行を渡してください。
analytics.events.listByCorrelationId() も同じ runId を受け取りますが、さらに非推奨になっています。runId が必須になったことで、これは analytics.events.list({ runId, correlationId }) の特殊なケースとなり、次のメジャーバージョンで削除されます。ひとまず runId を追加し、同じファイルを編集中に呼び出しを list に移してください。ストレージの events.listByCorrelationId() は非推奨ではありません。
// v4
const events = await world.events.listByCorrelationId({ correlationId });
// v5
const events = await world.events.listByCorrelationId({ runId, correlationId });
hook.getConflict() は Run を返します
解決される値は { runId } ではなく、競合する実行ハンドルになりました。conflict.runId は引き続き同じ値を参照するため、既存のコードはそのまま動作します。ただし、ステップを経由して実行を取得する処理は削除できます。
// 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.status と await conflict.cancel() を利用できます。トークンが正常に取得された場合、getConflict() は引き続き null を返すため、if (conflict) の null チェックは削除しないでください。
hook.metadata は Promise です
getHookByToken() と resumeHook() が返すフックでは、metadata はメモ化された Promise を返すゲッターになりました。これは run.returnValue と同じ形式です。トークンによるフックの検索は 1 回の読み取りで済みます。メタデータの取得にはネットワーク往復が追加で発生する場合がありますが、それを待機するコードだけがそのコストを負担します。
// 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 });
}
メタデータが保存されていないフックで待機すると、I/O を行わずに undefined に解決されるため、await にガードは不要です。{ ...hook } と JSON.stringify(hook) には metadata が含まれなくなったため、フックオブジェクト全体を転送していたハンドラーは、待機後の値を明示的に転送する必要があります。hook.runId、hook.token、その他のフィールドに変更はなく、world.hooks.getByToken() は引き続き生の Hook レコードを返します。TypeScript では古い形式だとビルドに失敗します。通常の JavaScript では hook.metadata.allowedUserId が Promise から undefined を読み取るため、上記のようなチェックはすべてのリクエストを何も通知せず拒否します。
experimental_setAttributes は setAttributes に名前が変更されました
// v4
import { experimental_setAttributes } from 'workflow';
// v5
import { setAttributes } from 'workflow';
古い名前は v5 で削除されたため、この書き換えが必要です。ExperimentalSetAttributesOptions も SetAttributesOptions に変更されました。start() の attributes オプションに変更はありません。
SWC 変換から mode: 'client' が削除されました
コンパイラーを直接呼び出すカスタムビルド連携にのみ関係します。client モードは step に統合されました。step は、巻き上げられた変数参照の処理とデッドコード除去を行います。mode: 'step' を渡してください。
NestLocalBuilder が @workflow/nest のルートから移動しました
// v4
import { NestLocalBuilder } from '@workflow/nest';
// v5
import { NestLocalBuilder } from 'workflow/nest/builder';
NestVercelBuilder は workflow/nest/vercel-builder にあります。WorkflowModule は引き続き @workflow/nest からインポートします。この分割により、ビルドツールチェーンがランタイムバンドルに含まれないようにしています。そのため、ランタイムコードがインポートするモジュールからビルダーを再エクスポートしないでください。
ステップ 3 — 判断が必要な動作変更を確認する
これらはコードの編集ではありません。該当するものをそれぞれ報告し、黙って「修正」しないでください。
- トレースのデフォルトが
linkedになりました。 各ワークフローとステップの呼び出しは、それぞれ独立したトレースルートとなり、エンキュー箇所と実行元にスパンリンクを持ちます。実行ごとに 1 つのトレースを作成する方式ではありません。アプリに、実行ごとに単一のトレース ID をキーとするダッシュボード、保存済みクエリ、アラートがある場合は、workflow.run.id属性を使うよう変更するか、WORKFLOW_TRACE_MODE=continuousを設定して 4.x の形式に戻してください。 - イベント作成時の事前条件ガードがなくなりました。
WORKFLOW_PRECONDITION_GUARDは存在しなくなったため、設定されている場合はアプリの環境設定とデプロイ設定から削除してください。SDK のどの World も、古いスナップショットに対する書き込みを拒否しなくなりました。リプレイが遅れている場合、次に行う書き込みから未反映分を把握します。 - Turbo モードがデフォルトで有効になりました。 実行の初回呼び出しでは
run_startedがバックグラウンドで処理され、最初のイベントログの読み込みが省略されます。WORKFLOW_TURBO=0で無効にできます。 - エラーの型が保持されます。
WorkflowRunFailedError.causeでは、元のクラス ID と原因チェーンが保持されるようになりました。4.x ではクラスがフラット化されていたため、error.messageに基づいてパターンマッチングしていたコードはinstanceofを使用できます。ただし、エラー処理を頼まれずに書き換えるのではなく、該当箇所を報告してください。 - イベント ID は ULID ではなくスロット番号です。 イベント ID は、実行のログにおける 1 から始まる位置(
evnt_00000000000000000000000042)となり、実行内でのみ一意で、タイムスタンプは含まれません。イベント ID をグローバルキーとして使うアプリコード(runIdと組み合わせてください)や、ID から時刻をデコードするコード(イベントのcreatedAtを読み取ってください)があれば報告してください。その他のエンティティ ID に変更はありません。 - 実行ごとのイベント上限が適用されます。 World が上限を指定します(Local と Vercel Worlds では 25,000 イベント)。上限に達した実行は
MAX_EVENTS_EXCEEDEDで失敗します。無限ループを含むワークフローがあれば報告してください。修正にはバッチごとの子実行が必要です。これは設計変更であり、移行時の編集ではありません。 - ストリームの書き込みでは先頭チャンクが即時にフラッシュされます。 デフォルトのフラッシュ間隔は 10ms から 0 になりました。小さなチャンクが短時間に連続した場合に、間隔を利用してまとめていたアプリでは、
streamFlushIntervalMsまたはWORKFLOW_STREAM_FLUSH_INTERVAL_MSを設定できます。 - 生成されるステップ、ワークフロー、Webhook のバンドルは ESM です(VM で実行されるワークフローバンドルは引き続き CJS です)。ビルド出力を後処理するホストでのみ影響があります。
- ステップ ID またはワークフロー ID の重複はビルドエラーになりました。 4.x では、ワークスペース内の異なるファイルにある、同じ名前のエクスポートされていない関数が衝突すると、後に書かれたものが優先されていました。これが原因でビルドが失敗する場合は、どちらかの名前を変更してください。チェックを抑制しないでください。
- アップグレード前に書き込まれた
world-postgresの行について。 4.x で保存された失敗した実行を読み込むと、errorJsonではなく旧形式のerrorテキスト列にペイロードが格納されているため、error: undefinedになります。データ移行はありません。アップグレード前の失敗について、最近の履歴を表示するダッシュボードでエラーが空欄になることがあります。 world-localのストリームチャンクの保存先がstreams/chunks/<streamName>/に変更されました。 旧形式のフラットなレイアウトのファイルは読み込まれず、古いファイルもそのまま残ります。ローカル開発の状態なので、データディレクトリを削除しても問題ありません。- ワークフローのサンドボックスで非決定的な処理がより厳しく制限されます。
WeakRef、FinalizationRegistry、Atomics.waitAsync、非同期WebAssemblyコンパイルはワークフロー関数内で使えなくなり、crypto.subtle.digestは同期的に計算するようになりました(結果は同じで、実行タイミングは決定的です)。これらの API を使用していないか、"use workflow"ファイルを grep してください。修正には該当コードをステップに移す必要があります。これは設計変更なので、頼まれずに構成を変更せず、該当箇所を報告してください。 - 実行中の実行は移行されません。 4.x のデプロイで作成された実行は、そのデプロイ上で実行を続けます。開始したデプロイ上で完了するまで待ち、「ドレイン」や再割り当てのコードを追加しないでください。
ステップ 4 — カスタム World 実装
アプリが World 自体を実装している場合に限ります。この部分には migrating-world-v4-to-v5 スキルを使用してください(npx skills add https://github.com/vercel/workflow --skill migrating-world-v4-to-v5)。このスキルには、以下に要約するイベント ID の割り当て作業を含む、World の完全な移行手順が含まれています。以下は、作業範囲を把握し、それがまだ行われていないことを判断するのに十分な内容です。
上記のストリームとステップのシグネチャに加えて、インターフェースには以下が追加されました。
- 名前空間としての
streams(ステップ 2 の表を参照)。 analytics— 実行、ステップ、イベント、フック、待機、属性のメタデータのみの一覧。- 実行に対する属性のサポート。
experimentalSetAttributesを含みます。 - ケイパビリティの通知。これによりランタイムが最適化を制御できます。通知されていないケイパビリティはフェイルクローズドとなり、つまり不完全な World は正しいままですが遅くなります。ケイパビリティは、それが本当に実装されてから初めて通知してください。イベント ID の割り当てはこれらには含まれません。これは要件であり、ケイパビリティではありません。旧来の
preconditionGuardフラグの背後で古い書き込みを拒否していた World は、そのコードを削除できます。フラグは削除されており、それを置き換えるものは何もないからです。 - 実行の読み取り時に返される、省略可能な実行ごとのイベント上限。これはランタイムが強制します。
- 省略可能な
createRunId()と、キューオプションのregion。実行状態をリージョン単位で配置する World 向けです。
4 つのコントラクト変更が既存の実装に影響します。最初のものは必須であり、型シグネチャからは見えません。
-
イベント ID はスロットであり、その割り当ては必須です。 イベント ID はもはや World が自由に生成する ULID ではありません。それは
evnt_に続けて、その実行のログにおけるイベントの 1 始まりの位置を 26 文字にゼロ埋めしたものです(@workflow/worldのslotToEventId()がこれをフォーマットします)。位置は一意で、穴のない密なものでなければならないため、競合はプロセス内で最大値を読んで 1 を足すのではなく、ストア側で解決する必要があります。すなわち(runId, eventId)の一意制約か条件付き書き込みです。events.create()のパラメータには書き込み位置を指定するeventCountが含まれます。その位置が取られている場合、World は create を拒否してはならず、次の空き位置に進み、そこでコミットし、飛ばした位置を占めるイベントを成功レスポンスで返します。位置はイベントを追加するのと同じ操作で、コミット時に取得してください。それが、リーダーがすでに通過した位置の後ろにイベントが着地するのを防ぐからです。宣言できるケイパビリティはなく、フォールバックもありません。ランタイムは読み込むすべての ID から位置を読み取り、読み取れない場合は実行を失敗させます。したがって、ID が位置になっていない World は型チェックを通り、実行を開始し、最初のリプレイでEvent id is not slot-numberedにより失敗します。ユーザーには、すでにストアにある実行は新しいコードでリプレイできないことを警告してください。ULID で番号付けされた実行は位置として読み取れず、ランタイムはそれを拒否するため、それらの実行はまず 4.x で排出する必要があります。ただし、プラットフォームが実行を作成したデプロイメントに固定する場合(Vercel はそうしており、これで解決します)を除きます。リプレイ時ではなく適合性スイートで誤った ID スキームを検出する
@workflow/world-testingのnumbers events by positionと、完全なルールについては「Upgrading a World to v5」ガイドを案内してください。 -
一時停止とディスパッチ。 待機時の非対称な
{ timeoutSeconds }戻り値の契約は廃止されました。待機はdelaySecondsを伴う通常のキュー継続処理となり、待機とステップのディスパッチは、一時停止ごとに単一の並列バッチへ統合されました。旧来の待機戻り値を特別扱いしていた World は、現行インターフェースに合わせて書き直す必要があります。 -
ステップキューのトピックは廃止されました。
'step'キューの種類はなくなりました。キューに登録されたステップはワークフロートピックを通じて送られ(ペイロードにstepId/stepNameを含みます)、統合されたフローハンドラーで実行されます。個別の__wkf_step_*トピックをプロビジョニングまたはルーティングしていた World では、それらを削除できます。 -
World の解決はビルド時に行われます。 World は実行時に動的に選択されるのではなく、ホストバンドルに静的に注入されます。また、ファーストパーティの World パッケージは
createWorld()ファクトリーを公開します。カスタムまたはコミュニティの World はビルド時に解決できる必要があります。実行時の検索を前提にせず、その World に対してアプリが引き続き起動することを確認してください。
省略可能なメソッドは省略できます。実行時にフォールバックされます。メソッド本体を独自に作成するのではなく、完全なインターフェース差分と上記の契約変更を記載した migrating-world-v4-to-v5 スキルと「World を v5 にアップグレードする」ガイドを案内してください。
必須の出力形式
次の構成で移行内容を返してください。
## Summary
## Dependency Changes
## Code Changes
## Behavior Changes To Review
## Verification
## Open Questions
## Code Changesには、適用したルールごとに1項目を記載し、変更したファイルパスを含めます。該当しなかったルールは、黙って省略せず、該当なしとして1回記載します。## Behavior Changes To Reviewには、このアプリに該当するステップ3の項目を記載し、それぞれ具体的なフォローアップ(どのダッシュボード、どの環境変数か)を示します。## Open Questionsには、コードから判断できなかった事項、特にインターフェース作業が必要なカスタム World を記載します。
検証
次の順に実行し、実際の出力を報告してください。
- インストールとビルド。ビルドによってワークフロー/ステップバンドルがエラーなく再生成される必要があります。
- 型チェック。非同期の
getWorld()への変更とsteps.getのシグネチャを確認します。 - アプリのテストスイート。
- アプリを起動し、実際の実行を1回エンドツーエンドで行って、最初のステップが実行され、実行が終端状態に達することを確認します。
次のいずれかに該当する場合、移行は失敗です。
-
workflowと@workflow/*のバージョンに 4.x と 5.x が混在している -
getWorld()/createWorld()/getWorldHandlers()がawaitなしで呼び出されている -
runStepまたはstepEntrypointが引き続きインポートされている -
createLocalWorldまたはcreateVercelWorldが引き続きインポートされている -
workflow/internal/privateまたは@workflow/core/privateが引き続きインポートされている -
world.streams.*の呼び出しで、v4 の引数順(name が runId より前)が使われている -
streams.getがrunIdなしで呼び出されている -
world.steps.getの第1引数にundefinedが渡されている -
NestLocalBuilderがworkflow/nest/builderではなく@workflow/nestからインポートされている - コンパイラーの呼び出しに
mode: 'client'が渡されている -
getHookByToken()またはresumeHook()の結果から、awaitなしでmetadataが読み取られている - ステップ3の動作変更が、報告されずに「修正」されている
- ワークフローまたはステップの本体が、上記のルールを超えて再構成されている
-
.well-known/workflow/v1/配下の生成済み出力が手動編集されている - ビルド、型チェック、またはテストの結果が実際に実行され、報告されていない
参考資料
- 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