awaitly vs Vercel Workflow
Vercel Workflow is a durable execution engine for TypeScript that uses compiler directives ("use workflow", "use step") to create long-running, resumable workflows. awaitly provides explicit Result types and workflow orchestration that runs anywhere Node.js runs.
Philosophy Comparison
Section titled “Philosophy Comparison”Vercel Workflow awaitly┌─────────────────────────┐ ┌──────────────────────────┐│ Compiler magic │ │ Explicit types ││ ────────────────────── │ │ ────────────────────── ││ • "use step" directive │ │ • AsyncResult<T, E> ││ • "use workflow" │ │ • createWorkflow() ││ • Platform durability │ │ • Tagged errors ││ • Zero-config setup │ │ • Platform-agnostic ││ • Vercel integration │ │ • Bring your own store │└─────────────────────────┘ └──────────────────────────┘Quick Comparison
Section titled “Quick Comparison”| Feature | Vercel Workflow | awaitly |
|---|---|---|
| Step Definition | "use step" directive | Return AsyncResult<T, E> |
| Workflow Definition | "use workflow" directive | createWorkflow() factory |
| Error Handling | throw Error / RetryableError | return err(error) with TaggedError |
| Type Safety | Basic TypeScript | Enhanced with Result types |
| Parallel Execution | Promise.all() | step.all() / step.parallel() with named steps; step.map() for arrays |
| Race Conditions | Promise.race() | step.race() with anyAsync() |
| Retry Logic | RetryableError class | Retry options in step config |
| Saga Pattern | Manual try/catch rollback | createSagaWorkflow with auto-compensation |
| Static Analysis | Limited | awaitly-analyze with Mermaid diagrams |
| ESLint Plugin | None | eslint-plugin-awaitly with autofixes |
| Runtime Visualization | Limited | awaitly-visualizer with multiple formats (Mermaid, ASCII, JSON) |
| Platform Lock-in | Vercel only | Any Node.js environment |
| Durability | Built-in (Vercel platform) | Configurable (PostgreSQL, Redis, etc.) |
| Compiler Required | Yes (custom compiler) | No (standard TypeScript) |
| Workflow Metadata | getWorkflowMetadata() | WorkflowContext (3rd callback arg) |
| Step Metadata | getStepMetadata() with attempt | onEvent callback with retry events |
| Hooks/Webhooks | createHook() with suspension | Custom implementation via onEvent |
| AI Agents | DurableAgent class | Any AI library e.g. AI SDK, Tanstack AI, Langchain etc |
| Sleep | sleep("5s") built-in | step.sleep(“delay”, “5s”) |
Core Patterns
Section titled “Core Patterns”Defining Steps
Section titled “Defining Steps”import { Awaitly, type AsyncResult, TaggedError } from "awaitly";
class UserNotFoundError extends TaggedError("UserNotFoundError")<{ id: string }> {}
async function fetchUser(id: string): AsyncResult<User, UserNotFoundError> { const user = await db.findUser(id); if (!user) { return Awaitly.err(new UserNotFoundError({ id })); } return Awaitly.ok(user);}async function fetchUser(id: string): Promise<User> { "use step"; const user = await db.findUser(id); if (!user) { throw new Error("User not found"); } return user;}Key difference: awaitly makes error types explicit in the function signature. The compiler tracks what can fail.
Creating Workflows
Section titled “Creating Workflows”import { createWorkflow } from "awaitly/workflow";
export const userWorkflow = createWorkflow('workflow', { fetchUser }, { description: "Fetch user workflow" });
export async function runWorkflow(id: string) { return await userWorkflow(async (step, { fetchUser }) => { // Use function wrapper for proper caching/resume const user = await step('fetchUser', () => fetchUser(id), { key: "fetch-user" }); return user; });}export async function workflow() { "use workflow"; const user = await fetchUser("123"); return user;}Error Handling
Section titled “Error Handling”Retryable vs Fatal Errors
Section titled “Retryable vs Fatal Errors”class InvalidAmountError extends TaggedError("InvalidAmountError")<{ amount: number }> {}class AmountTooLargeError extends TaggedError("AmountTooLargeError")<{ amount: number }> {}
async function processPayment( amount: number): AsyncResult<PaymentResult, InvalidAmountError | AmountTooLargeError> { if (amount < 0) { return Awaitly.err(new InvalidAmountError({ amount })); } if (amount > 10000) { return Awaitly.err(new AmountTooLargeError({ amount })); } return Awaitly.ok(await chargeCard(amount));}
// Configure retry for specific errorsconst payment = await step('processPayment', () => processPayment(amount), { key: "process-payment", retry: { attempts: 3, backoff: "exponential", initialDelay: 100, },});async function processPayment(amount: number) { "use step"; if (amount < 0) { throw new RetryableError("Invalid amount"); } if (amount > 10000) { throw new Error("Amount too large"); // Fatal } return await chargeCard(amount);}Parallel Execution
Section titled “Parallel Execution”export const parallelWorkflow = createWorkflow('workflow', { fetchUser, fetchPosts, fetchComments }, { description: "Parallel fetch workflow" });
export async function runParallelWorkflow(id: string) { return await parallelWorkflow(async (step) => { // Named object form (recommended - better type inference) const { user, posts, comments } = await step.parallel("Fetch all data", { user: () => deps.fetchUser(id), posts: () => deps.fetchPosts(id), comments: () => deps.fetchComments(id), });
return { user, posts, comments }; });}export async function parallelWorkflow() { "use workflow"; const [user, posts, comments] = await Promise.all([ fetchUser(id), fetchPosts(id), fetchComments(id), ]); return { user, posts, comments };}Key difference: awaitly’s step.parallel() provides named steps for better observability and type inference.
Race Conditions
Section titled “Race Conditions”import { Awaitly } from "awaitly";
export const raceWorkflow = createWorkflow('workflow', { fetchFromPrimary, fetchFromFallback }, { description: "Race workflow" });
export async function runRaceWorkflow() { return await raceWorkflow(async (step) => { const result = await step.race("Fastest API", () => Awaitly.anyAsync([deps.fetchFromPrimary(), deps.fetchFromFallback()]) ); return result; });}export async function raceWorkflow() { "use workflow"; const result = await Promise.race([ fetchFromPrimary(), fetchFromFallback(), ]); return result;}Workflow Context & Metadata
Section titled “Workflow Context & Metadata”import { Awaitly } from "awaitly";import { createWorkflow } from "awaitly/workflow";
export const myWorkflow = createWorkflow('workflow', { myStep }, { description: "Workflow with context", onEvent: (event) => { // Access retry metadata via events if (event.type === "step_retry") { console.log(`Retry attempt ${event.attempt}/${event.maxAttempts}`); } }, });
export async function runWorkflow() { return await myWorkflow.run(async ({ step, deps, ctx }) => { // ctx contains: workflowId, onEvent, context, signal console.log(`Workflow ID: ${ctx.workflowId}`);
// Check for cancellation if (ctx.signal?.aborted) { return Awaitly.err("CANCELLED"); }
const result = await step('myStep', () => deps.myStep(), { key: "my-step" }); return result; });}export async function myWorkflow() { "use workflow";
// Access workflow metadata const workflowCtx = getWorkflowMetadata(); console.log(`Workflow ID: ${workflowCtx.workflowId}`);
const result = await myStep(); return result;}
async function myStep() { "use step"; // Access step metadata inside step const { attempt } = getStepMetadata(); console.log(`Attempt: ${attempt}`); return "done";}Key difference: Vercel Workflow provides synchronous getters (getStepMetadata(), getWorkflowMetadata()) callable inside steps. awaitly provides WorkflowContext as a callback argument and retry info via onEvent.
Saga Pattern (Distributed Transactions)
Section titled “Saga Pattern (Distributed Transactions)”This is where awaitly shines. Compare manual rollback with automatic compensation.
import { createSagaWorkflow } from "awaitly/saga";
export const orderProcessingSaga = createSagaWorkflow('saga', { reserveInventory, releaseInventory, processPayment, refundPayment, createShippingLabel, }, { onEvent: (event) => { if (event.type === "saga_compensation_start") { console.log(`Rolling back ${event.stepCount} steps...`); } }, });
export async function processOrder(request: OrderRequest) { return await orderProcessingSaga(async ({ saga, deps }) => { await saga.step( "reserve-inventory", () => deps.reserveInventory(request.items), { compensate: async () => { await deps.releaseInventory(request.items); }, } );
const payment = await saga.step( "process-payment", () => deps.processPayment(request.paymentMethod), { compensate: async (p: PaymentResult) => { await deps.refundPayment(p.transactionId, p.amount); }, } );
const label = await saga.step( "create-shipping-label", () => deps.createShippingLabel(request.address), { compensate: async (label: ShippingLabel) => { await cancelShippingLabel(label.trackingNumber); }, } );
return { orderId: "...", payment, label }; });}export async function orderWorkflow(request: OrderRequest) { "use workflow"; let reservation, payment, label;
try { reservation = await reserveInventory(request.items); payment = await processPayment(request.paymentMethod); label = await createShippingLabel(request.address); return { orderId: "...", payment, label }; } catch (error) { // Manual rollback - must track state yourself if (label) await cancelShippingLabel(label.trackingNumber); if (payment) await refundPayment(payment.transactionId); if (reservation) await releaseInventory(request.items); throw error; }}awaitly Saga Compensation Flow──────────────────────────────────────────────────
SUCCESS PATH: reserve ──► payment ──► shipping ──► DONE
FAILURE PATH (shipping fails): reserve ──► payment ──► shipping ✗ │ ◄───────────────┘ auto-compensate │ release ◄── refund ◄── (LIFO order)AI Integration
Section titled “AI Integration”import { streamText } from "ai";import { createWorkflow } from "awaitly/workflow";import { createMemoryStreamStore } from "awaitly/streaming";
const streamStore = createMemoryStreamStore();
export const aiWorkflow = createWorkflow('workflow', { }, { description: "AI workflow", streamStore });
export async function runAIWorkflow(prompt: string) { return await aiWorkflow(async (step) => { const writer = step.getWritable<string>({ namespace: "ai-tokens" });
await step('streamText', () => streamText({ model: openai("gpt-4"), prompt, onChunk: async ({ chunk }) => { if (chunk.type === "text-delta") { await writer.write(chunk.text); } }, }), { key: "stream-text" });
await writer.close(); });}import { DurableAgent } from "@workflow/ai/agent";import { getWritable } from "workflow";
export async function aiWorkflow(messages: UIMessage[]) { "use workflow";
const writable = getWritable<UIMessageChunk>();
const agent = new DurableAgent({ model: "anthropic/claude-4-opus", tools: { getWeather: { description: "Get weather for a city", inputSchema: z.object({ city: z.string() }), execute: getWeatherInfo, }, }, });
await agent.stream({ messages: convertToModelMessages(messages), writable, });}Key difference: Both support streaming. awaitly uses step.getWritable<T>() with pluggable stores (memory, file), Vercel Workflow uses platform getWritable(). Vercel Workflow has DurableAgent with built-in tool registration. awaitly uses any AI SDK which is more flexible but requires manual tool orchestration.
Static Analysis & Visualization
Section titled “Static Analysis & Visualization”awaitly provides tooling that Vercel Workflow doesn’t offer.
Static Analysis
Section titled “Static Analysis”npx awaitly-analyze workflow.tsGenerates Mermaid diagrams showing:
- Workflow structure
- Step dependencies
- Parallel execution
- Conditional logic
- Saga compensation paths
Runtime Visualization
Section titled “Runtime Visualization”import { createVisualizer } from "awaitly-visualizer";
const viz = createVisualizer({ workflowName: "my-workflow" });
const workflow = createWorkflow('workflow', { myDeps }, { onEvent: viz.handleEvent, });
// After executionconsole.log(viz.render()); // ASCII visualizationconsole.log(viz.renderAs("mermaid")); // Mermaid diagramExample ASCII Output────────────────────────────────────────────
my-workflow [completed in 234ms]├─ fetch-user [completed 45ms]├─ validate-order [completed 12ms]├─ charge-card [completed 156ms]│ └─ (retry #1 after 100ms)└─ send-email [completed 21ms]Migration Patterns
Section titled “Migration Patterns”Pattern 1: Simple Sequential Workflow
Section titled “Pattern 1: Simple Sequential Workflow”import { Awaitly, type AsyncResult } from "awaitly";import { createWorkflow } from "awaitly/workflow";
async function add(a: number, b: number): AsyncResult<number, never> { return Awaitly.ok(a + b);}
export const simpleWorkflow = createWorkflow('workflow', { add }, { description: "Simple workflow" });
export async function simple(i: number) { return await simpleWorkflow(async (step, { add }) => { const a = await step('add', () => add(i, 7), { key: "add-1" }); const b = await step('add', () => add(a, 8), { key: "add-2" }); return b; });}async function add(a: number, b: number): Promise<number> { "use step"; return a + b;}
export async function simple(i: number) { "use workflow"; const a = await add(i, 7); const b = await add(a, 8); return b;}Pattern 2: Batch Processing
Section titled “Pattern 2: Batch Processing”import { processInBatches } from "awaitly/batch";
export const batchWorkflow = createWorkflow('workflow', { processItem }, { description: "Batch processing workflow" });
export async function runBatchWorkflow(items: string[]) { return await batchWorkflow(async (step, { processItem }) => { const result = await step('processInBatches', () => processInBatches( items, processItem, { batchSize: 50, onProgress: (progress) => { console.log(`Processed ${progress.processed}/${progress.total}`); }, } ), { key: "batch-process" }); return result; });}export async function batchWorkflow(items: string[]) { "use workflow"; const chunks = chunk(items, 50); for (const batch of chunks) { await Promise.all(batch.map(processItem)); }}When to Choose Each
Section titled “When to Choose Each”Choose Vercel Workflow when:
Section titled “Choose Vercel Workflow when:”- You’re already on Vercel’s platform
- You want zero-config durability (handled by platform)
- You prefer compiler magic over explicit types
- You need tight integration with Vercel services
- You need
DurableAgentfor AI workflows with streaming - You need
createHook()for webhook-based workflow suspension
Choose awaitly when:
Section titled “Choose awaitly when:”- You want maximum type safety with explicit error handling
- You need to run workflows anywhere (not just Vercel)
- You want static analysis and visualization tools
- You need saga pattern support out of the box
- You want standard TypeScript (no custom compiler)
- You need fine-grained control over retry policies
- You want better observability with named steps
Comparison Summary
Section titled “Comparison Summary”┌─────────────────────────────────────────────────────────────┐│ Platform Spectrum │├─────────────────────────────────────────────────────────────┤│ ││ Vercel Workflow ─────────────────────────────► awaitly ││ ││ Platform-integrated Platform-agnostic ││ Compiler magic Explicit types ││ Zero-config durability Bring your own store ││ DurableAgent for AI Any AI SDK ││ createHook() for webhooks Custom via onEvent ││ Vercel only Any Node.js environment ││ │└─────────────────────────────────────────────────────────────┘