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

Coding Agents

Workflows

Realtime

Databases

Games

Concepts

Agent Tools

Agent Coordination

Agent Clients

Connectors

Last updated November 10, 2026