Use more than one agent in your app
Run several agents in one app or website — a support agent in Help, a sales agent on Plans, a coach on Home. Configure each under a name, choose one per screen, and know what they share, what each keeps to itself, and how calls work with several agents.
One agent can do a lot, but different jobs often want different agents: a different prompt, voice, design, set of actions and booking rules. Your app can run several. Each agent has its own publishable key; your app configures each one under a name and chooses, screen by screen, which one the customer talks to.
When to use several agents
| Agent | Where it works | Why it is its own agent |
|---|---|---|
| Support | Help, orders, account | It knows your policies, and its actions open a ticket or move a delivery |
| Sales | Plans and pricing | A different tone; it compares plans and books a demo call |
| Coach | Home and onboarding | It speaks up when someone is stuck, in a lighter design |
Keep one agent when the jobs share a prompt and the customer moves between them in a single conversation: switching agents starts a new conversation. And give each surface its own agent where its actions differ — the reasoning is in One agent per surface.
How it fits together
Each screen talks to one agent. What makes the customer the same person — who they are, their device, their choices, what they are looking at — is the app's, shared by all of them.
1. Configure each agent
Configure every agent once, at launch, each with its own publishable key (Agent → Embed & Widgets). The agent you configure without a name is the default agent — the one every view draws, and the one an existing single-agent app already has. Configuring a name that is already configured returns that agent unchanged.
React Native has no names: each agent is a GraineProvider of its own. Keep the
default agent's provider at the root, and nest another around the screens its
agent serves. Every hook on a screen — the screen context, actions, the chat —
talks to its nearest provider.
The providers share one customer and one device: identify() through any of
them reaches every agent, including one whose screen mounts later. Give every
provider the same storage (or link the calls package) so they share the install
ID and the memory choice. A client you make yourself works the same way —
<GraineProvider client={sales}> — and client.dispose() lets it go.
A name is trimmed; an empty name is the default agent. In an app that names every agent and configures none as the default, the first one configured acts as the default.
2. Choose the agent on each screen
Views take the agent they draw; without one they draw the default agent.
The nearest provider decides: a screen inside PlansStack talks to the sales
agent, its GraineAgentBar, useGraineAction and useGraineScreen included.
Register each action on the agent that declares it: an action registered on the
support agent is never offered by the sales agent. Book calls on the agent that
should ring — scheduleCall on an agent uses that agent's booking rules.
What is shared, and what is each agent's
| Shared by every agent | Each agent's own | |
|---|---|---|
| Who the customer is | identify on any agent identifies the customer for all of them; identify(null) signs them out everywhere | Which trait is the identifier, by its own identity setting — pass every trait your agents use (user_id, email…) |
| The device | The install ID | — |
| Memory | The customer's memory choice | What it remembers, under its own Memory settings: This agent only, or Agents that share memory with your other agents set to share — and its own recent conversation, which is never shared |
| The screen | The current screen is the app's: each agent receives it while it has a conversation | — |
| The agent itself | — | Its prompt, voice, design and components, from its own dashboard settings |
| Actions | — | The actions registered on it, and only those |
| Booking | — | Its booking rules, and the calls booked with it |
| Conversations | One at a time across all agents | Its chat, its voice bar, its conversations in Call History |
React Native shares them the same way across its providers: identify through
any provider reaches every agent, the screen reported under one reaches all of
them, and the install ID and memory choice are shared when every provider has
the same storage (or the calls package is linked).
One conversation at a time
Only one conversation runs at a time, across all your agents: starting a chat or a call in the bar with one agent ends a live one with another. A native call is never cut off by the bar — starting the bar during a call is refused, as with a single agent.
Calls with several agents
Your app has one call engine, shared by every agent: one ring, one call screen, one call at a time.
- The customer calls an agent:
startCallwith the agent's name places the call with that agent — the default agent when you name none. In React Native, the calls package takes every agent by name inconfigure({ agents }), and you attach each provider's client. - Graine rings the customer: a ring names the agent that booked the call, and the SDK answers with that agent — its prompt and actions, and its design on the call screen, kept on the device so a ring that wakes a closed app is drawn correctly. A ring that names no agent configured in this build is answered by the default agent, and the SDK logs a warning.
- Registering the device: the push token belongs to the install, not to an agent. The SDK registers it with each configured agent the first time, so any of them can ring this device.
Or one agent at a time: configure({ publishableKey, displayName }), then
configure("sales", { publishableKey: "pk_live_sales…" }). Attach each
provider's client with GraineCalls.attach(client) — matched to its agent by
key, or attach(client, { agent: "sales" }). Several agents need the calls
package's native code: rebuild the app after upgrading.
Example: a voice assistant and a support chat in one app
A voice assistant in the bar on every screen, and a support agent whose chat fills the Help screen. They are two agents: two prompts, two Garage designs, two conversations.
In the dashboard, on each agent's Embed & Widgets → Appearance:
| The voice assistant (the default agent) | Support | |
|---|---|---|
| What visitors get (Setup) | Voice only | Chat only |
| Its design (the Garage) | Its own, drawn by the bar | Its own, drawn by the chat window; Chat window → Window size: Full screen |
| Its history | Its own calls, in Call History | Its own chat — and, for a returning customer with memory on, its own kept conversation, never shown by the voice assistant |
Then, in the app, the voice assistant is the default agent the bar draws, and the Help screen hands its whole screen to the support agent's chat.
While GraineChatScreen is on screen the customer is in the support chat and the
voice bar steps aside.
What follows from their being two agents:
- One customer. Identify the customer once, on either agent: both know who they are, and the memory choice is the same for both.
- One conversation at a time. Opening the support chat ends a call with the voice assistant; the bar steps aside while the chat is up.
- Separate histories. Each keeps its own conversation; starting a new one in
the support chat — its header's New conversation, or
newConversation()on the support agent's chat — leaves the voice assistant's untouched, and memory stays.

