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

推荐订阅源

Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Threat Research - Cisco Blogs
Latest news
Latest news
Project Zero
Project Zero
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Google DeepMind News
Google DeepMind News
P
Privacy & Cybersecurity Law Blog
T
Troy Hunt's Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AWS News Blog
AWS News Blog
Hacker News: Ask HN
Hacker News: Ask HN
S
Security @ Cisco Blogs
C
Cisco Blogs
Help Net Security
Help Net Security
I
Intezer
W
WeLiveSecurity
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
腾讯CDC
S
Secure Thoughts
MyScale Blog
MyScale Blog
Recorded Future
Recorded Future
G
GRAHAM CLULEY
L
LINUX DO - 热门话题
A
About on SuperTechFans
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
J
Java Code Geeks
The Hacker News
The Hacker News
阮一峰的网络日志
阮一峰的网络日志
Scott Helme
Scott Helme
Recent Announcements
Recent Announcements
AI
AI
Cisco Talos Blog
Cisco Talos Blog
B
Blog RSS Feed
V
Vulnerabilities – Threatpost
C
Check Point Blog
Security Latest
Security Latest
S
SegmentFault 最新的问题
T
The Exploit Database - CXSecurity.com
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
M
MIT News - Artificial intelligence
T
The Blog of Author Tim Ferriss
Attack and Defense Labs
Attack and Defense Labs
PCI Perspectives
PCI Perspectives
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Tailwind CSS Blog
Apple Machine Learning Research
Apple Machine Learning Research

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.