Graine AI

Components

Draw the answer on screen instead of reading it out — lists with pictures, plans, time slots and quick actions, designed in the dashboard and drawn natively by the SDK.

Reading three cars and their prices out loud loses the customer at the second one. A component puts them on the screen they are already holding, and hands your agent a structured answer back from the tap.

You build it once in the dashboard. The SDK draws it. Nothing about it lives in your app — not the items, not the look, not where a tap goes.

The five looks a component can take on a phone — Gallery, Cards, List, Spotlight and Compact

Watch it first

A live call in a real app: the agent draws the component, the customer taps, and the tap reaches the conversation.

How it works, in one picture

The customer“show me SUVsunder 20 lakh”The agentcalls car_showcasewith max_price=2000000Your APIoptional — returnsthe live itemsThe phonethe SDK draws itin your lookThey tapa turn in the conversation, the structured answer for your CRM,and — if you set one — a screen opening in your appback to the agent

The dashed box is optional: a component with no endpoint shows what you typed. The dashed arrow is the tap arriving as the customer’s own turn.

In one line: design it in Web components, switch it on for this agent under Embed & Widgets → On screen, name it in the prompt. In the app you write nothing — the agent bar already draws it.

Everything you can change, and where

Nothing in this table is code. Each row is a field in the dashboard, and each one reaches the phone the next time you press Push to the agent.

WhereChoices
ContentWeb components → ContentTyped here, or live from your API when it is shown
LookLook & feelGallery · Cards · List · Spotlight · Compact
Size, density, button, facts, columnsLook & feelPer-look knobs
SurfaceLook & feelFull screen · sheet · card above the bar · auto
Whose shapesLook & feel → ChangeFollow the phone · iPhone · Android · Samsung
One or severalLook & feel → ChooseOne tap answers, or tick two and press once
Corners and accentLook & feel → ChangeFrom Appearance, or this component's own
What a tap doesWhen they tapTell the agent · open a screen · run an action
How waiting looksAppearance → LoadingDots · Pulse · Ring · Bars · your logo
What the agent calls itAgent → Tool namecar_showcase, plus the prompt paragraph to paste

Turning one on

Four things have to be true before a component can appear, and one of them fails silently if your agent is older.

YOU, IN THE DASHBOARDAT CALL TIME1 · Build itlayout and content,and a tool name2 · Point at your APIoptional — leave itblank to stay static3 · Savepushes it intoagent config4 · On screenwhich agentsmay show itThe agent calls itby its tool name,when it decides toYour API is calledfrom our servers,merged over your contentTo the devicekind + data, verbatimIt drawsembed and app,from one definition

An agent with no web-call variant is SKIPPED

open that agent, save it once, and try again

Saving is what pushes a component into an agent’s config — a live agent reads a snapshot of its tools, not the library. Components are deliberately never written to a phone agent, so an embed with no web-call variant is skipped and the dashboard tells you which.

Is it actually on the agent?

There are two copies and only one is callable: the library in the dashboard, and the snapshot inside the agent's config that the agent reads. Embed & Widgets → On screen reads the snapshot back and says one of three things:

  • The agent can call … — it is live; mention it in the prompt.
  • Nothing — press Save selection to push it.
  • No web-call variant — components are never written to a phone agent. Open that agent, save it once to create one, and come back.

A component is a tool the model calls. Refer to it in the prompt the way you would any other tool — by what it does, no special syntax. An agent never told it exists answers in words and reaches for whatever else it has. On a flow agent, say it in the instructions of the step that should show it.

Live content from your API

Leave the endpoint blank and the component shows what you typed. Fill it in and it is called the moment the agent reaches for the component; the JSON it returns is merged over what you authored, so the layout stays yours and the items, prices, images or times are live.

{ "items": [
  { "id": "creta-sx", "title": "Creta SX(O)", "price": "₹19.2L",
    "imageUrl": "https://cdn.yours.com/creta.jpg",
    "facts": ["6 airbags", "Panoramic sunroof"] }
] }
Called fromOur servers, not the device — a key in a header never ships to the app
Must behttps, reachable from the internet. Private addresses and cloud metadata are refused
Timeout8 seconds, and only on a channel that can draw — a phone call never pays it
If it failsThe component still shows what you authored

Press Test endpoint in the editor to call it with sample values. It reports the status, the time, how many items came back, and puts the live data into the phone preview.

Ask it for what the customer said

A placeholder in the URL, a query value or the body becomes a parameter the model fills from the conversation:

