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

推荐订阅源

博客园_首页
F
Full Disclosure
Martin Fowler
Martin Fowler
The GitHub Blog
The GitHub Blog
L
LangChain Blog
T
The Blog of Author Tim Ferriss
D
DataBreaches.Net
GbyAI
GbyAI
Y
Y Combinator Blog
博客园 - Franky
WordPress大学
WordPress大学
Apple Machine Learning Research
Apple Machine Learning Research
A
About on SuperTechFans
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
G
Google Developers Blog
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
博客园 - 三生石上(FineUI控件)
IT之家
IT之家
Jina AI
Jina AI
N
Netflix TechBlog - Medium
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recent Announcements
Recent Announcements
酷 壳 – CoolShell
酷 壳 – CoolShell
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 聂微东
腾讯CDC
H
Help Net Security
H
Heimdal Security Blog
J
Java Code Geeks
C
CXSECURITY Database RSS Feed - CXSecurity.com
T
The Exploit Database - CXSecurity.com
The Register - Security
The Register - Security
大猫的无限游戏
大猫的无限游戏
T
Threatpost
B
Blog RSS Feed
T
Threat Research - Cisco Blogs
Microsoft Security Blog
Microsoft Security Blog
S
Securelist
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
B
Blog
量子位
AWS News Blog
AWS News Blog
Project Zero
Project Zero
P
Proofpoint News Feed
Cisco Talos Blog
Cisco Talos Blog
Spread Privacy
Spread Privacy
S
Schneier on Security

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

Move an AWS Step Functions state machine to the Workflow SDK by replacing JSON state definitions, Task states, Choice/Wait/Parallel states, Retry/Catch blocks, and .waitForTaskToken callbacks with Workflows, Steps, Hooks, and idiomatic TypeScript control flow.

Move an AWS Step Functions state machine to the Workflow SDK by replacing JSON state definitions with TypeScript functions. This guide shows the direct mapping between ASL states and Workflow SDK primitives.

Install the Workflow SDK migration skill:

npx skills add https://github.com/vercel/workflow --skill migrating-to-workflow-sdk
  • Orchestration code is TypeScript, not JSON ASL. Transitions are await, branches are if/switch, and parallelism is Promise.all.
  • Streaming is built in. Write durable progress from steps with getWritable() and named streams. No DynamoDB or SNS glue to surface status to clients.
  • Infrastructure lives in one deployment. No separate state machine, per-task Lambda, IAM role wiring, or callback SQS queues.
  • Error handling is TypeScript-native: step-level retries, RetryableError, and FatalError replace per-state Retry/Catch blocks.
  • The npx workflow CLI and npx workflow web observability UI ship out of the box.
  • AI/agent helpers — @workflow/ai for AI-SDK integration and the Claude migration skill — are available as separate installs.

This guide assumes Standard workflows. Express workflows have different semantics (at-least-once, 5-minute max duration, no execution history) and may need a different target — consider keeping them on Step Functions, moving them to a queue consumer, or ensuring your steps are idempotent before replaying the pattern here.

AWS Step Functions defines workflows as JSON state machines using Amazon States Language (ASL). Each state (Task, Choice, Wait, Parallel, Map) is a node in a declarative graph. Lambda functions handle tasks, Retry/Catch blocks configure per-state error handling, and .waitForTaskToken manages callbacks.

The Workflow SDK replaces that JSON DSL with TypeScript. "use workflow" functions orchestrate "use step" functions in the same file. Branching is if/else. Waiting is sleep(). Parallelism is Promise.all(). Retries move down to the step level.

The migration replaces declarative configuration with idiomatic TypeScript and collapses the orchestrator and compute split. Business logic stays the same.

AWS Step FunctionsWorkflow SDKMigration note
State machine (ASL JSON)"use workflow" functionThe workflow function is the state machine.
Task state / Lambda"use step" functionSide effects go in steps. No separate Lambda.
Choice stateif / else / switchNative TypeScript control flow.
Wait statesleep()Import sleep from workflow.
Parallel statePromise.all()Standard concurrency primitives.
Map stateInline sequential → for loop; bounded parallel (MaxConcurrency: N) → batched Promise.all or a concurrency limiter like p-limit; Distributed Map / large fan-out → step-wrapped start() per item, then step-wrapped getRun() to collect.Match the concurrency mode of the original Map.
Retry / CatchStep retries, RetryableError, FatalErrorRetry logic moves to step boundaries.
Catch to a compensation statetry/catch in the workflow function, calling compensation steps in reverse order (push/pop a rollback stack)See /docs/foundations/errors-and-retries for the SAGA pattern.
.waitForTaskTokencreateHook() or createWebhook()Hooks for typed signals; webhooks for HTTP.
Child state machine (StartExecution)"use step" around start() / getRun()Return the Run object, await its result from another step.
Execution event historyWorkflow event logSame durable replay model.
Progress via DynamoDB / SNS for client pollinggetWritable() + named streamsStream durable updates; clients read from the stream.

