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

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
B
Blog RSS Feed
宝玉的分享
宝玉的分享
腾讯CDC
博客园_首页
T
Tailwind CSS Blog
月光博客
月光博客
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
SecWiki News
SecWiki News
美团技术团队
P
Privacy International News Feed
H
Help Net Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
Know Your Adversary
Know Your Adversary
Y
Y Combinator Blog
D
DataBreaches.Net
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
S
Schneier on Security
G
GRAHAM CLULEY
博客园 - 三生石上(FineUI控件)
Cisco Talos Blog
Cisco Talos Blog
小众软件
小众软件
Forbes - Security
Forbes - Security
D
Docker
T
Tenable Blog
S
Secure Thoughts
雷峰网
雷峰网
S
Security @ Cisco Blogs
T
The Exploit Database - CXSecurity.com
The Cloudflare Blog
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志

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 Upgrading Workflows 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 Secure Credential Handling Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK
Migrating from trigger.dev
2026-04-16 · via Workflow SDK Documentation

Move a trigger.dev v3 TypeScript app to the Workflow SDK by replacing task(), schemaTask(), wait.for / wait.forToken, triggerAndWait, and metadata streams with Workflows, Steps, Hooks, and start() / getRun().

Install the Workflow SDK migration skill:

npx skills add https://github.com/vercel/workflow --skill migrating-to-workflow-sdk
  • Streaming is built in via named streams (getWritable() and getWritable({ namespace })). Durable status writes replace metadata.stream() and metadata.set(), and clients read from the end of the stream for current status.
  • Orchestration and infrastructure live in a single deployment. There is no separate trigger.dev cloud or self-hosted worker fleet to operate.
  • TypeScript-first DX: plain async/await control flow, no task() factory, no schemaTask() wrapper for payloads you already type with TypeScript.
  • Agent-first tooling: the npx workflow CLI, @workflow/ai for durable AI agents, and a Claude skill for generating workflows.
  • Retry policy is per step. Throw RetryableError to retry with a delay, or FatalError to stop. There is no central task-level retry config.

trigger.dev v3 defines durable work with task() or schemaTask() from @trigger.dev/sdk (trigger.dev v3), deploys tasks to the trigger.dev cloud or a self-hosted instance, and triggers runs via tasks.trigger(). A separate worker fleet picks up runs, applies retry policies, and routes wait.for, wait.forToken, and metadata.stream calls through the platform.

The Workflow SDK replaces that with "use workflow" functions that orchestrate "use step" functions in plain TypeScript. There is no task registry, separate deploy target, or SDK client. Durable replay, retries, and event history ship with the runtime.

Migration collapses the task abstraction into plain async functions. Business logic stays the same.

trigger.devWorkflow SDKMigration note
task({ id, run })"use workflow" function started with start()No factory or id registry.
schemaTask({ schema, run })Typed function + "use workflow"Validate inputs at the call site.
Inline run body"use step" functionSide effects move into named steps.
logger / metadata.setconsole + getWritable({ namespace: 'status' })Logs flow through the run timeline. Status writes go on a named stream.
wait.for({ seconds | minutes | hours | days }) / wait.until({ date })sleep()Import from workflow.
wait.forToken({ timeout })createHook() + Promise.race with sleep()Hooks carry a typed token.
tasks.trigger() / triggerAndWait()start() and getRun(runId).returnValueWrap both in "use step" functions.
batch.triggerAndWait()Promise.all(runIds.map(collectResult))Fan out via standard concurrency.
AbortTaskRunErrorFatalErrorStops retries immediately.
retry.onThrow / retry.fetchRetryableError, FatalError, maxRetriesRetry count lives on the step via myStep.maxRetries = N (default 3). Control delay between attempts by throwing new RetryableError(msg, { retryAfter: '5s' }) — there is no built-in exponential helper; compute the delay yourself based on getStepMetadata().attempt if you need one.
metadata.stream() / RealtimegetWritable() / getWritable({ namespace })For granular business status (progress updates, current-stage messages), prefer writing to getWritable({ namespace: 'status' }) from a step and reading from the end of the named stream on the client. Use getRun(runId).status for terminal/lifecycle state only.
Self-hosted worker + dashboardManaged execution + built-in UINo worker fleet to operate.

trigger.dev's task.run body has full Node.js access. The SDK's 'use workflow' body runs in a sandboxed VM — side effects (I/O, Date.now(), Math.random(), DB, fetch) must live inside 'use step' functions. Orchestration stays in the workflow body.

Start with the shell. trigger.dev wraps the handler in task(); the Workflow SDK marks the function with a directive.

import { task } from '@trigger.dev/sdk';

