Skip to main content

Rivet

Run durable Actors, workflows, sandboxes, and generated apps on one control plane.

  1. 1
    Install
    npm install rivetkit
  2. 2
    Define an Actor index.ts
    import { actor, setup } from "rivetkit";
    import { db } from "rivetkit/db";
    
    // A chat room Actor. Every room key gets its own instance with its own SQLite
    // database, which survives restarts and redeploys.
    export const chatRoom = actor({
    	// Create the messages table the first time a room starts.
    	db: db({
    		onMigrate: async (db) => {
    			await db.execute(`
    				CREATE TABLE IF NOT EXISTS messages (
    					id INTEGER PRIMARY KEY AUTOINCREMENT,
    					sender TEXT NOT NULL,
    					text TEXT NOT NULL
    				)
    			`);
    		},
    	}),
    
    	actions: {
    		// Save a message, then push it to everyone connected to this room.
    		sendMessage: async (c, sender: string, text: string) => {
    			await c.db.execute(
    				"INSERT INTO messages (sender, text) VALUES (?, ?)",
    				sender,
    				text,
    			);
    
    			c.broadcast("newMessage", { sender, text });
    		},
    
    		// Read the room's history, oldest first.
    		getHistory: async (c) => {
    			return (await c.db.execute(
    				"SELECT sender, text FROM messages ORDER BY id",
    			)) as { sender: string; text: string }[];
    		},
    	},
    });
    
    // Register the Actor and start serving it on http://localhost:6420.
    export const registry = setup({ use: { chatRoom } });
    registry.start();
  3. 3
    Talk to it from a client client.ts
    import { createClient } from "rivetkit/client";
    import type { registry } from "./index";
    
    // Connect to the server from index.ts. The registry type checks every call.
    const client = createClient<typeof registry>("http://localhost:6420");
    
    // Get the room keyed "general", creating it on first use.
    const room = client.chatRoom.getOrCreate(["general"]);
    
    // Open a realtime connection and print each message as it arrives.
    const connection = room.connect();
    connection.on("newMessage", (message: { sender: string; text: string }) => {
    	console.log(`${message.sender}: ${message.text}`);
    });
    
    // Send a message. The Actor saves it to SQLite before broadcasting it.
    await connection.sendMessage("alice", "Hello, Rivet!");
    
    // The history is still there after the Actor sleeps or restarts.
    console.log(await room.getHistory());
  1. 1
    Install
    npm install @rivet-dev/pi @earendil-works/pi-durable rivetkit
  2. 2
    Define the agent server.ts
    import { createRegistry, defineExtension } from "@earendil-works/pi-durable";
    import { createEditTool, createReadTool, createWriteTool } from "@earendil-works/pi-durable/tools";
    import { pi } from "@rivet-dev/pi";
    import { setup } from "rivetkit";
    
    // read, write, and edit. With no sandbox, the files live in the Actor's own database.
    const extensions = createRegistry();
    extensions.install(defineExtension({ name: "files", tools: [createReadTool(), createWriteTool(), createEditTool()] }));
    
    const agent = pi({
    	model: "anthropic/claude-opus-5-5",
    	registry: extensions,
    });
    
    export const registry = setup({ use: { agent } });
    
    registry.start();
  3. 3
    Send a prompt client.ts
    import { createClient } from "rivetkit/client";
    import type { registry } from "./server";
    
    const client = createClient<typeof registry>();
    const agent = client.agent.getOrCreate(["user-123"]);
    
    const result = await agent.prompt(
    	"Write a hello-world Rivet Actor to counter.ts, then read the file back to me.",
    	{ requestId: crypto.randomUUID() },
    );
    
    console.log(result.status === "done" ? result.text : `Unanswered: ${result.reason}`);
  1. 1
    Install
    npm install rivetkit @rivet-dev/agentos-core
  2. 2
    Give the Actor a VM actor.ts
    import { actor, setup } from "rivetkit";
    import { AgentOs } from "@rivet-dev/agentos-core";
    
    export const sandbox = actor({
      // One VM per Actor. It boots with the Actor and is released
      // when the Actor sleeps.
      createVars: async () => ({ vm: await AgentOs.create() }),
      onSleep: async (c) => {
        await c.vars.vm.dispose();
      },
      onDestroy: async (c) => {
        await c.vars.vm.dispose();
      },
    
      actions: {
        // `code` came from a model. It runs in the VM,
        // not in your process.
        run: async (c, code: string) => {
          const result = await c.vars.vm.javascript.evaluate(code);
          if (result.outcome !== "succeeded") {
            throw new Error(result.error.message);
          }
          return result.value;
        },
      },
    });
    
    export const registry = setup({
      use: { sandbox },
    });
  3. 3
    Run code in it from a client client.ts
    import { createClient } from "rivetkit/client";
    import type { registry } from "./actor";
    
    const client = createClient<typeof registry>("http://localhost:8080");
    
    // One Actor per agent session, so each session has its own VM.
    const sandbox = client.sandbox.getOrCreate(["session-123"]);
    
    const value = await sandbox.run("[1, 2, 3].map((n) => n * 2)");
    console.log(value); // [2, 4, 6]
  1. 1
    Install
    npm install @rivet-dev/pi @rivet-dev/sandbox-adapter @earendil-works/pi-durable rivetkit
  2. 2
    Let the sandbox call the agent callback.ts
    // Outside the workspace, so the agent doesn't commit it.
    export const SCRIPT_PATH = "/tmp/call-tool.sh";
    
    // The maximum. The script is rewritten after every wake.
    const TOKEN_TTL_SECONDS = 24 * 60 * 60;
    
    // Writes the script into every sandbox the agent connects to.
    export function withCallbackScript(provider: SandboxProvider): SandboxProvider {
    	return {
    		...provider,
    		connect: async (c, id) => {
    			const sandbox = await provider.connect(c, id);
    			if (sandbox) await writeScript(sandbox, c);
    			return sandbox;
    		},
    	};
    }
    
    async function writeScript(sandbox: Sandbox, c: SandboxActorContext): Promise<void> {
    	// The agent's own client uses your Rivet token. The scoped token reaches only
    	// this Actor, but can call every action on it.
    	const client = c.client() as Client<typeof registry>;
    	const { token } = await client.agent.getForId(c.actorId).issueToken({ subject: "sandbox", expiresIn: TOKEN_TTL_SECONDS });
    	await sandbox.writeFile(SCRIPT_PATH, callbackScript(`${gatewayEndpoint()}/gateway/${c.actorId}`, token));
    	const chmod = await sandbox.exec(`chmod 700 ${SCRIPT_PATH}`, { cwd: sandbox.cwd });
    	if (chmod.exitCode !== 0) throw new Error(`Installing ${SCRIPT_PATH} failed. ${chmod.stderr}`);
    }
  3. 3
    Add the tools the code can call server.ts
    export const agent = pi({
    	model: "anthropic/claude-opus-5-5",
    	registry: extensions,
    	// Writes call-tool.sh into the sandbox. The image has curl, Node.js, and Python with numpy.
    	// Docker is only for local development, where the sandbox can reach the control plane
    	// on this machine. In production, switch to a hosted sandbox provider such as Daytona
    	// or E2B, and set RIVET_PUBLIC_ENDPOINT to an address the sandbox can reach.
    	sandbox: withCallbackScript(dockerProvider({ image: "sandbox-code-mode" })),
    	state: { notes: [] as string[] },
    	events: {
    		progress: event<string>(),
    	},
    	// The tools call-tool.sh calls. Each one is an action on the agent.
    	actions: {
    		progress: (c, message: unknown) => {
    			c.broadcast("progress", requireText(message));
    			return null;
    		},
    		saveNote: (c, text: unknown) => {
    			c.state.notes.push(requireText(text));
    			return { saved: c.state.notes.length };
    		},
    		listNotes: (c) => c.state.notes,
    		// In a real app, this queries your metrics database with your keys.
    		getReadings: (_c, sensor: unknown, cursor: unknown) => readingsPage(sensor, cursor),
    	},
    });
    
    function requireText(value: unknown): string {
    	if (typeof value !== "string" || value.length === 0) throw new UserError("Pass a non-empty string.");
    	return value;
    }
  1. 1
    Install
    npm install @rivet-dev/workflows rivetkit
  2. 2
    Define a workflow index.ts
    import {
    	setup,
    	type WorkflowStepContextOf,
    	workflow,
    } from "@rivet-dev/workflows";
    import { db } from "rivetkit/db";
    
    // An order workflow, one instance per order. Each step runs exactly once:
    // after a crash or redeploy, the workflow replays and skips finished steps.
    export const order = workflow({
    	// Each order gets its own SQLite database to record its progress.
    	db: db({
    		onMigrate: async (db) => {
    			await db.execute(`
    				CREATE TABLE IF NOT EXISTS progress (
    					id INTEGER PRIMARY KEY AUTOINCREMENT,
    					step TEXT NOT NULL
    				)
    			`);
    		},
    	}),
    
    	run: async (ctx) => {
    		// Reserve stock, then record that the step finished.
    		await ctx.step("reserve", (step) => record(step, "reserve"));
    
    		// Wait durably. The workflow keeps its place across restarts.
    		await ctx.sleep("wait-for-payment", 1_000);
    
    		// Charge the card and send the receipt, recording each step.
    		await ctx.step("charge", (step) => record(step, "charge"));
    		await ctx.step("receipt", (step) => record(step, "receipt"));
    	},
    
    	actions: {
    		// Read which steps have finished so far.
    		getProgress: async (c) => {
    			return (await c.db.execute("SELECT step FROM progress ORDER BY id")) as {
    				step: string;
    			}[];
    		},
    	},
    });
    
    // Write one progress row to this order's database.
    async function record(step: WorkflowStepContextOf<typeof order>, name: string) {
    	await step.db.execute("INSERT INTO progress (step) VALUES (?)", name);
    }
    
    // Register the workflow and start serving it on http://localhost:6420.
    export const registry = setup({ use: { order } });
    registry.start();
  3. 3
    Start it from a client client.ts
    import { createClient } from "rivetkit/client";
    import type { registry } from "./index";
    
    // Connect to the server from index.ts. The registry type checks every call.
    const client = createClient<typeof registry>("http://localhost:6420");
    
    // Get the order keyed "order-123". Creating it starts its workflow.
    const order = client.order.getOrCreate(["order-123"]);
    
    // Give the workflow a moment to run, then read the steps it has finished.
    await new Promise((resolve) => setTimeout(resolve, 2_000));
    console.log(await order.getProgress());
  1. 1
    Install
    npm install rivetkit @rivetkit/react
  2. 2
    Define a room that broadcasts src/index.ts
    import { actor, event, setup } from "rivetkit";
    import { db } from "rivetkit/db";
    
    export type Message = { sender: string; text: string; timestamp: number };
    
    export const chatRoom = actor({
    	// Persist chat history in the actor's SQLite database: https://rivet.dev/docs/actors/sqlite
    	db: db({
    		onMigrate: async (db) => {
    			await db.execute(`
    				CREATE TABLE IF NOT EXISTS messages (
    					id INTEGER PRIMARY KEY AUTOINCREMENT,
    					sender TEXT NOT NULL,
    					text TEXT NOT NULL,
    					timestamp INTEGER NOT NULL
    				)
    			`);
    		},
    	}),
    	events: {
    		newMessage: event<Message>(),
    	},
    
    	actions: {
    		// Callable functions from clients: https://rivet.dev/docs/actors/actions
    		sendMessage: async (c, sender: string, text: string) => {
    			const message: Message = { sender, text, timestamp: Date.now() };
    			await c.db.execute(
    				"INSERT INTO messages (sender, text, timestamp) VALUES (?, ?, ?)",
    				sender,
    				text,
    				message.timestamp,
    			);
    			// Send events to all connected clients: https://rivet.dev/docs/actors/events
    			c.broadcast("newMessage", message);
    			return message;
    		},
    
    		getHistory: async (c) => {
    			const rows = await c.db.execute(
    				"SELECT sender, text, timestamp FROM messages ORDER BY id ASC",
    			);
    			return rows as Message[];
    		},
    	},
    });
    
    // Register actors for use: https://rivet.dev/docs/setup
    export const registry = setup({
    	use: { chatRoom },
    });
    
    // Start the server on port 6420
    registry.start();
  3. 3
    Connect from React App.tsx
    const { useActor } = createRivetKit<typeof registry>("http://localhost:6420");
    
    export function App() {
    	const [roomId, setRoomId] = useState("general");
    	const [username, setUsername] = useState("User");
    	const [input, setInput] = useState("");
    	const [messages, setMessages] = useState<Message[]>([]);
    
    	const chatRoom = useActor({
    		name: "chatRoom",
    		key: [roomId],
    	});
    
    	useEffect(() => {
    		if (chatRoom.connection) {
    			chatRoom.connection.getHistory().then(setMessages);
    		}
    	}, [chatRoom.connection]);
    
    	chatRoom.useEvent("newMessage", (message: Message) => {
    		setMessages((prev) => [...prev, message]);
    	});
    
    	const sendMessage = async () => {
    		if (chatRoom.connection && input.trim()) {
    			await chatRoom.connection.sendMessage(username, input);
    			setInput("");
    		}
    	};

Once it runs

Guides

Agents

Personal Agent over SMS

Give each user their own agent that they sign in to with their phone number and talk to by text message.

Durable AI Agents

Run an agent whose conversation, in-flight tool calls, and plan survive crashes, deploys, and restarts.

Multi-Agent Systems

Split a large task across an orchestrator agent and specialist agents that work in parallel.

AI Agent

Build an AI agent backend with persistent memory: one Rivet Actor per conversation, ordered and durable message handling, and streaming LLM responses as realtime events.

Side Questions (/btw)

Ask a quick question about an agent's work by forking the agent, asking the fork, and discarding it, without interrupting the agent or adding to its conversation.

Branch Conversations (/fork)

Fork an agent into a new thread that keeps the full history up to the fork, keep working in it, and let the original thread continue separately.

Forking Subagents

Fork an agent once per subtask so every subagent starts with the full context, have each one report back, and combine the reports in the parent.

Agent Swarms

Fan an agent out to many forks at once, such as one per file in a repository, collect a one-line review from each fork, and summarize them.

Sign in with ChatGPT

Let users connect their ChatGPT plan so your agents run on it, with no OpenAI API key.

Code Mode in the Sandbox

Let programs the agent writes in its sandbox call your backend tools, authenticated with a scoped JWT.

Agent Design Patterns

Patterns for agent keys, subagents, agent-to-agent messages, workflows, schedules, background jobs, and shared credentials.

Trace Agent Runs with OpenTelemetry

Trace every agent run with OpenTelemetry.

Coding Agents

Workflows

Realtime

Databases

Games

Concepts

Agent Tools

Agent Coordination

Agent Clients

Connectors

Last updated November 11, 2026