Skip to main content
Concepts

Patterns

Apply production workflow patterns for progress tracking, queue-driven cron jobs, orchestration, and reliable multi-step operations.

These are common workflow shapes used in production systems.

Store workflow progress in state + broadcast

Store progress in state so replay and recovery always restore it. Broadcast state changes so clients can render progress in realtime.

Cron (queue-driven)

Rivet scheduling triggers actions. For cron-like workflows, use a small scheduled action as a bridge that enqueues work, then process that work in the workflow loop.

import {
	queue,
	type ScheduledFireInfo,
	setup,
	workflow,
} from "@rivet-dev/workflows";
export const cronActor = workflow({
	state: {
		runs: 0,
		lastRunAt: null as number | null,
	},
	queues: {
		"cron-tick": queue<{
			scheduledAt: number;
		}>(),
	},
	onCreate: async (c) => {
		await c.cron.every({
			name: "workflow-tick",
			interval: 60000,
			action: "enqueueCronTick",
			args: [],
			maxHistory: 100,
		});
	},
	actions: {
		enqueueCronTick: async (c, fire: ScheduledFireInfo) => {
			await c.queue.send("cron-tick", { scheduledAt: fire.scheduledAt });
		},
		getState: (c) => c.state,
	},
	run: async (ctx) => {
		await ctx.loop("cron-loop", async (loopCtx) => {
			const message = await loopCtx.queue.next("wait-cron-tick");
			await loopCtx.step("run-cron-job", async (step) => {
				step.state.runs += 1;
				step.state.lastRunAt = message.body.scheduledAt;
			});
		});
	},
});
export const registry = setup({ use: { cronActor } });

Setup & teardown

Use this when you need one-time initialization before a long-lived loop, plus cleanup when the actor stops sleeping or is destroyed.

import { setup, workflow } from "@rivet-dev/workflows";

function openResource(): string {
	return "connected";
}
function closeResource(_resource: string): void {}
export const setupRunTeardownActor = workflow({
	vars: {
		resource: null as string | null,
	},
	state: {
		initialized: false,
		ticks: 0,
	},
	onWake: (c) => {
		c.vars.resource = openResource();
	},
	onSleep: (c) => {
		if (!c.vars.resource) return;
		closeResource(c.vars.resource);
		c.vars.resource = null;
	},
	run: async (ctx) => {
		await ctx.step("setup", async (step) => {
			if (!step.vars.resource) step.vars.resource = openResource();
			step.state.initialized = true;
		});
		await ctx.loop("main-loop", async (loopCtx) => {
			await loopCtx.sleep("tick", 1000);
			await loopCtx.step("tick-step", async (step) => {
				step.state.ticks += 1;
			});
		});
	},
	actions: {
		getState: (c) => c.state,
	},
});
export const registry = setup({ use: { setupRunTeardownActor } });