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

推荐订阅源

F
Fortinet All Blogs
美团技术团队
T
Tenable Blog
大猫的无限游戏
大猫的无限游戏
Schneier on Security
Schneier on Security
S
Schneier on Security
博客园_首页
C
Cyber Attacks, Cyber Crime and Cyber Security
I
Intezer
T
Tailwind CSS Blog
GbyAI
GbyAI
C
CERT Recently Published Vulnerability Notes
博客园 - 【当耐特】
IT之家
IT之家
G
Google Developers Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
Vercel News
Vercel News
Cyberwarzone
Cyberwarzone
N
News and Events Feed by Topic
Application and Cybersecurity Blog
Application and Cybersecurity Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Register - Security
The Register - Security
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Project Zero
Project Zero
V
V2EX
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
月光博客
月光博客
Hacker News - Newest:
Hacker News - Newest: "LLM"
WordPress大学
WordPress大学
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
MongoDB | Blog
MongoDB | Blog
F
Full Disclosure
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
N
News and Events Feed by Topic
G
GRAHAM CLULEY
H
Hacker News: Front Page
Forbes - Security
Forbes - Security
H
Heimdal Security Blog
L
LINUX DO - 最新话题
L
Lohrmann on Cybersecurity
Apple Machine Learning Research
Apple Machine Learning Research
Help Net Security
Help Net Security
C
Cybersecurity and Infrastructure Security Agency CISA
Recent Announcements
Recent Announcements
C
Cisco Blogs
L
LINUX DO - 热门话题

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 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 Migrating from trigger.dev Secure Credential Handling Local World | Workflow SDK Postgres World | Workflow SDK Vercel World | Workflow SDK
Migrating from Inngest
2026-05-31 · via Workflow SDK Documentation

Move an Inngest TypeScript SDK v3 or v4 app to the Workflow SDK by replacing createFunction, step.run(), step.sleep(), step.waitForEvent(), and step.invoke() 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. Durable progress writes go to named streams via getWritable(). There is no separate realtime publish channel or WebSocket layer to operate.
  • Infrastructure and orchestration live in a single deployment. Workflows run where the app runs. There is no separate Inngest Dev Server or event bus to operate.
  • TypeScript-first DX. Steps are named async functions marked with "use step". No inline closures tied to a framework-specific lifecycle.
  • Agent-first tooling: the npx workflow CLI, @workflow/ai integration for durable AI agents, and a Claude skill for generating workflows.

Inngest code samples in this guide use the v4 two-argument createFunction({ id, triggers }, handler) shape. If the app is still on Inngest v3, treat the three-argument form createFunction({ id }, { event: '...' }, handler) as equivalent — the triggers option in v4 replaces the second argument in v3.

Inngest defines functions with inngest.createFunction(), registers them through a serve() handler, and breaks work into steps with step.run(), step.sleep(), and step.waitForEvent(). The platform routes events, schedules steps, and applies retries.

The Workflow SDK replaces that with "use workflow" functions that orchestrate "use step" functions in plain TypeScript. There is no function registry, event dispatch layer, or SDK client. Durable replay, automatic retries, and step-level persistence are built into the runtime.

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

Inngest's event-bus model is loosely coupled — publishers don't know consumers. start() requires the caller to import the workflow function directly, giving stronger type safety but tighter coupling. For event-bus-like fan-out, wrap start() in a shared publisher module.

InngestWorkflow SDKMigration note
inngest.createFunction()"use workflow" function started with start()No wrapper needed.
step.run()"use step" functionStandalone async function with Node.js access.
step.sleep() / step.sleepUntil()sleep()sleep('5m') for a duration; sleep(date) for sleep-until.
step.waitForEvent()createHook() or createWebhook()Hooks for typed signals, webhooks for HTTP.
step.invoke()"use step" wrappers around start() / getRun()Spawn a child run, pass runId forward.
inngest.send() / event triggersstart() from your app boundaryStart workflows directly.
Retry configuration (retries)RetryableError, FatalError, maxRetriesRetry logic lives at the step level.
step.sendEvent()"use step" wrapper around start()Fan out via start(), not an event bus.
Realtime / step.realtime.publish()getWritable() / getWritable({ namespace })Named streams are the canonical way for clients to read workflow status. No database or getRun() polling required.

Start with the shell of a function. Inngest wraps it in createFunction; Workflow SDK marks it with a directive.

export const processOrder = inngest.createFunction(
  {
    id: 'process-order',
    triggers: { event: 'order/created' },
  },
  async ({ event, step }) => {
    return { orderId: event.data.orderId, status: 'completed' };
  }
);
export async function processOrder(orderId: string) {
  'use workflow'; 
  return { orderId, status: 'completed' };
}

What changed: the factory + event binding collapses into a plain exported function with a "use workflow" directive.

Add a step

step.run() closures become named "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 like any async function: const order = await loadOrder(orderId). Additional side effects (reserveInventory, chargePayment) follow the same shape.

