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 │
└─────────────────────────┘ └──────────────────────────┘
FeatureVercel Workflowawaitly
Step Definition"use step" directiveReturn AsyncResult<T, E>
Workflow Definition"use workflow" directivecreateWorkflow() factory
Error Handlingthrow Error / RetryableErrorreturn err(error) with TaggedError
Type SafetyBasic TypeScriptEnhanced with Result types
Parallel ExecutionPromise.all()step.all() / step.parallel() with named steps; step.map() for arrays
Race ConditionsPromise.race()step.race() with anyAsync()
Retry LogicRetryableError classRetry options in step config
Saga PatternManual try/catch rollbackcreateSagaWorkflow with auto-compensation
Static AnalysisLimitedawaitly-analyze with Mermaid diagrams
ESLint PluginNoneeslint-plugin-awaitly with autofixes
Runtime VisualizationLimitedawaitly-visualizer with multiple formats (Mermaid, ASCII, JSON)
Platform Lock-inVercel onlyAny Node.js environment
DurabilityBuilt-in (Vercel platform)Configurable (PostgreSQL, Redis, etc.)
Compiler RequiredYes (custom compiler)No (standard TypeScript)
Workflow MetadatagetWorkflowMetadata()WorkflowContext (3rd callback arg)
Step MetadatagetStepMetadata() with attemptonEvent callback with retry events
Hooks/WebhookscreateHook() with suspensionCustom implementation via onEvent
AI AgentsDurableAgent classAny AI library e.g. AI SDK, Tanstack AI, Langchain etc
Sleepsleep("5s") built-instep.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 { 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;
});
}
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.parallel("Fetch all data", {
user: () => deps.fetchUser(id),
posts: () => deps.fetchPosts(id),
comments: () => deps.fetchComments(id),
});
return { user, posts, comments };
});
}

Key difference: awaitly’s step.parallel() 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.

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 };
});
}
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 │
│ │
└─────────────────────────────────────────────────────────────┘