https://api.yoursite.com/cars?max_price={max_budget}&min_price={min_budget}
Where they workThe URL, any query value, the body at any depth
Where they do notHeaders, deliberately — a placeholder there would let the model choose an authorization value
Required?Never. Someone who has not named a budget still sees the default list
Left blankThe query parameter is dropped, not sent empty — ?max_price= asks something different from not asking
SafetyValues are encoded on substitution and the finished URL is re-checked before the call

Name parameters for their direction, and say what each means. A component whose endpoint takes only {max_price} gets filled with max_price whatever the customer said — including "cars above 25 lakh", which then comes back as everything below it. A filter that looks like it worked is the worst kind of wrong. Write one line per parameter in what it means, for the agent; it becomes the first thing the model reads in that parameter's description.

The editor also generates the paragraph to paste into the prompt — the tool name, when to reach for it, and each parameter in your words. An agent told only "call car_showcase" sends whichever parameter it can guess at.

Name it for the moment it should be used

The tool name is the biggest lever on whether a component is ever shown, because it is all the model has when deciding.

Harder to triggerEasier to trigger
plan_comparisonshow_health_plans
product_listshow_cars_in_budget
calendar_slotsoffer_appointment_times

Letters, numbers and underscores; it cannot start with a digit, and end_call, transfer_call and switch_language are built into every agent. Renaming changes what the prompt must say — update both in the same sitting, or the agent keeps calling a name that no longer exists, which looks exactly like a broken component.

Look and feel

Chosen on the component, in Web components → Look & feel. The SDK draws whatever is saved; nothing in your app decides it and nothing in your app changes when it changes.

PresetWhat it drawsBest for
GalleryA big photo on every item, full-width buttonThings people buy with their eyes
CardsThumbnail left, facts right, three to a screen. The default.Most lists
ListDense rows; the row is the buttonLong results — plans, slots, SKUs
SpotlightOne at a time, swipe to compareTwo to five items to compare
CompactNo pictures, one line eachPlans, add-ons, time slots

The same Cards component drawn with Apple's, Material's and One UI's grammar

Platform is whose design grammar to use. Follow the phone — the default — draws Apple's shapes on an iPhone, Material's on Android and One UI's on a Samsung: corner radii, chip and button shapes, separator insets, the sheet's grabber. Colours stay the brand's. Pinning one draws that grammar everywhere.

The preview beside the editor is the same renderer the phone uses, in a phone-sized frame, with Preview as and a light/dark switch.

An app on an older SDK draws Cards for a look it has never heard of. Nothing breaks on a phone that has not updated.

When they tap

A tap is always a turn: the choice goes to the agent as the customer's own sentence, and the structured answer goes to your webhook. Beyond that, you choose.

Open a screen. Register one handler, once:

useGraineAction("open_screen", async ({ path }) => {
  navigate(path);                 // whatever your navigator understands
  return { status: "ok" };
});

From then on, When they tap → Open a screen is a field on every component:

compare?ids={ids}

{id}, {title}, {price} and {summary} are the item they tapped; {ids} and {titles} are all of them when several may be chosen. Change the path, push to the agent, and the next tap goes somewhere else — no app release.

Run an action names a specific action instead, for a tap that is not navigation; a path you also set arrives as a path argument. Either way, your app reports whatever screen it lands on, which is how the agent knows where the customer now is — and an action this build does not implement is refused, reported, and the conversation carries on.

Choosing several

A comparison needs two. Look & feel → Choose → Several turns the rows into ticks with one button underneath. Two or three picks, one press, one turn: the customer's message names every item they picked. The answer carries every item plus ids, titles and count, which is what compare?ids={ids} reads.

Below two it says how many more are needed rather than offering a press that does nothing; past the cap (default 3) the oldest tick gives way, because a tap that does nothing reads as a broken row.

Closing is not answering

Close a component without choosing and the agent is told — quietly. It does not speak about it, but it stops referring to options nobody can see and it knows which tool to call if they ask again. useGraineWidget().closed holds it and reopen() puts it back with no second call to your API.

How waiting looks

The five loaders — Dots, Pulse, Ring, Bars, your logo

Chosen once, in Appearance → Loading: the kind, its size and speed, its colour, and the words for each moment — Connecting…, Reconnecting…, Thinking…, Loading…. The web chat draws the same, and so does the SDK's voice bar (GraineVoiceBar, and the bar on Android, iOS and Flutter) — in its status line while the call connects, reconnects and while the agent thinks. Drawing your own bar, the same loader is one component:

