Skip to main content
Core

Design Patterns

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

Each agent is an Actor, so agent apps use the same building blocks as the rest of Rivet: keys pick the agent, and agents call other Actors. These patterns build on the Quickstart agent.

One Agent per Key

The key decides which conversation a prompt continues. Pick it from what the agent should remember:

  • Per user: one agent remembers everything a user has asked.
  • Per thread: each conversation, such as a Slack thread, gets its own session.
  • Per task: each job starts fresh. To delete the agent afterwards, give it an action that calls c.destroy(), like the researchers below.
[“user”, id]Agentremembers the usergetOrCreate[“thread”, id]Agentone conversationgetOrCreate[“task”, id]Agentone jobgetOrCreate
import { createClient } from "rivetkit/client";
import type { registry } from "../quickstart/server";

const client = createClient<typeof registry>();

export function agentsFor(userId: string, threadId: string, taskId: string) {
	return {
		user: client.agent.getOrCreate(["user", userId]),
		thread: client.agent.getOrCreate(["thread", threadId]),
		task: client.agent.getOrCreate(["task", taskId]),
	};
}

Each key also gets its own sandbox, so files don’t leak between users, threads, or tasks.

Lead Agent with Subagents

A lead agent splits a question into parts and gives each part to a new agent, then waits for their answers. Each researcher is destroyed once its answer is read.

Lead agentResearcherResearcherResearcherpromptanswer, finish()
import { createClient } from "rivetkit/client";
import type { registry } from "../subagents/server";

const client = createClient<typeof registry>();

export async function ask(question: string) {
	const researcher = client.researcher.getOrCreate([crypto.randomUUID()]);
	await researcher.prompt(question);
	const answer = await researcher.getLastAssistantText();
	await researcher.finish();
	return answer;
}

See Subagents for the full example.

Agents Messaging Each Other

When an agent hands work off and doesn’t need the answer, it sends a message instead of waiting. The receiving agent schedules its own prompt, so the message survives the sender going away.

Coder agentReviewer agentkey: repositoryrequestReview()returns right away
import { pi } from "@rivet-dev/pi";
import type { Registry } from "rivetkit";
import { createClient } from "rivetkit/client";

export const reviewer = pi({
	model: "anthropic/claude-opus-5-5",
	actions: {
		requestReview: async (c, request: string) => {
			await c.schedule.after(0, "prompt", request, { streamingBehavior: "followUp" });
		},
	},
});

const client = createClient<Registry<{ reviewer: typeof reviewer }>>();

export async function handOff(repo: string, branch: string) {
	await client.reviewer.getOrCreate([repo]).requestReview(`Review the ${branch} branch.`);
}

See Agent-to-Agent Messages.

Agent as a Workflow Step

When the work has fixed steps, waits, or retries, let a workflow drive the agent. Each step calls abort first, so a retried step doesn’t collide with a run the previous attempt left going.

Workflow steptriageAgentabort, prompt
import { pi } from "@rivet-dev/pi";
import { type Registry, workflow } from "@rivet-dev/workflows";

export const agent = pi({ model: "anthropic/claude-opus-5-5" });

type Agents = Registry<{ agent: typeof agent }>;

export const triage = workflow({
	run: async (ctx) => {
		await ctx.step({
			name: "triage",
			timeout: 10 * 60_000,
			run: async (step) => {
				const triager = step.client<Agents>().agent.getOrCreate([step.actorId]);
				await triager.abort();
				await triager.prompt("Triage yesterday's failed builds.");
			},
		});
	},
});

See Workflows.

Scheduled Agents

An agent can wake itself on a schedule and sleep between runs.

Cron0 9 * * *Agentasleep between runsprompt
import { pi } from "@rivet-dev/pi";

export const agent = pi({
	model: "anthropic/claude-opus-5-5",
	onCreate: async (c) => {
		await c.cron.set({
			name: "morning-triage",
			expression: "0 9 * * *",
			action: "prompt",
			args: ["Summarize yesterday's failed builds."],
		});
	},
});

See Schedules.

Credentials Shared per Tenant

Start agent keys with the tenant id, and pick the credentials Actor from it. Every agent in a tenant shares one set of model logins, and never sees another tenant’s.

[“acme”, “alice”][“acme”, “bob”][“globex”, “carol”]credentials acmecredentials globex
import { pi } from "@rivet-dev/pi";
import type { Registry } from "rivetkit";
import { credentials } from "../user-subscriptions/credentials";

export const agent = pi({
	model: "anthropic/claude-opus-5-5",
	credentials: (c) => {
		const [tenantId] = c.key;
		const client = c.client<Registry<{ credentials: typeof credentials }>>();
		return client.credentials.getOrCreate([tenantId]);
	},
});

See User Subscriptions.

Anti-Patterns

One Agent for Every User

An agent runs one prompt at a time. With a single key for everyone, a prompt from one user throws while another user’s prompt is running, and every user shares one session.

import { createClient } from "rivetkit/client";
import type { registry } from "../quickstart/server";

const client = createClient<typeof registry>();

export async function answer(userId: string, message: string) {
	const agent = client.agent.getOrCreate(["support"]);
	await agent.prompt(`${userId}: ${message}`);
	return agent.getLastAssistantText();
}

Solution: Key the agent by user, thread, or task.

A New Key per Request

A new key creates a new agent with an empty session, so the agent forgets the conversation after every message and leaves an Actor behind.

import { createClient } from "rivetkit/client";
import type { registry } from "../quickstart/server";

const client = createClient<typeof registry>();

export async function answer(message: string) {
	const agent = client.agent.getOrCreate([crypto.randomUUID()]);
	await agent.prompt(message);
	return agent.getLastAssistantText();
}

Solution: Reuse the key of the conversation the message belongs to.

Where to Go Next

GoalRead
Give the agent your own APIsCustom Tools
Let the agent run commands and edit filesSandboxes, then Built-in Tools
Run agents on your users’ own subscriptionsUser Subscriptions
Wait for a person before a risky actionHuman in the Loop
Share a result through a linkScoped Access with JWTs
Talk to users in Slack, Linear, GitHub, or DiscordSlack and the other connectors
Build a chat UIReact SDK
Secure and deploy agentsSecurity Model, then Deploy