Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
ad8b7f1
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 27, 2026
e0cb61b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 28, 2026
051cadc
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 29, 2026
af8ac1f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 29, 2026
e68e7d2
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 30, 2026
8305e5b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 30, 2026
f3da688
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Jan 31, 2026
f8ee413
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 1, 2026
589cbd7
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 2, 2026
e979fdf
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 3, 2026
d4baed2
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 6, 2026
53503a4
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 10, 2026
d51e2e4
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
5123088
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
dd1a307
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 13, 2026
00bcfcd
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 14, 2026
f6a157b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 15, 2026
816f35b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 16, 2026
2b87dce
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 17, 2026
146de28
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
0ef6455
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
2d8c690
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 18, 2026
3513471
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 20, 2026
0f5d29e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 20, 2026
c76001e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 23, 2026
f97184f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 24, 2026
f71c531
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 25, 2026
530e598
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 25, 2026
ea38511
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Feb 27, 2026
19b1a3f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 2, 2026
757bfdb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 2, 2026
5fb4cfb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
cb5adc3
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
046fdbc
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
2a7884f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 3, 2026
60ecae6
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 5, 2026
71a6e95
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 6, 2026
25fb139
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 9, 2026
8fe6a36
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 9, 2026
d7cb1a3
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 10, 2026
99b34fe
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 10, 2026
e63bfca
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
810af84
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
67142f9
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 11, 2026
58b47e7
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 12, 2026
4dfac66
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 12, 2026
a423b68
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
b8f1f2e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
30fe808
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
972c20b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
0c882fb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 13, 2026
e8d3775
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 16, 2026
29a316d
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 16, 2026
70b3e67
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 17, 2026
d97b801
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 17, 2026
e50f179
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 18, 2026
f6f1a4e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 18, 2026
6bc491f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 18, 2026
bacb19e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 18, 2026
6a97701
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 20, 2026
d64bb0e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 20, 2026
04ef7fd
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 22, 2026
9748e93
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 24, 2026
27a5ca6
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 24, 2026
c0fcfbd
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 26, 2026
3c9b9f5
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 26, 2026
b317a73
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 28, 2026
119d147
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Mar 30, 2026
c77eede
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 1, 2026
8a8732e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 3, 2026
86b1861
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 5, 2026
47af9ca
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 7, 2026
b0f125e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 10, 2026
f1dbb56
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 11, 2026
deeab3e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 12, 2026
ea40643
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 13, 2026
e780833
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 13, 2026
a26e834
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 14, 2026
6ebb540
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 14, 2026
dc18bf6
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 14, 2026
70e6f37
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 14, 2026
edb2946
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 15, 2026
d5b3511
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 15, 2026
1497def
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 16, 2026
429d7fa
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 16, 2026
5ea7996
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 17, 2026
8d3b46f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 22, 2026
b28953f
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 22, 2026
96d2a1e
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 22, 2026
59427f5
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 23, 2026
573f38b
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 28, 2026
d41a682
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 28, 2026
c6909cb
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 29, 2026
2556fb9
Merge branch 'main' of github.com:vercel/workflow
karthikscale3 Apr 29, 2026
34a3ce5
docs(cookbook): add Upgrading Workflows guide
karthikscale3 Apr 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/docs/cookbook/advanced/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"pages": [
"child-workflows",
"distributed-abort-controller",
"upgrading-workflows",
"serializable-steps",
"publishing-libraries"
]
Expand Down
207 changes: 207 additions & 0 deletions docs/content/docs/cookbook/advanced/upgrading-workflows.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
title: Upgrading Workflows
description: Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward.
type: guide
summary: 'Identify a clean upgrade point and hand off to a fresh run via `start(self, [state], { deploymentId: "latest" })` — either automatically on every iteration, or on demand via a dedicated upgrade hook.'
related:
- /cookbook/common-patterns/workflow-composition
- /docs/api-reference/workflow-api/start
- /docs/foundations/hooks
---

Workflows that block on external events for days, weeks, or months can outlive many deployments. **The key is to identify a clean upgrade point in the workflow** — a moment where it's safe to checkpoint state and start fresh — and then call [`start()`](/docs/api-reference/workflow-api/start) with `deploymentId: "latest"` to spawn a new run carrying that state forward. The current run ends; the next run begins on whatever deployment is live at that moment, so shipped fixes apply immediately without ever migrating an in-flight run.

A clean upgrade point is any spot in the workflow where:

