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.
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.
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.
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.`);
}
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.
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.
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.
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
| Goal | Read |
|---|---|
| Give the agent your own APIs | Custom Tools |
| Let the agent run commands and edit files | Sandboxes, then Built-in Tools |
| Run agents on your users’ own subscriptions | User Subscriptions |
| Wait for a person before a risky action | Human in the Loop |
| Share a result through a link | Scoped Access with JWTs |
| Talk to users in Slack, Linear, GitHub, or Discord | Slack and the other connectors |
| Build a chat UI | React SDK |
| Secure and deploy agents | Security Model, then Deploy |