惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Security @ Cisco Blogs
H
Hacker News: Front Page
P
Privacy International News Feed
N
News and Events Feed by Topic
T
Threatpost
Simon Willison's Weblog
Simon Willison's Weblog
S
Schneier on Security
K
Kaspersky official blog
S
Secure Thoughts
V2EX - 技术
V2EX - 技术
Security Latest
Security Latest
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
www.infosecurity-magazine.com
www.infosecurity-magazine.com
C
CERT Recently Published Vulnerability Notes
L
Lohrmann on Cybersecurity
Jina AI
Jina AI
P
Proofpoint News Feed
AI
AI
雷峰网
雷峰网
T
Tailwind CSS Blog
Engineering at Meta
Engineering at Meta
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
博客园 - 叶小钗
Webroot Blog
Webroot Blog
Apple Machine Learning Research
Apple Machine Learning Research
SecWiki News
SecWiki News
罗磊的独立博客
N
Netflix TechBlog - Medium
Martin Fowler
Martin Fowler
Google DeepMind News
Google DeepMind News
Cyberwarzone
Cyberwarzone
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
Schneier on Security
Schneier on Security
The GitHub Blog
The GitHub Blog
S
Security Affairs
Blog — PlanetScale
Blog — PlanetScale
Last Week in AI
Last Week in AI
P
Proofpoint News Feed
月光博客
月光博客
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Securelist
W
WeLiveSecurity
T
Troy Hunt's Blog
A
Arctic Wolf
博客园 - 司徒正美

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors node-js-module-in-workflow serialization-failed start-invalid-workflow-function step-not-registered timeout-in-workflow webhook-invalid-respond-with-value webhook-response-not-sent workflow-not-registered Errors & Retrying Hooks & Webhooks Idempotency Foundations Serialization Starting Workflows Streaming Versioning Workflows and Steps How the Directives Work Encryption Event Sourcing Framework Integrations Understanding Directives Migration Guides Migrating from AWS Step Functions Migrating from Inngest Migrating from Temporal Observability Testing Server-Based Testing createHook createWebhook defineHook FatalError fetch getStepMetadata getWorkflowMetadata getWritable workflow RetryableError sleep @workflow/vitest DurableAgent @workflow/ai WorkflowChatTransport getHookByToken getRun getWorld workflow/api resumeHook resumeWebhook Chat Session Modeling runtime-decryption-failed abort-signal-timeout-in-workflow Cancellation How Cancellation Works Internal Serializable AbortController and AbortSignal Eager Processing of Steps & Incremental Event Replay TanStack Start Agent Cancellation Sequential & Parallel Execution Workflow Composition Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK Migrating from trigger.dev Secure Credential Handling Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK
Upgrading Workflows
2026-05-22 · via Workflow SDK Documentation

Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward.

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() 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.

For the underlying model — why runs pin to a deployment by default, how cancel-and-rerun works, and how state crosses the version boundary — see Versioning. This recipe focuses on event-driven workflows that need to keep advancing across deployments.

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). 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). 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.

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.

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"; 

  // `deploymentId: "latest"` resolves to whichever deployment is current
  // when this spawn lands — NOT the deployment running this code.
  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();

  // 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 }); 

  await processItem(itemId);

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

Resuming the hook

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

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

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

  await nextItemHook.resume(runId, { itemId }); 

  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.

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.

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 }>(); 

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([ 
      nextItemHook
        .create({ token: workflowRunId })
        .then((payload) => ({ kind: "work" as const, payload })),
      upgradeHook 
        .create({ token: workflowRunId }) 
        .then(() => ({ kind: "upgrade" as const })), 
    ]);

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

    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.

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 }); 

  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.

  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() 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.
  • 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.
  • 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.
  • 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.