- All in-progress side effects have completed (or aren't needed by the next iteration)
- The relevant state can be serialized into the workflow's input arguments
- It's natural for the workflow to "checkpoint" — typically right after handling an external event, completing a batch, or finishing a logical phase

There are two ways to apply this:

1. **Upgrade on every iteration** ([Method 1](#method-1-upgrade-on-every-iteration)). Each run handles a single event and unconditionally hands off to a fresh run on the latest deployment before exiting. Simple — no extra triggers — but every event pays the respawn cost.
2. **Upgrade on demand via a dedicated hook** ([Method 2](#method-2-upgrade-on-demand-via-a-dedicated-hook)). A single long-lived run handles many events in a loop and only respawns when an `upgradeHook` fires. A separate endpoint resumes that hook from your control plane (e.g. after a deploy). More control and fewer respawns, at the cost of an explicit trigger.

### When to use each

- **Method 1** when iterations are short and frequent, the work is cheap to checkpoint, and you want shipped fixes to apply on the very next event. Long-lived "session" workflows (subscriptions, queues, FSMs) that already process events one at a time fit this naturally.
- **Method 2** when iterations are infrequent or expensive (you don't want to respawn on every event), or when you need to roll out a fix to a fleet of in-flight runs after a deploy by fanning out to a control-plane endpoint. Also fits when "upgrade" should be an explicit operation rather than a side effect of handling each event.

## Method 1: Upgrade on every iteration

Each run inherits state via its argument, blocks on a hook, processes the resume, then unconditionally hands off to its successor. The `start()` call is wrapped in a `"use step"` function (required) and passes `deploymentId: "latest"` so the new run lands on the freshest code.

```typescript lineNumbers
import { defineHook, getWorkflowMetadata } from "workflow";
import { start } from "workflow/api";

declare function processItem(itemId: string): Promise<void>; // @setup

interface QueueState {
processed: number;
cursor: string | null;
}

export const nextItemHook = defineHook<{ itemId: string }>();

async function spawnSelfOnLatest(state: QueueState): Promise<string> {
"use step"; // [!code highlight]

// `deploymentId: "latest"` resolves to whichever deployment is current
// when this spawn lands — NOT the deployment running this code.
const next = await start(longRunningQueue, [state], { // [!code highlight]
deploymentId: "latest", // [!code highlight]
}); // [!code highlight]
return next.runId;
}

export async function longRunningQueue(
state: QueueState = { processed: 0, cursor: null },
): Promise<void> {
"use workflow";

const { workflowRunId } = getWorkflowMetadata();

// Block until something fires the hook — could be hours, days, or longer.
// Per-run hook tokens (workflowRunId) keep concurrent chains isolated.
const { itemId } = await nextItemHook.create({ token: workflowRunId }); // [!code highlight]

await processItem(itemId);

// Hand off to a fresh run on the latest deployment. THIS run ends here.
await spawnSelfOnLatest({ // [!code highlight]
processed: state.processed + 1, // [!code highlight]
cursor: itemId, // [!code highlight]
}); // [!code highlight]
}
```

### Resuming the hook

Any server-side code can resume the currently-active iteration by calling `.resume()` with the run ID:

```typescript
import { nextItemHook } from "@/workflows/long-running-queue";

export async function POST(req: Request) {
const { runId, itemId } = await req.json();

await nextItemHook.resume(runId, { itemId }); // [!code highlight]

return Response.json({ success: true });
}
```

The caller tracks the active `runId` (e.g. in a database, KV, or returned from the previous iteration) and updates it whenever the chain advances.

## Method 2: Upgrade on demand via a dedicated hook

Use a single long-running workflow that handles events in a loop. Define a second hook — `upgradeHook` — alongside the work hook, and race them. While only the work hook fires, the run keeps handling events on its current deployment. When `upgradeHook` resumes, the workflow captures current state and respawns on the latest deployment, then exits.

```typescript lineNumbers
import { defineHook, getWorkflowMetadata } from "workflow";
import { start } from "workflow/api";

declare function processItem(itemId: string): Promise<void>; // @setup

interface QueueState {
processed: number;
cursor: string | null;
}

export const nextItemHook = defineHook<{ itemId: string }>();
export const upgradeHook = defineHook<{ reason?: string }>(); // [!code highlight]

async function spawnSelfOnLatest(state: QueueState): Promise<string> {
"use step";

const next = await start(longRunningQueue, [state], {
deploymentId: "latest",
});
return next.runId;
}

export async function longRunningQueue(
state: QueueState = { processed: 0, cursor: null },
): Promise<void> {
"use workflow";

const { workflowRunId } = getWorkflowMetadata();

while (true) {
// Race a normal work event against the upgrade signal.
const event = await Promise.race([ // [!code highlight]
nextItemHook
.create({ token: workflowRunId })
.then((payload) => ({ kind: "work" as const, payload })),
upgradeHook // [!code highlight]
.create({ token: workflowRunId }) // [!code highlight]
.then(() => ({ kind: "upgrade" as const })), // [!code highlight]
]);

if (event.kind === "upgrade") { // [!code highlight]
// Checkpoint current state and hand off to a fresh run
// on whatever deployment is live now. THIS run ends here.
await spawnSelfOnLatest(state); // [!code highlight]
return; // [!code highlight]
}

await processItem(event.payload.itemId);
state = {
processed: state.processed + 1,
cursor: event.payload.itemId,
};
}
}
```

### Triggering the upgrade

Expose a separate endpoint that resumes `upgradeHook` for a given run. Call it from your deploy pipeline, an admin UI, or a fan-out script that iterates over every active run after shipping a fix.

```typescript
import { upgradeHook } from "@/workflows/long-running-queue";

export async function POST(req: Request) {
const { runId, reason } = await req.json();

// The workflow exits its loop, captures state, and respawns
// on the latest deployment.
await upgradeHook.resume(runId, { reason }); // [!code highlight]

return Response.json({ success: true });
}
```

To upgrade a fleet of runs after a deploy, list active runs (e.g. from a tracking store) and call this endpoint for each.

## How it works

1. **`deploymentId: "latest"` is the upgrade knob.** Without it, the spawn pins to the current deployment. With it, the new run resolves to whatever deployment is current when the runtime picks it up — so any shipped fix applies starting from that respawn. Both methods rely on this.
2. **`start()` from a step.** [`start()`](/docs/api-reference/workflow-api/start) is not allowed directly inside `"use workflow"` functions — wrap it in a `"use step"` helper to keep the spawn deterministic across replays.
3. **State carries through the function argument.** The accumulating context flows from run N to run N+1 as a serialized argument. No external store is required for the state itself.
4. **Per-run hook tokens.** Using `workflowRunId` as the hook token scopes each iteration's wait to its own run, so multiple chains can run concurrently without interfering.
5. **Method 1 vs Method 2 is just where the spawn happens.** In Method 1 every run spawns its successor unconditionally before exiting — there is no long-lived process to migrate. In Method 2 the spawn happens only when the upgrade hook fires; otherwise the loop keeps handling events on the same run.

## Adapting to your use case

- **Combine with a sleep.** Race the hook against `sleep()` so iterations also tick on a timer: `Promise.race([hook, sleep("1d")])` lets the workflow advance even if no external event arrives.
- **Stateless successors.** If the next iteration doesn't need the previous state (e.g. a pure event router), call `start(longRunningQueue, [], { deploymentId: "latest" })` and skip the argument plumbing.
- **Persist state externally.** If state needs to be readable from outside the workflow (dashboards, debugging, recovery), write it to a database in a step before spawning the next run.
- **Track the active runId externally.** Whatever resumes the hook needs to know the current run. Have the spawn step write the new `runId` to a KV/database keyed by a stable session identifier so resumers always look up the latest one.

## Caveats

- **Backward compatibility matters.** Because the next run executes on a different deployment, the workflow's input arguments and return type must remain compatible across deployments. Adding required fields, removing fields, or changing types can cause serialization failures. See the [`deploymentId: "latest"` callout](/docs/api-reference/workflow-api/start#using-deploymentid-latest).
- **Workflow identity is the function name + file path.** Renaming the function or moving the file across a deployment changes the workflow ID — the next iteration will fail to resolve. Treat the workflow's name and location as stable interfaces.
- **There is a tiny gap between iterations.** The current run ends as soon as `start()` returns; the next run starts asynchronously. A resume that arrives in that window can fail with "hook not found." Make resumers retry, or have the API persist pending payloads and apply them once the next iteration is ready.
- **Method 2: track active runs externally.** Because Method 2's runs are long-lived, the set of in-flight runs only changes when one starts, completes, or upgrades. Persist run IDs (and clean them up on completion or upgrade) so a rollout script can fan out reliably. After resuming `upgradeHook`, also update the tracked run ID once the new run reports back, the same way you would in Method 1.
- **`start()` must be called from a step**, never directly from the workflow body.

## Key APIs

- [`"use workflow"`](/docs/foundations/workflows-and-steps) — marks the orchestrator function
- [`"use step"`](/docs/foundations/workflows-and-steps) — required wrapper for `start()` calls
- [`start()`](/docs/api-reference/workflow-api/start) with [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) — spawn the successor on the newest deployment
- [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspend the workflow until an external event resumes it
- [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) — exposes `workflowRunId` for per-run hook tokens
1 change: 1 addition & 0 deletions docs/content/docs/cookbook/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ A curated collection of workflow patterns with clean, copy-paste code examples f

- [**Child Workflows**](/cookbook/advanced/child-workflows) — Spawn and orchestrate child workflows from a parent
- [**Distributed Abort Controller**](/cookbook/advanced/distributed-abort-controller) — Build a cross-process abort controller using workflow streams and hooks
- [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows) — Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward
- [**Serializable Steps**](/cookbook/advanced/serializable-steps) — Wrap non-serializable third-party objects so they cross the workflow boundary
- [**Publishing Libraries**](/cookbook/advanced/publishing-libraries) — Ship npm packages that export reusable workflow functions
8 changes: 8 additions & 0 deletions docs/lib/cookbook-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const slugToCategory: Record<string, string> = {
// Advanced
'child-workflows': 'advanced',
'distributed-abort-controller': 'advanced',
'upgrading-workflows': 'advanced',
'serializable-steps': 'advanced',
'publishing-libraries': 'advanced',
};
Expand Down Expand Up @@ -183,6 +184,13 @@ export const recipes: Record<string, Recipe> = {
'Build a cross-process abort controller using workflow streams and hooks to coordinate cancellation by semantic ID.',
category: 'advanced',
},
'upgrading-workflows': {
slug: 'upgrading-workflows',
title: 'Upgrading Workflows',
description:
'Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward.',
category: 'advanced',
},
'serializable-steps': {
slug: 'serializable-steps',
title: 'Serializable Steps',
Expand Down
Loading