Skip to main content
Workflows

Vercel Workflows (Beta)

Vercel Workflows backed by Rivet Actors.

This integration is in beta. APIs may change between releases.

@rivet-dev/vercel-world implements the Vercel World API with native Rivet Actors.

View the complete example →

Quickstart

Create a project

Set up a Vercel Workflows project for your framework on Node.js 22 or newer. The Workflows getting-started guides cover Next.js, Astro, Express, Fastify, Hono, Nitro, Nuxt, SvelteKit, TanStack Start, and Vite.

Install the World

npm install workflow @rivet-dev/vercel-world

Write a workflow

Create workflows/order.ts:

import { sleep } from "workflow";

export async function processOrder(id: string) {
	"use workflow";

	const reserved = await reserveInventory(id);
	await sleep("1 hour");
	return chargeOrder(reserved);
}

async function reserveInventory(id: string) {
	"use step";
	return { id, reservationId: `reservation-${id}` };
}

async function chargeOrder(order: { id: string; reservationId: string }) {
	"use step";
	return { ...order, status: "charged" as const };
}

Serve Workflow and Rivet together

The World starts Rivet lazily in the Workflows process. You do not need a second server or a framework instrumentation hook.

Create .env with the variables under Configuration, then build and run:

npm run build
npm run dev

Validate the workflow

Start a run:

curl -X POST http://localhost:3000/orders/42

Use Vercel’s Workflow Vitest harness. The first World operation starts the native Rivet registry and Engine in the test process:

npm test

Configuration

Select the World and configure its Rivet connection in the application process:

WORKFLOW_TARGET_WORLD=@rivet-dev/vercel-world
WORKFLOW_RUNTIME_URL=http://127.0.0.1:3000
VariableRequiredPurpose
WORKFLOW_TARGET_WORLDYesLoads @rivet-dev/vercel-world through Vercel Workflows
WORKFLOW_RUNTIME_URLYesExternally reachable base URL of the Workflows HTTP server
WORKFLOW_QUEUE_NAMESPACENoShared queue namespace used by Workflows and crash-safe initial dispatch
RIVET_ENDPOINTRemote onlyRivet Engine endpoint
RIVET_NAMESPACERemote onlyNamespace containing the World actors
RIVET_POOLRemote onlyPool that hosts the native World registry
RIVET_TOKENCloud onlyToken sent to Rivet
RIVET_WORKFLOW_SECRETRecommended when publicShared bearer secret for World-to-runtime delivery

Local development starts the native Rivet Engine automatically. For production, configure the remote Rivet connection. Run the combined server at WORKFLOW_RUNTIME_URL; its World client and native registry use the same Rivet endpoint, namespace, and pool.

HTTP routes

Your framework integration serves the combined workflow handler at .well-known/workflow/v1/flow. WORKFLOW_RUNTIME_URL must resolve to the service hosting that route. Do not point it at the Rivet Engine. If RIVET_WORKFLOW_SECRET is set, delivery carries that value as a bearer token and rejects requests without it.

Durability

The World stores runs, event logs, queues, streams, hook tokens, and recovery alarms in Rivet Actors.

Recovery is local to each run; startup does not scan all actors. Queue an initial workflow with the default namespace or WORKFLOW_QUEUE_NAMESPACE. The current Vercel Workflows does not include a per-call start({ namespace }) value in the run_created event, so that per-call override cannot be reconstructed after a crash and is not supported by this World.

Application code continues to use "use workflow", "use step", workflow/api, hooks, sleeps, and streams exactly as documented by Vercel.

Testing

import { workflow } from "@workflow/vitest";
import { defineConfig } from "vitest/config";

export default defineConfig({
	plugins: [workflow()],
	test: {
		include: ["workflows/**/*.integration.test.ts"],
		setupFiles: ["./vitest.setup.ts"],
		testTimeout: 60_000,
	},
});
import { waitForSleep } from "@workflow/vitest";
import { expect, test } from "vitest";
import { getRun, start } from "workflow/api";

import { processOrder } from "./order.ts";

test("runs the workflow end to end", async () => {
	const run = await start(processOrder, ["42"]);
	const sleepId = await waitForSleep(run);

	await getRun(run.runId).wakeUp({ correlationIds: [sleepId] });

	await expect(run.returnValue).resolves.toEqual({
		id: "42",
		reservationId: "reservation-42",
		status: "charged",
	});
	expect(await run.status).toBe("completed");
});

waitForSleep observes the durable sleep, wakeUp resumes that exact correlation, and the assertions wait for the final persisted result.

See the Vercel Workflows documentation for the SDK itself.