export const processOrder = task({
  id: 'process-order',
  run: async (payload: { orderId: string }) => {
    return { orderId: payload.orderId, status: 'completed' };
  },
});
export async function processOrder(orderId: string) {
  'use workflow'; 
  return { orderId, status: 'completed' };
}

What changed: the task() factory and its id field disappear. The function is a plain export tagged with "use workflow".

Add a step

The body of run becomes one or more "use step" functions.

async function loadOrder(orderId: string) {
  'use step'; 
  const res = await fetch(`https://example.com/api/orders/${orderId}`);
  return res.json() as Promise<{ id: string }>;
}

Call it from the workflow with a plain await. Each additional side effect (reserveInventory, chargePayment) follows the same shape.

Start the run

trigger.dev dispatches with tasks.trigger<typeof processOrder>('process-order', { orderId }). The Workflow SDK calls start() directly:

import { start } from 'workflow/api';
import { processOrder } from '@/workflows/order';

export async function POST(request: Request) {
  const { orderId } = (await request.json()) as { orderId: string };
  const run = await start(processOrder, [orderId]); 
  return Response.json({ runId: run.runId });
}

No id lookup, no API key, no separate worker. start() returns a handle immediately.

wait.forToken() becomes createHook() plus await.

// import { wait } from '@trigger.dev/sdk';
const token = await wait.createToken({ timeout: '7d' });
const approval = await wait.forToken<{ approved: boolean }>(token.id).unwrap();
// External system resumes with: await wait.completeToken(token.id, { approved: true });
using approval = createHook<{ approved: boolean }>({ 
  token: `refund:${refundId}:approval`,
});
const payload = await approval;

What changed: the platform-issued opaque token becomes an app-owned string. The caller that resumes the run supplies that same string, so there is no token lookup. (trigger.dev also exposes token.url for external callers; the SDK analog is createWebhook().url).

Resume from an API route

There are two shapes of resume, and wait.forToken can map to either:

  • Server-side resume (known token): createHook<T>({ token: 'business-token' }) + resumeHook(token, payload) from an API route. Use this when your app knows the token shape and controls the resume call.
  • Third-party callback URL (generated token): createWebhook({ respondWith: 'default' }) + pass webhook.url to the external system. The external system hits the URL to resume.

See /docs/foundations/hooks for both surfaces.

trigger.dev completes a token with wait.completeToken(tokenId, { approved }). The SDK equivalent is resumeHook:

import { resumeHook } from 'workflow/api';

export async function POST(request: Request, { params }: { params: Promise<{ refundId: string }> }) {
  const { refundId } = await params;
  const { approved } = (await request.json()) as { approved: boolean };
  await resumeHook(`refund:${refundId}:approval`, { approved }); 
  return Response.json({ ok: true });
}

Add a timeout and branch on the payload

trigger.dev's timeout: '7d' option maps to a Promise.race() with sleep():

const result = await Promise.race([
  approval.then((p) => ({ type: 'decision' as const, approved: p.approved })),
  sleep('7d').then(() => ({ type: 'timeout' as const })), 
]);

if (result.type === 'timeout') return { refundId, status: 'timed-out' };
if (!result.approved) return { refundId, status: 'rejected' };
return { refundId, status: 'approved' };

A hook is an inbound write channel. The caller that knows the token resumes the run with a typed payload. For granular business status (progress updates, current-stage messages), prefer writing to getWritable({ namespace: 'status' }) from a step and reading from the end of the named stream on the client. Use getRun(runId).status for terminal/lifecycle state only.

triggerAndWait() splits into two steps: spawn and collect. start() and getRun() are runtime APIs, so wrap them in "use step" functions. Return the full Run object from spawnChild so observability tooling can deep-link to the child run.

You can return either the full Run object (enables deep-linking) or just run.runId (simpler). The runtime serializes Run to its runId in the event log either way.

import { start } from 'workflow/api';

async function spawnChild(item: string) {
  'use step';
  return start(childWorkflow, [item]); 
}

Await the result in a second step, then orchestrate both from the parent:

import { getRun } from 'workflow/api';

async function collectResult(runId: string) {
  'use step';
  const run = getRun(runId);
  return (await run.returnValue) as { item: string; result: string }; 
}

export async function parentWorkflow(item: string) {
  'use workflow';
  const child = await spawnChild(item);
  return await collectResult(child.runId);
}

To fan out, call spawnChild inside a loop, then Promise.all the collectResult calls. That replaces batch.triggerAndWait().

Promise.all rejects on first failure; use Promise.allSettled if you need batch-mode error tolerance similar to trigger.dev's { ok, output, error } per-run result.

Dropping the trigger.dev SDK removes several moving parts:

  • No task registry or id strings. Workflow files carry directive annotations and export plain functions.
  • No @trigger.dev/sdk client or API key. start() launches runs directly from API routes or server actions.
  • No worker fleet or self-hosted instance. The runtime schedules execution inside the app's deploy target.
  • No separate Realtime channel. getWritable() streams updates from steps over the run's durable stream.
  • No dashboard account. The built-in observability UI (npx workflow web) reads the same event log the runtime writes.

