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.
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 app/api/orders/[id]/route.ts:
import { start } from "workflow/api";
import { processOrder } from "@/workflows/order";
export async function POST(
_request: Request,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const run = await start(processOrder, [id]);
return Response.json({ runId: run.runId });
}
Hono has no build system of its own, so use Nitro to compile the workflow and serve its handler in the same process.
Create nitro.config.ts:
import { defineConfig } from "nitro";
export default defineConfig({
modules: ["workflow/nitro"],
routes: {
"/**": "./src/server.ts",
},
});
Create src/server.ts:
import { Hono } from "hono";
import { getRun, start } from "workflow/api";
import { processOrder } from "../workflows/order.ts";
const app = new Hono()
.post("/orders/:id", async (c) => {
const run = await start(processOrder, [c.req.param("id")]);
return c.json({ runId: run.runId });
})
.get("/orders/:runId", async (c) => {
const run = getRun(c.req.param("runId"));
return c.json({ status: await run.status });
});
export default app;
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
| Variable | Required | Purpose |
|---|---|---|
WORKFLOW_TARGET_WORLD | Yes | Loads @rivet-dev/vercel-world through Vercel Workflows |
WORKFLOW_RUNTIME_URL | Yes | Externally reachable base URL of the Workflows HTTP server |
WORKFLOW_QUEUE_NAMESPACE | No | Shared queue namespace used by Workflows and crash-safe initial dispatch |
RIVET_ENDPOINT | Remote only | Rivet Engine endpoint |
RIVET_NAMESPACE | Remote only | Namespace containing the World actors |
RIVET_POOL | Remote only | Pool that hosts the native World registry |
RIVET_TOKEN | Cloud only | Token sent to Rivet |
RIVET_WORKFLOW_SECRET | Recommended when public | Shared 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.