Skip to main content
Core

Design Patterns

Patterns for agent keys, subagents, agent-to-agent messages, workflows, schedules, background jobs, 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.

One Agent per Key

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

  • Per user: a personal assistant remembers everything a user has asked, across every chat.
  • Per thread: each Slack thread, support ticket, or chat tab is its own conversation.
  • Per task: each job, such as fixing one GitHub issue, starts fresh in its own sandbox. When it’s done, an action that calls c.destroy() deletes the agent and its sandbox, like finish below.
assistant/:userIdAssistantremembers the usergetOrCreateslackThread/:channel/:tsSlack threadone conversationgetOrCreateissueFixer/:repo/:issueIssue fixerone fix, then deletedgetOrCreate

Keys are arrays, so a channel id or repository name from a webhook can’t break the key’s structure. See Actor Keys.

The prompt action uses the agent’s root conversation, so by default the key is the conversation. An agent can also hold more conversations and forks, which lets related conversations share one Actor, one sandbox, and its documents.

Lead Agent with Subagents

A lead agent hands a focused task to a specialist with its own tools and waits for its answer. The specialist starts with a fresh context, so only the answer comes back.

Support agentask_specialist toolOrders specialistget_order toolEngineering specialistsandboxquestion

The get_order tool in get-order.ts is the one from Custom Tools.

See Subagents.

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

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 prompts the agent and waits for its answer.

Workflow steptriageAgentprompt
npm add @rivet-dev/workflows

See Workflows.

Scheduled Agents

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

Cron0 9 * * *Agentasleep between runsprompt

See Schedules.

Background Agents That Must Finish

When nobody is watching a run, such as an agent started by a webhook, a crash must not lose the work or repeat a side effect. Every model call and tool call of a pi() run is saved before the next step starts, so the run continues where it stopped. Pass a requestId, so a redelivered webhook returns the first submission instead of starting a second run.

GitHub webhookrequestId: deliveryFixer agentkey: repo, issueOpen PRnever repeatedsubmit
  • The conversation.submit action returns once the input is saved, so the webhook answers right away while the agent works.
  • The open_pull_request tool has no replay, so a call cut off by a crash isn’t repeated. The model is told the call was interrupted. Mark only tools that are safe to run twice as replay: "safe".
  • When you ship new code, the Actor lets the run finish, for up to sleepGracePeriod, before it restarts on the new version. Anything still running then carries on from its last saved step.

See Architecture.

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

See User Subscriptions.

Anti-Patterns

One Agent for Every User

With a single key for everyone, every user shares one conversation and its history. A prompt from one user waits in the queue while another user’s prompt runs.

import { createClient } from "rivetkit/client";
import type { registry } from "./agent-per-key/server";

const client = createClient<typeof registry>();

export async function answer(userId: string, message: string) {
	const result = await client.assistant.getOrCreate(["support"]).prompt(`${userId}: ${message}`);
	return result.status === "done" ? result.text : undefined;
}

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

A New Key per Request

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

import { createClient } from "rivetkit/client";
import type { registry } from "./agent-per-key/server";

const client = createClient<typeof registry>();

export async function answer(message: string) {
	const result = await client.assistant.getOrCreate([crypto.randomUUID()]).prompt(message);
	return result.status === "done" ? result.text : undefined;
}

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

Webhooks Without a requestId

Webhook providers deliver the same event again when your handler is slow or fails. Without a requestId, each delivery starts its own run, so the agent does the work twice and a tool with side effects can run twice.

Solution: Pass the delivery id as the requestId of every submission. See Background Agents That Must Finish.

Where to Go Next

GoalRead
Give agents model accessLLM API Keys
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
Make runs finish after a crash, with no one to retry themArchitecture
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
See what happens while an agent answers a promptSession Lifecycle
Build a chat UIReact SDK
Secure and deploy agentsSecurity Model, then Deploy