Rivet
Run durable Actors, workflows, sandboxes, and generated apps on one control plane.
- 1 Install
npm install rivetkit - 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 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 Install
npm install @rivet-dev/pi @earendil-works/pi-durable rivetkit - 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 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 Install
npm install rivetkit @rivet-dev/agentos-core - 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 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 Install
npm install @rivet-dev/pi @rivet-dev/sandbox-adapter @earendil-works/pi-durable rivetkit - 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 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 Install
npm install @rivet-dev/workflows rivetkit - 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 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 Install
npm install rivetkit @rivetkit/react - 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 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
Give each user their own agent that they sign in to with their phone number and talk to by text message.
Run an agent whose conversation, in-flight tool calls, and plan survive crashes, deploys, and restarts.
Split a large task across an orchestrator agent and specialist agents that work in parallel.
AI AgentBuild an AI agent backend with persistent memory: one Rivet Actor per conversation, ordered and durable message handling, and streaming LLM responses as realtime events.
Let users connect their ChatGPT plan so your agents run on it, with no OpenAI API key.
Let programs the agent writes in its sandbox call your backend tools, authenticated with a scoped JWT.
Agent Design PatternsPatterns for agent keys, subagents, agent-to-agent messages, workflows, schedules, background jobs, and shared credentials.
Trace Agent Runs with OpenTelemetryTrace every agent run with OpenTelemetry.
Coding Agents
Run Claude Code in a cloud sandbox from your app, one session per Rivet Actor, and stream its output as it works.
Run the OpenAI Codex CLI in a cloud sandbox from your app, one session per Rivet Actor, and stream its output as it works.
Run OpenCode in a cloud sandbox from your app, one session per Rivet Actor, and stream its output as it works.
Run the Amp CLI in a cloud sandbox from your app, one session per Rivet Actor, and stream its output as it works.
Give an Agent a SandboxRun an agent without a sandbox, or give it a shell in agentOS, E2B, Daytona, Freestyle, Docker, or a sandbox provider you write.
Use one API to create and drive sandboxes on agentOS, E2B, Daytona, Freestyle, or Docker from any Rivet Actor, without the agent harness.
Workflows
Pause a workflow or an agent's tool call until a person approves or rejects it, even if the answer takes days.
Chain LLM calls and tool calls into one job that picks up at the failed step after a crash or a rate-limit error.
Accept webhooks from Stripe, GitHub, or Shopify, process each event once, and keep events for the same customer in order.
Run a daily report on a cron schedule inside a Rivet Actor, then add one-shot and fixed-interval jobs that survive sleep, restarts, and deploys.
Send your app's events to customer endpoints in order, retry failed deliveries, and turn off endpoints that keep failing.
Realtime
Build a realtime chat room backend with Rivet Actors: one actor per room, SQLite-backed message history, and WebSocket broadcast to every connected client.
Build a collaborative text editor such as Google Docs or Notion with Yjs, where one Actor per document relays edits and presence and saves the document.
Build a collaborative text editor with operational transformation, where one Actor per document orders every edit and keeps the operation log.
Build live cursors and presence for a shared canvas, where one room Actor tracks who is connected and broadcasts every cursor move.
Databases
Games
Build a battle royale game like Fortnite, Apex Legends, or PUBG, where players fill a lobby and fight in a shrinking zone until one is left.
Build an IO game like Agar.io or Slither.io, where players drop into an open lobby at any time and the server moves everyone.
Build an arena game such as a team deathmatch, a free-for-all, or a Rocket League style match, where players queue for a mode and play a fixed-size match.
Build a turn-based game such as correspondence chess, where a match Actor validates every move and keeps the board between turns.
Build a Minecraft-style open world that is split into chunks, with one Rivet Actor per chunk and players walking between them.
Build a party game with private lobbies, where a host creates a room, friends join with a party code, and the host starts the game.
Build a ranked queue that pairs players by rating, runs each duel in its own match Actor, and updates ratings and a leaderboard when it ends.
Build an idle game like Cookie Clicker, where each player's world Actor keeps producing resources while they are offline and a shared leaderboard ranks everyone.
Build a 2D multiplayer physics game where a Rivet Actor runs Rapier on the server and streams every body's position to the players.
Build a 3D multiplayer physics game where a Rivet Actor runs Rapier on the server and players render the world with three.js.
Concepts
Log users in on your backend, mint a JWT scoped to each user's Rivet Actor, connect from the browser with it, and authorize actions inside the Actor.
Why actors eliminate complexity instead of managing it, and how merging state and compute removes the biggest source of latency in modern applications.
Agent Tools
Give an agent tools that run in your backend, with access to your APIs and secrets.
Give an Agent File and Shell ToolsPi's file and shell tools, where they run with and without a sandbox, and how to choose them.
Bundle Tools and Hooks into ExtensionsBundle tools, prompt sections, hooks, and tasks into named extensions, and choose which ones each conversation uses.
Build an Agent's System PromptBuild an agent's system prompt from sections, load AGENTS.md from the workspace, and add instructions to one conversation or context to one prompt.
Give an Agent SkillsGive an agent instructions for specific tasks that it loads only when a task needs them.
Human in the LoopMake a tool wait for a person's approval before it acts.
Scoped Access with JWTsLet a tool publish a result and share it through a link that reaches one Actor and expires.
Agent Coordination
Let an agent hand a focused task to a specialist agent with its own tools.
Agent-to-Agent MessagesLet one agent hand work to another agent without waiting for it.
Run an Agent as a Workflow StepRun agents as steps in durable, multi-step workflows.
Run an Agent on a ScheduleWake agents on a schedule to run work while nobody is watching.
Agent Clients
Connectors
Talk to an agent by mentioning it in a Slack channel or thread.
Connect an Agent to LinearMention an agent in a Linear issue comment.
Connect an Agent to GitHubMention an agent in a GitHub issue or pull request comment.
Connect an Agent to DiscordTalk to an agent with a slash command in Discord.