.waitForTaskToken becomes createHook() or createWebhook(). Choice states become if/else. Map states become Promise.all(). Retry policies move from per-state configuration to step-level defaults.

Start with a single Task state. In ASL, even "call one Lambda" requires a state machine shell:

"LoadOrder": {
  "Type": "Task",
  "Resource": "arn:aws:states:::lambda:invoke",
  "Parameters": { "FunctionName": "loadOrder", "Payload.$": "$" },
  "End": true
}

Examples use JSONPath mode. If your state machine sets QueryLanguage: 'JSONata', the shape of Arguments/Output fields differs but the TypeScript translation is identical.

export async function processOrder(orderId: string) {
  'use workflow'; 
  return await loadOrder(orderId);
}

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

What changed: the ASL state machine and its Lambda collapse into two directive-tagged functions in one file.

Adding a second step

In ASL, a second Task means a new state and a "Next" transition. In the Workflow SDK, it's another await:

export async function processOrder(orderId: string) {
  'use workflow';
  const order = await loadOrder(orderId);
  await reserveInventory(order.id); 
  return { orderId: order.id, status: 'reserved' };
}

await replaces "Next". Each new step is a new function with "use step"; no additional deployment. The second version also reshapes the return value; the workflow return type can be anything serializable.

Starting from an API route

Step Functions starts a run via StartExecution (AWS SDK or API Gateway integration). The Workflow SDK starts a run with start() from a route handler:

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

Waiting for a fixed duration

A Wait state becomes sleep():

{ "Type": "Wait", "Seconds": 60, "Next": "Next" }

The minimal ASL for a callback is a Task with .waitForTaskToken:

"WaitForApproval": {
  "Type": "Task",
  "Resource": "arn:aws:states:::sqs:sendMessage.waitForTaskToken",
  "Parameters": {
    "QueueUrl": "https://sqs.us-east-1.amazonaws.com/123456789012/approvals",
    "MessageBody": {
      "refundId.$": "$.refundId",
      "TaskToken.$": "$$.Task.Token"
    }
  },
  "End": true
}
import { createHook } from 'workflow';

export async function refundWorkflow(refundId: string) {
  'use workflow';
  using approval = createHook<{ approved: boolean }>({ 
    token: `refund:${refundId}:approval`,
  });
  return await approval;
}

What changed: no SQS queue, no task token, no callback Lambda. The hook suspends the workflow durably until it is resumed.

Resuming the hook

Step Functions resumes by calling SendTaskSuccess with the task token. The Workflow SDK resumes by calling resumeHook with the hook's token:

import { resumeHook } from 'workflow/api';

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

Branching on the result

In ASL, branching after the wait requires a Choice state. In TypeScript, it's just if/else:

"CheckApproval": {
  "Type": "Choice",
  "Choices": [
    { "Variable": "$.approved", "BooleanEquals": true, "Next": "Approved" }
  ],
  "Default": "Rejected"
}
const { approved } = await approval;
if (approved) return { refundId, status: 'approved' }; 
return { refundId, status: 'rejected' };

In ASL, a parent machine calls StartExecution (usually via .sync or .waitForTaskToken) to launch a child. In the Workflow SDK, start() and getRun() are runtime APIs, so wrap them in "use step" functions. Returning the Run object from the spawn step lets workflow observability deep-link to the child run.

Parent starts a child

import { start } from 'workflow/api';

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

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

Awaiting the child's result

Add a second step that wraps getRun() and awaits returnValue:

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

Then in the workflow: const result = await collectResult(run.runId);. The child workflow itself (childWorkflow) is defined elsewhere with "use workflow".

Moving off Step Functions removes these surfaces from the application:

  • ASL state machine JSON and its reference syntax.
  • Per-task Lambda functions, their IAM roles, and CloudFormation/CDK wiring.
  • Task-token delivery infrastructure (SQS queues, callback Lambdas).
  • Separate progress channels (DynamoDB, SNS) for client-visible updates.
  • Remove CloudWatch and X-Ray wiring that was specific to orchestrator state transitions. Keep (or re-wire) any application-level CloudWatch alarms, log retention policies, or X-Ray propagation that the rest of your AWS footprint still depends on. Workflow SDK exports OTEL traces, so existing OTEL-compatible backends can continue to ingest them.

