Skip to main content
Workflows

Workflow SDK (Beta)

Vercel's Workflow SDK backed by Rivet Actors.

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

@rivet-dev/workflow-world implements the Workflow SDK’s World API with native Rivet Actors.

View the complete example →

Quickstart

Create a project

Set up a Workflow SDK project for your framework on Node.js 22 or newer. The 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/workflow-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 the Workflow SDK and Rivet together

The World starts Rivet lazily in the Workflow SDK 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 the Workflow SDK’s Vitest harness. The first World operation starts the native Rivet registry and control plane in the test process:

npm test

Configuration

Select the World and point it at your own HTTP server:

WORKFLOW_TARGET_WORLD=@rivet-dev/workflow-world
WORKFLOW_RUNTIME_URL=http://127.0.0.1:3000

# The Rivet connection uses the standard RivetKit environment variables. Local
# development needs none of them.
VariableRequiredPurpose
WORKFLOW_TARGET_WORLDYesLoads @rivet-dev/workflow-world as the World
WORKFLOW_RUNTIME_URLYesExternally reachable base URL of your Workflow SDK HTTP server
WORKFLOW_QUEUE_NAMESPACENoShared queue namespace used by the Workflow SDK and crash-safe initial dispatch
RIVET_WORKFLOW_SECRETRecommended when publicShared bearer secret for World-to-runtime delivery

The World does not read the Rivet connection itself. It hands configuration to RivetKit, so the standard RivetKit environment variables apply unchanged. Local development starts the control plane automatically and needs none of them; set them to run against a remote control plane.

Run the combined server at WORKFLOW_RUNTIME_URL; its World client and native registry use the same 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 control plane. If RIVET_WORKFLOW_SECRET is set, delivery carries that value as a bearer token and rejects requests without it.

Deploying

Deploy the app as a Rivet worker like any other. See Self-Host for the per-platform guides. Two constraints come from this World specifically:

  • The process must be long-lived. The first World operation opens a persistent worker connection and waits for it. This World does not use RivetKit’s serverless request handler, so a host that only runs per-request functions cannot serve it.
  • WORKFLOW_RUNTIME_URL must be reachable from your workers, not just from browsers. The dispatcher calls it to run every queued step. Point it at the app’s load-balanced base URL when running more than one replica, and secure the route as described in HTTP routes once it is publicly reachable.

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 Workflow SDK 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 the Workflow SDK.

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 Workflow SDK documentation for the SDK itself.