Graine AI

Screen context

Tell the agent which screen the customer is on, which fields are filled and what is failing — so it helps with the actual problem.

This is what makes the agent in-app rather than merely in your app.

The route — free“they are on KycDocuments”One hook — you write thistitle, journey step,each field + status + errorThe SDKmasks personal data,numbers every frame,drops stale onesWhat the agent reads“Step 2 of 4. PAN is invalid:checksum failed. Aadhaar filled.”— before every single reply

In one minute: call useGraineScreen with screen, title, journey and a fields array where every field has a status. status is what turns "need a hand?" into naming the field that is wrong. Everything else here is refinement.

One hook

Pass navigationRef to the provider and the SDK already reports which screen the customer is on, for every screen, including ones you never touch. What the route cannot carry is what is on it. That is this hook:

PanScreen.tsx
useGraineScreen({
  screen: "kyc_pan",
  title: "PAN verification",
  journey: { name: "KYC", step: 2, of: 4, next: "Take a selfie" },
  fields: [
    { name: "pan", label: "PAN Number", value: pan,
      status: error ? "invalid" : pan ? "filled" : "empty", error },
  ],
});

That is the whole integration for a screen. Write it inline — the hook compares by value, so an unchanged screen is never resent and calling it on every render costs nothing.

Two rules, and breaking either one crashes rather than warns: put the hook below every piece of state it reads (the object is built during render), and above every early return (if (loading) return <Spinner/> before it changes the hook order). That is the most common way this integration breaks.

Report the real values. Masking runs on the way out — a screen that pre-redacts only blinds the agent.

What the agent actually reads

Rendered into the prompt on every turn, not once at the start:

Screen: PAN verification (kyc_pan)
Journey: KYC — step 3 of 5
Next in this journey: Take a selfie
2 of 4 still need attention: PAN number, Date of birth
Fields on this screen:
  - PAN number: ERROR — That PAN is already registered.
  - Date of birth: empty
  - Full name: Asha Menon
The customer has been idle on this screen for 42s.
Who you are talking to: name Asha Menon, plan Plus
In-app tools available RIGHT NOW:
  - fill_field: Type a value into a field on this screen.
YOU REPORTTHE AGENT READSscreen + titlejourneyfieldsuseGraineTapidentify()useGraineHighlightScreen: PAN verification (kyc_pan)Journey: KYC — step 3 of 5 · Next: Take a selfie2 of 4 still need attention: PAN, Date of birthPAN number: ERROR — already registered.The customer has just done this: tapped submitWho you are talking to: name Asha, plan PlusYou can point at these: pan_field, submit

One report becomes several lines. fields alone produces both the count of what is outstanding and the error the agent can offer to fix.

The N of M still need attention line is counted for the agent from your fields — anything empty, invalid or carrying an error, excluding locked and optional. The agent answers "what's left?" from that line rather than tallying the list itself, which is the kind of thing a model gets wrong by one.

The keys

KeyWhat it gives the agent
screenA stable id. Keep it constant — analytics group on it.
titleWhat the agent calls this screen out loud.
journey{ name, step, of, next }. next is the only one it cannot infer — without it, asked "what comes after this?", it invents a step.
fieldsWhat is filled, empty or rejected.
errorA screen-level banner, as opposed to a per-field message.
dataAnything else, app-specific and free-form.
availableActionsNarrows what may be called here. Omit it and every action registered on the mounted screen is offered; [] deliberately offers none.

The five statuses

statusMeansWhat the agent does
emptyNothing enteredOffers to fill it
filledHas a valueLeaves it alone
invalidRejected — pair with errorNames what is wrong and offers the fix
pendingIn flightWaits rather than reporting an outcome
lockedCannot be changed hereRoutes elsewhere instead of offering to type

locked is the one people skip and the one that prevents a bad conversation: an OS permission marked empty gets the agent offering to type a value it cannot type. Marked locked, it tells them where to go.

error is the most valuable key on the page. invalid says something is wrong; the message the customer is actually looking at says what, so the agent can offer the fix instead of asking them to read it out.

