




















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-sdkgetWritable() and getWritable({ namespace })). Durable status writes replace metadata.stream() and metadata.set(), and clients read from the end of the stream for current status.task() factory, no schemaTask() wrapper for payloads you already type with TypeScript.npx workflow CLI, @workflow/ai for durable AI agents, and a Claude skill for generating workflows.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.dev | Workflow SDK | Migration 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" function | Side effects move into named steps. |
logger / metadata.set | console + 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).returnValue | Wrap both in "use step" functions. |
batch.triggerAndWait() | Promise.all(runIds.map(collectResult)) | Fan out via standard concurrency. |
AbortTaskRunError | FatalError | Stops retries immediately. |
retry.onThrow / retry.fetch | RetryableError, FatalError, maxRetries | Retry 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() / Realtime | getWritable() / 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 + dashboard | Managed execution + built-in UI | No 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".
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.
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).
There are two shapes of resume, and wait.forToken can map to either:
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.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 });
}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:
id strings. Workflow files carry directive annotations and export plain functions.@trigger.dev/sdk client or API key. start() launches runs directly from API routes or server actions.getWritable() streams updates from steps over the run's durable stream.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.
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.
task() to a "use workflow" exportDrop 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";
// ...
}"use step" named functionsEach 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());
}wait.* with hooks and sleepwait.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.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 });
}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().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).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.npx workflow web for local inspection and the Vercel Observability tab for deployed runs.task({ id, run }) with a "use workflow" function; launch it with start()."use step" functions.wait.for / wait.until for sleep() from workflow.wait.forToken for createHook() (internal) or createWebhook() (HTTP).wait.forToken timeouts as Promise.race() between the hook and sleep().triggerAndWait() with "use step" wrappers around start() and getRun().batch.triggerAndWait() with Promise.all over the collected child Run handles.schemaTask validation to the call site; pass typed arguments into the workflow.AbortTaskRunError with FatalError; model retries per step with RetryableError and maxRetries.getStepMetadata().stepId as the idempotency key for external side effects.metadata.stream() and Realtime with getWritable().@trigger.dev/sdk dependency, trigger.config.ts, and any self-hosted worker.Verified against workflow@5.0.0-beta.1 and @trigger.dev/sdk v3 on 2026-04-16.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。