<GraineVoiceLauncher webView={WebView} active={open}>
  {(api) => (
    api.phase === "listening" || api.phase === "idle"
      ? <MicButton onPress={api.start} />
      : <GraineLoader moment={api.phase === "speaking" ? undefined : api.phase} />
  )}
</GraineVoiceLauncher>

api.phase is idle · connecting · reconnecting · thinking · speaking · listening. Pass label to override the words, or kind / size / color for one place.

A component on its way shows the loader, and only the loader. The agent says one is coming before it calls your API, and GraineWidget draws your loader — centred, at reading size, with the words set for Loading… — on the same surface the component will take, so nothing jumps when the content lands. It stays up for at least 450 ms, because a loader that vanishes within two frames reads as a glitch rather than as loading.

Tick also show the shape in Appearance to draw a dimmed outline of the coming component beneath it. Off by default: fake rows above a loader read as content that is not real, and on a short phone they push the only honest thing on screen below the fold.

Drawing components yourself? useGraineWidget().loading is { id, kind } while one is in flight.

Drawing them yourself

The agent bar already draws every kind. Do this only if you want your own UI.

const { widget, submit, dismiss } = useGraineWidget();
 
if (widget?.kind === "product_list") {
  const { items, selectLabel } = widget.data as any;
  return items.map((it) => (
    <Row key={it.id} image={it.imageUrl} title={it.title} facts={it.facts}
      price={it.price} disabled={it.unavailable}
      buttonLabel={selectLabel ?? "Choose"}
      onPress={() => submit({ id: it.id, title: it.title, price: it.price },
                            `I'll take a look at ${it.title}.`)} />
  ));
}

submit takes two things and both matter: data, the structured answer your backend wants, and summary, a sentence for the conversation. Send only the first and the customer taps a card, sees a tick, and the agent carries on as though nothing happened.

For any kind you do not draw, render widget.fallbackText — what the agent would have said instead. dismiss() closes it locally.

imageUrl takes an absolute URL, a root-relative path (resolved against the agent's origin before it reaches the device — React Native would otherwise draw an empty box and say nothing), or a data URI.

The kinds the dashboard ships

lead_form, plan_comparison, info_card, calendar_slots, quick_actions, product_list, blank. The SDK relays kind verbatim, so an agent may send one of your own — handle what you draw and fall back for the rest.

One library, chosen per agent

Components belong to the workspace, not to an agent — a lead form is the same lead form whichever agent shows it. Which of them an agent may show is per agent, under Embed & Widgets → On screen.

Pick nothing and the agent gets every enabled component, including ones added later. Tick even one and it sees only what is ticked. Untick everything and it draws nothing and answers in words — a real choice, and not the same as having picked nothing.

Every component you give an agent becomes a tool it can call, so a car catalogue on a loan agent is not clutter but a tool a model will eventually reach for.

Pointing at something

A second way to use the screen: instead of drawing something new, point at what is already there.

"Tap Continue" is useless if the customer cannot find Continue.

import { useGraineHighlight } from "@graineai/inapp-react-native";
 
function ContinueButton() {
  const { highlighted } = useGraineHighlight("continue_button");
  return (
    <Pressable style={[styles.btn, highlighted && styles.glow]}>
      <Text>Continue</Text>
    </Pressable>
  );
}

That is the whole integration. Registering the name tells the agent the element exists and can be pointed at; the boolean tells you when it is being pointed at.

The SDK does not draw the highlight. It does not own your design language, and a ring that looks right in one app looks broken in the next. You get a boolean and style it the way the rest of your app would — a border, a pulse, a scroll-into-view.

Highlights expire after six seconds. One still lit after the conversation moved on is worse than none: the customer keeps looking at the wrong thing.

Scoped to the component. Unmounting deregisters it, so the agent is never offered a target on a screen the customer has left.

The one dashboard step

Declare highlight_element on the agent, under Integrations & Tools, alongside your other actions:

{
  "name": "highlight_element",
  "description": "Draw the customer's attention to something on their screen. Use it when you tell them to tap or look at a specific control.",
  "parameters": {
    "type": "object",
    "properties": { "name": { "type": "string", "description": "The element to point at." } },
    "required": ["name"]
  }
}

You do not implement it. The SDK answers it: it checks the name against what is actually registered on the current screen and refuses with the real list when the agent invents one — so a wrong guess produces a useful sentence rather than a dead action.

Next steps