Start the run

Inngest dispatches via inngest.send({ name: 'order/created', data: { orderId } }). Workflow SDK launches directly:

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

const run = await start(processOrder, [orderId]); 

No event bus, no registry. start() returns a handle immediately.

step.waitForEvent() becomes createHook() plus await.

const approval = await step.waitForEvent('wait-for-approval', {
  event: 'refund/approved',
  match: 'data.refundId',
  timeout: '7d',
});
using approval = createHook<{ approved: boolean }>({ 
  token: `refund:${refundId}:approval`,
});
const payload = await approval;

What changed: event name + match expression collapse into a single token string. The caller supplies that token directly, with no event schema.

Resume from an API route

Inngest resumes with inngest.send({ name: 'refund/approved', data: { refundId, 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

Inngest'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' };

Event matching disappears. A hook's token encodes the routing (for example, refund:${refundId}:approval), and the caller supplies that token to resumeHook().

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

You can return either the full Run object (enables deep-linking) or just run.runId (simpler).

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

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

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

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

Dropping the Inngest SDK removes several moving parts:

  • No SDK client or serve handler. Workflow files carry directive annotations. No registry, no serve endpoint.
  • No event bus. start() launches workflows directly from API routes, server actions, or other entry points. No event schemas or dispatch layer.
  • No inline step closures. Steps are named async functions. They type-check and test like any other TypeScript function.
  • No separate streaming transport. getWritable() delivers progress to clients without WebSockets or SSE glue.
  • No idle workers. Workflows suspended on sleep() or a hook consume no compute until resumed.

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

Step 1: Install the Workflow SDK

Install the SDK. Framework integrations (workflow/next, workflow/nitro, workflow/nuxt, etc.) are subpath exports of the same workflow package — no additional install needed.

Step 2: Convert createFunction to a "use workflow" export

Replace the factory call with a plain async export. Move the handler body up. The event-binding argument goes away.

// Before (Inngest)
// export const processOrder = inngest.createFunction(
//   { id: "process-order", triggers: { event: "order/created" } },
//   async ({ event, step }) => { ... }
// );

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

Step 3: Convert step.run callbacks into named step functions

Each inline callback becomes a named function with "use step" on the first line. The workflow calls them with a plain await.

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

Configure per-step retry counts by assigning maxRetries as a function property:

async function callApi(endpoint: string) {
  "use step";
  const response = await fetch(endpoint);
  return response.json();
}
callApi.maxRetries = 5;

See Errors and retries for full retry docs.

Step 4: Replace waitForEvent, sleep, and invoke

  • step.waitForEvent(...)createHook({ token }) + await hook. Resume it from an API route with resumeHook(token, payload).
  • step.sleep(...)sleep("5m") from workflow.
  • step.invoke(child, { data }) → wrap start(child, [data]) in a "use step" function that returns the Run, and optionally read its return value with getRun(run.runId).returnValue.

Step 5: Start runs from the app

Delete the serve() handler and event dispatch. Launch runs directly from an API route:

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 the Inngest infrastructure

Remove the inngest client, the serve() route, event schemas, and the Inngest Dev Server from the app. Verify the run in npx workflow web before shipping.

  • Cron / scheduled functions (triggers: { cron: '...' }). The SDK has no built-in scheduler. Trigger runs from Vercel Cron or a system cron calling start() from an API route.
  • Concurrency, throttling, rate limiting, debounce, singleton, priority, and batchEvents. These function-level settings have no direct analog. Enforce limits inside steps (semaphores, external rate-limiter service) or debounce at the publisher before calling start().
  • EventSchemas / typed events. The event-bus indirection goes away; publishers import the workflow function directly, giving the same type safety through a different mechanism.
  • Event-bus fan-out by name match. inngest.send() that triggered multiple functions by event name must be replaced by explicit start() calls for each target workflow.
  • Replace inngest.createFunction() with a "use workflow" function; launch it with start().
  • Convert each step.run() callback into a named "use step" function.
  • Swap step.sleep() / step.sleepUntil() for sleep() from workflow.
  • Swap step.waitForEvent() for createHook() (internal) or createWebhook() (HTTP).
  • Model waitForEvent timeouts as Promise.race() between the hook and sleep().
  • Replace step.invoke() with "use step" wrappers around start() and getRun().
  • Replace step.sendEvent() fan-out with start() called from a "use step" function.
  • Remove the Inngest client, serve() handler, and event definitions.
  • Push retry configuration down to step boundaries via maxRetries, RetryableError, and FatalError.
  • Use getStepMetadata().stepId as the idempotency key for external side effects.
  • Replace step.realtime.publish() with getWritable().
  • Deploy and verify end-to-end with the built-in observability UI.

Verified against workflow@5.0.0-beta.1 and Inngest TypeScript SDK v4 on 2026-04-16.