Workflow and step functions live in the same deployment as the application. State transitions are ordinary control flow (await, if, Promise.all, for). Progress streaming, retries, and observability are built in.

What you take on

Steps that previously invoked AWS services via optimized integrations (EventBridge, DynamoDB, Bedrock, ECS.RunTask.sync, etc.) become ordinary SDK calls inside 'use step' functions. Credentials and retries move into the step, and .sync-style waits for long-running jobs become explicit polling loops or hook-based callbacks.

Pick one state machine 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 workflow runtime package.

Step 2: Rewrite the state machine as a "use workflow" function

Transitions become await calls. Control flow (Choice, Wait, Parallel, Map) becomes if/switch, sleep, Promise.all, and loops.

export async function processOrder(orderId: string) {
  "use workflow"; 
  const order = await loadOrder(orderId);
  if (order.total > 1000) await reviewManually(order);
  await chargePayment(order);
}

Step 3: Move each Lambda into a step function

Inline the Lambda body into a function with "use step" on the first line. Step functions keep full Node.js access, so existing SDK calls work unchanged.

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

Step 4: Replace .waitForTaskToken with a hook

Swap the task-token callback Lambda for createHook(). Callers resumeHook(token, payload) instead of SendTaskSuccess.

Move Retry/Catch off per-state configuration and onto step boundaries. Set maxRetries as a function property; throw RetryableError or FatalError to control retry behavior:

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

See /docs/foundations/errors-and-retries for the full retry and SAGA compensation patterns.

Step 5: Start runs from an API route

Delete the StartExecution call and IAM wiring. Launch runs directly from a route handler:

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 Step Functions infrastructure

Delete the ASL JSON, per-task Lambda deployments, IAM roles, and callback queues. Remove CloudWatch and X-Ray wiring that was specific to orchestrator state transitions — keep alarms, log retention, and traces for resources you still depend on. Verify the run in npx workflow web before shipping.

  • Express workflows. At-least-once semantics and 5-minute duration make them a poor fit for the SDK's durable replay model. Consider keeping them on Step Functions or migrating to a queue consumer.
  • Distributed Map state. Up to 10,000 concurrent child executions with S3 item sources has no 1:1 analog; fan out with step-wrapped start() per item, then Promise.all with p-limit to bound concurrency.
  • Optimized AWS service integrations (arn:aws:states:::dynamodb:*, eventbridge:*, bedrock:*, ecs:runTask.sync, etc.). These become regular SDK calls inside 'use step' functions — credentials, retries, and polling move into the step.
  • Per-state IAM roles. ASL lets each state run under its own IAM role. In the SDK, all steps share the deployment's credentials; scope secrets and roles at deployment time.
  • CloudWatch alarms / X-Ray cross-service traces / CloudWatch Logs retention. The SDK event log + observability UI replaces orchestrator state transitions, not AWS-wide observability. Keep alarms and traces for other resources.
  • JSONata QueryLanguage mode. Valid at the source; the TS translation is identical regardless of mode.
  • Replace the ASL state machine with a single "use workflow" function. Transitions become await calls.
  • Convert each Task / Lambda into a "use step" function in the same file.
  • Replace Choice states with if/else/switch.
  • Replace Wait states with sleep() from workflow.
  • Replace Parallel states with Promise.all().
  • Replace Map states based on their concurrency mode: inline sequential → for loop; bounded parallel (MaxConcurrency: N) → batched Promise.all or a concurrency limiter like p-limit; Distributed Map / large fan-out → step-wrapped start() per item, then step-wrapped getRun() to collect.
  • Replace StartExecution child machines with "use step" wrappers around start() and getRun().
  • Replace .waitForTaskToken with createHook() (internal callers) or createWebhook() (HTTP callers).
  • Move Retry/Catch to step boundaries using maxRetries, RetryableError, and FatalError.
  • Use getStepMetadata().stepId as the idempotency key for external side effects.
  • Stream progress from steps with getWritable() instead of polling DynamoDB or SNS.
  • Deploy and verify runs end-to-end with built-in observability.

Verified against workflow@5.0.0-beta.1 and the AWS Step Functions Amazon States Language spec on 2026-04-16.