Skip to content

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.

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 │
└─────────────────────────┘ └──────────────────────────┘
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.all() 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”)
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);
}

Key difference: awaitly makes error types explicit in the function signature. The compiler tracks what can fail.

import { run } from 'awaitly/run';
import { type ErrorsOf } from 'awaitly';
const deps = { fetchUser };
type Errors = ErrorsOf<typeof deps>;
export async function runWorkflow(id: string) {
return await run<User, Errors>(async ({ step }) => {
const user = await step('fetchUser', () => fetchUser(id));
return user;
});
}
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 errors
const payment = await step('processPayment', () => processPayment(amount), {
key: "process-payment",
retry: {
attempts: 3,
backoff: "exponential",
initialDelay: 100,
},
});
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.all("Fetch all data", {
user: () => deps.fetchUser(id),
posts: () => deps.fetchPosts(id),
comments: () => deps.fetchComments(id),
});
return { user, posts, comments };
});
}

Key difference: awaitly’s step.all() provides named steps for better observability and type inference.

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

Key difference: Vercel Workflow provides synchronous getters (getStepMetadata(), getWorkflowMetadata()) callable inside steps. awaitly provides WorkflowContext as a callback argument and retry info via onEvent.

Compare manual rollback with automatic compensation.

import { createSagaWorkflow } from "awaitly/saga";
export const orderProcessingSaga = createSagaWorkflow('saga', {
reserveInventory,
releaseInventory,
processPayment,
refundPayment,
createShippingLabel,
});
export async function processOrder(request: OrderRequest) {
return await orderProcessingSaga.run(async ({ step, deps }) => {
await step(
"reserve-inventory",
() => deps.reserveInventory(request.items),
{
compensate: async () => {
await deps.releaseInventory(request.items);
},
}
);
const payment = await step(
"process-payment",
() => deps.processPayment(request.paymentMethod),
{
compensate: async (p: PaymentResult) => {
await deps.refundPayment(p.transactionId, p.amount);
},
}
);
const label = await step(
"create-shipping-label",
() => deps.createShippingLabel(request.address),
{
compensate: async (label: ShippingLabel) => {
await cancelShippingLabel(label.trackingNumber);
},
}
);
return { orderId: "...", payment, label };
});
}
awaitly Saga Compensation Flow
──────────────────────────────────────────────────
SUCCESS PATH:
reserve ──► payment ──► shipping ──► DONE
FAILURE PATH (shipping fails):
reserve ──► payment ──► shipping ✗
◄───────────────┘ auto-compensate
release ◄── refund ◄── (LIFO order)
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();
});
}

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.

awaitly provides tooling that Vercel Workflow doesn’t offer.

Terminal window
npx awaitly-analyze workflow.ts

Generates Mermaid diagrams showing:

  • Workflow structure
  • Step dependencies
  • Parallel execution
  • Conditional logic
  • Saga compensation paths
import { createVisualizer } from "awaitly-visualizer";
const viz = createVisualizer({ workflowName: "my-workflow" });
const workflow = createWorkflow('workflow', { myDeps },
{
onEvent: viz.handleEvent,
}
);
// After execution
console.log(viz.render()); // ASCII visualization
console.log(viz.renderAs("mermaid")); // Mermaid diagram
Example 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]
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;
});
}
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;
});
}
  • 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 DurableAgent for AI workflows with streaming
  • You need createHook() for webhook-based workflow suspension
  • 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
┌─────────────────────────────────────────────────────────────┐
│ 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 │
│ │
└─────────────────────────────────────────────────────────────┘