Add optional: true to a blank nobody has to fill. It stays visible to the agent but leaves the "still need attention" count, so a finished customer is not asked for a middle name. A field carrying an error is outstanding regardless.

Name every screen

The free route report is an identifier — kyc_pan, contacts_sync. Ship those and the agent reads your routing table back to your customers. Give it the words printed on the screen:

screenTitles.ts
export const SCREEN_TITLES: Record<ScreenName, string> = {
  kyc_pan: "PAN verification",
  contacts_sync: "Sync contacts",
};

Typing it over your own route union is what makes it stick: a screen added next year fails the build instead of reaching a customer as a slug.

One input at a time

A screen describes itself once; its inputs change one at a time. useGraineField binds one input where its state lives, merges into the current screen by name, and withdraws when the input unmounts:

useGraineField("pan", { label: "PAN Number", value: pan, error,
  status: error ? "invalid" : pan ? "filled" : "empty" });

Who the customer is

The screen says where; this says who.

const identify = useGraineIdentify();
useEffect(() => { identify(user ? { name: user.name, plan: user.plan } : null); }, [user]);

Traits become prompt variables (greet {name}), are masked like everything else, and land on the conversation as app_user — which is what stops Call History being a page of session ids.

Prompt variables, from your app's own data

Three places to set them, one merged view, every transport:

Set it withWhen
What the app knows at build time<GraineProvider variables={{ brand: "Acme" }}>Fixed for the life of the install
What the app learns while runninguseGraineVariables() / client.setVariables({ plan, cart_total })The plan they just upgraded to, the order on screen
Who is signed inidentify({ name, plan })After sign-in; null on sign-out
const setVariables = useGraineVariables();
useEffect(() => {
  setVariables({ plan: account.plan, cart_total: cart.total });
}, [account.plan, cart.total]);

Later rows win where two set the same key. A variable nothing sets is left out of the prompt — the agent never sees an empty placeholder — so write the prompt to read well without it. Values are strings in a sentence — numbers and booleans are flattened, objects are dropped — and everything is masked on the way out. The merged set reaches the next conversation however it connects — chat, voice or WebRTC — so a sign-in after the page loaded is still a name the agent learns. Unknown keys travel and simply substitute nowhere.

Things that should inform, not interrupt

const track = useGraineTrack();
track("payment_declined", { reason: "insufficient_funds", amount: 4999 });

track does not make the agent speak. It lands in the context, so when the customer asks why their card was refused, the agent already knows. The last 10 are kept.

client.reportEvent({ name, detail }) is the one that may make the agent speak first. detail must say what to offer — without it the agent reads an event name aloud, which is worse than silence. Reserve it for a dead end the customer would want addressed without asking: a save that never reached the server, an upload that failed twice.

Reporting is a request, not a command. The agent stays quiet while it is talking, while a reply is on its way, for 20 seconds after it last spoke up, and after three unsolicited interventions in a session. Those limits are the difference between help and an assistant people switch off.

Details worth knowing

  • It cleans up after itself. The description lives as long as the component and is withdrawn on unmount, guarded so a screen leaving during a transition cannot wipe the one that just arrived. Driving the client yourself: client.setScreen(ctx) / client.clearScreen(ctx.screen) — never setScreen(null) from a cleanup.
  • Fast navigation is safe. Every frame carries a sequence number, so a late frame never replaces a newer one, and the newest screen is replayed when the connection rejoins and on return to the foreground.
  • Hooks work outside the provider. Screens live under NavigationContainer; the provider often wraps only the bar. Every reporting hook falls back to the client the provider registered, or one you register once with setDefaultClient(client). Tap capture is the exception — mount <GraineTouchCapture> at your root, or it only sees taps on the bar.
  • Keep it small. A few fields per screen. A fifty-row list crowds out the agent's own instructions; send the three rows the customer can see.
  • Nothing sensitive leaves. PAN, Aadhaar, card and account numbers, phones and emails are masked before the frame is sent. See Privacy.

Next steps

On this page