Graine AI

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

AgentWhere it worksWhy it is its own agent
SupportHelp, orders, accountIt knows your policies, and its actions open a ticket or move a delivery
SalesPlans and pricingA different tone; it compares plans and books a demo call
CoachHome and onboardingIt 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

Your appHelporders · accountPlanspricing · trialsHomeonboardingSupport agentits key · design · componentsactions · booking rules · memorySales agentits key · design · componentsactions · booking rules · memoryCoach agentits key · design · componentsactions · booking rules · memoryShared by every agent — one app, one customer, one devicewho the customer is · the install ID · their memory choice · the current screenone call engine · one conversation at a timethe screen's agent: chat, voice, components

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.

<GraineProvider baseUrl="https://www.graine.ai" publishableKey="pk_live_support…" storage={AsyncStorage} navigationRef={navigationRef}>
  <NavigationContainer ref={navigationRef}><RootNavigator /></NavigationContainer>
</GraineProvider>
 
function PlansScreen() {
  return (
    <GraineProvider baseUrl="https://www.graine.ai" publishableKey="pk_live_sales…" storage={AsyncStorage}>
      <Plans />          {/* its hooks, bar and launcher are the sales agent's */}
    </GraineProvider>
  );
}

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 agentEach agent's own
Who the customer isidentify on any agent identifies the customer for all of them; identify(null) signs them out everywhereWhich trait is the identifier, by its own identity setting — pass every trait your agents use (user_id, email…)
The deviceThe install ID—
MemoryThe customer's memory choiceWhat 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 screenThe 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
ConversationsOne at a time across all agentsIts 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: startCall with 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 in configure({ 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.
await GraineCalls.configure({
  displayName: "Acme",
  agents: {
    support: { publishableKey: "pk_live_support…" },   // listed first: the default agent
    sales: { publishableKey: "pk_live_sales…" },
  },
});
await GraineCalls.startCall({ reason: "billing_question", agent: "support" });

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 onlyChat only
Its design (the Garage)Its own, drawn by the barIts own, drawn by the chat window; Chat window → Window size: Full screen
Its historyIts own calls, in Call HistoryIts 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.

// App.tsx — the voice assistant: the root provider, and the bar
<GraineProvider baseUrl="https://www.graine.ai" publishableKey="pk_live_voice…" storage={AsyncStorage} navigationRef={navigationRef}>
  <NavigationContainer ref={navigationRef}>
    <Stack.Navigator>
      <Stack.Screen name="Home" component={HomeScreen} />
      <Stack.Screen name="Help" component={HelpScreen} options={{ headerShown: false }} />
    </Stack.Navigator>
  </NavigationContainer>
  <GraineVoiceLauncher webView={WebView} autoStart>
    {(api) => <GraineVoiceBar api={api} />}
  </GraineVoiceLauncher>
</GraineProvider>
 
// HelpScreen.tsx — the support agent: its own provider, its chat the whole screen
function HelpScreen({ navigation }) {
  return (
    <GraineProvider baseUrl="https://www.graine.ai" publishableKey="pk_live_support…" storage={AsyncStorage}>
      <GraineChatScreen onClose={navigation.goBack} />
    </GraineProvider>
  );
}

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.

Next

On this page