Workflows suspended on sleep() or a hook consume no compute until resumed.

Pick one trigger.dev task and migrate it end-to-end before touching the rest. The steps below describe the smallest viable path.

Step 1: Install the Workflow SDK

Add the runtime. Framework integrations (Next.js, Nitro, Nuxt, SvelteKit, Astro, Nest) are subpath exports of the same workflow package, e.g. workflow/next — no additional install needed.

Step 2: Convert task() to a "use workflow" export

Drop the factory call and the id. Move the handler body up and replace the payload object with typed function arguments.

// Before (trigger.dev)
// export const processOrder = task({
//   id: "process-order",
//   run: async ({ orderId }: { orderId: string }) => { ... },
// });

// After (Workflow SDK)
export async function processOrder(orderId: string) {
  "use workflow"; 
  // ...
}

Step 3: Convert the task body into "use step" named functions

Each side effect becomes a named function with "use step" on the first line. The workflow calls them with a plain await. schemaTask() validation moves to the call site: validate with zod before calling start().

async function loadOrder(id: string) {
  "use step"; 
  return fetch(`/api/orders/${id}`).then((r) => r.json());
}

Step 4: Replace wait.* with hooks and sleep

  • wait.for({ seconds | minutes | hours | days }) / wait.until({ date })sleep('5m') or sleep(date) from workflow.
  • wait.forToken(token)createHook({ token }) + await. Complete it with resumeHook(token, payload) from an API route.
  • wait.forToken({ timeout })Promise.race([hook, sleep(timeout)]).
  • triggerAndWait(payload) → wrap start(child, [payload]) in a "use step" function and return the Run object, then read the result with a second step that calls getRun(runId).returnValue.

Step 5: Start runs from the app

Delete the trigger.dev client setup. Launch runs directly from an API route or server action:

import { start } from "workflow/api";
import { processOrder } from "@/workflows/order";

export async function POST(req: Request) {
  const { orderId } = await req.json();
  const run = await start(processOrder, [orderId]);
  return Response.json({ runId: run.runId });
}

Step 6: Retire trigger.dev infrastructure

Remove the @trigger.dev/sdk dependency, the trigger.config.ts file, the trigger/ directory, and any self-hosted worker deployment. Delete dashboard API keys from the environment. Verify the run in npx workflow web before shipping.

Retry count lives on the step function itself. Set it as a property on the step:

async function chargePayment(orderId: string) {
  "use step";
  // ...
}
chargePayment.maxRetries = 5;

Throw new RetryableError(msg, { retryAfter: '5s' }) to control delay between attempts, or FatalError to stop retries immediately. See /docs/foundations/errors-and-retries.

  • schedules.task() / cron triggers. The SDK has no built-in scheduler. Trigger runs from Vercel Cron or a system cron calling start().
  • Concurrency keys / queue concurrency limits. No direct analog. Enforce limits inside steps (semaphores, external coordinator) or debounce at the publisher.
  • machine presets / custom images. Machine specs are per-task in trigger.dev; in the SDK, function resources are per-deployment (configure via your hosting platform).
  • Realtime / subscribeToRun. Use getRun(runId).getReadable() plus named getWritable() streams for live progress.
  • onFailure lifecycle hook. No equivalent. Handle cleanup in the workflow body with a try/catch + compensation-stack pattern.
  • Trigger.dev dashboard. Workflow SDK ships npx workflow web for local inspection and the Vercel Observability tab for deployed runs.
  • Replace task({ id, run }) with a "use workflow" function; launch it with start().
  • Convert each task body into named "use step" functions.
  • Swap wait.for / wait.until for sleep() from workflow.
  • Swap wait.forToken for createHook() (internal) or createWebhook() (HTTP).
  • Model wait.forToken timeouts as Promise.race() between the hook and sleep().
  • Replace triggerAndWait() with "use step" wrappers around start() and getRun().
  • Replace batch.triggerAndWait() with Promise.all over the collected child Run handles.
  • Move schemaTask validation to the call site; pass typed arguments into the workflow.
  • Replace AbortTaskRunError with FatalError; model retries per step with RetryableError and maxRetries.
  • Use getStepMetadata().stepId as the idempotency key for external side effects.
  • Replace metadata.stream() and Realtime with getWritable().
  • Remove the @trigger.dev/sdk dependency, trigger.config.ts, and any self-hosted worker.
  • Deploy and verify runs end-to-end with the built-in observability UI.

Verified against workflow@5.0.0-beta.1 and @trigger.dev/sdk v3 on 2026-04-16.