Screen context
Tell the agent which screen the customer is on, which fields are filled, and what is failing — so it can help with the actual problem.
Screen context is what makes the agent in-app rather than merely in your app.
The route comes free
If you passed navigationRef to the provider, the SDK already knows which screen
the customer is on and reports it on every navigation — including screens you
never touch. You do not have to do anything for that.
What the route cannot carry is what is ON the screen: the values, the step, the field that is red. That is what this hook adds, and it layers on top of the route rather than replacing it — a screen that reports nothing still tells the agent where the customer is.
Adding the detail
One hook, placed next to the state the screen already holds.
That is the whole integration for a screen.
One input at a time
A screen describes itself once; its inputs change one at a time. Bind each input where its state lives and the agent sees the value (masked), the error the moment it is shown, and whether it is empty, filled or rejected — which is also what friction detection reads to notice a customer stuck on a field.
It merges into the current screen by name, is re-reported only when something
about it changed, and is withdrawn when the input unmounts. Driving the
client yourself: client.updateField({ name, … }) and client.removeField(name).
Screens outside the provider
Screens usually live under NavigationContainer; the provider often wraps
only your bar. Every hook that reports — useGraineScreen, useGraineField,
useGraineAction, useGraineTrack, useGraineTap, useGraineIdentify,
useGraineHighlight, useGraineWidget — works without a provider above it,
against the client the provider registered, or the one you register once:
There is no reason to re-implement any of them on your own client.
One component needs the same treatment: tap capture. The provider mounts
GraineTouchCapture around its own children, so a provider mounted deep
(around a call bar) sees taps on the bar and none on your screens. Mount it
at your root instead — it is exported and needs no provider above it (0.27.4; the active mount-early pattern needs 0.27.5):
Which actions this screen offers
availableActions on useGraineScreen narrows what the agent may call here.
Omit it — the default you want — and every action registered on the mounted
screen is offered. Pass a list to offer a subset. Pass [] to offer nothing,
deliberately, on a read-only screen. It can only narrow: an action the
dashboard never declared is not callable whatever appears here, and the server
enforces that. (Before 0.27.3 the SDK could send [] by accident while
handlers were still registering, which disarmed the agent for the screen; it
now sends [] only when you wrote [].)
What the agent is actually told
Everything below is rendered into the agent's prompt on every turn — not once at the start — so the model answers from the screen in front of the customer rather than from the conversation so far. Report it and the agent has it; leave it out and the agent says it does not know.
One report becomes several lines. fields alone produces both the count of what is
outstanding and the error the agent can offer to fix — which is why marking a status
accurately is worth more than adding another field.
Two of those lines are computed for the agent rather than reported by you:
| Line | Where it comes from |
|---|---|
N of M still need attention: … | Counted from your fields — anything empty, invalid, or carrying an error. locked is excluded: the customer cannot fix it by typing. |
All N fields on this screen are filled. | The same count, when nothing is outstanding |
The agent is told to answer "am I done?" and "what's left?" from that line rather than tallying the list itself, which is the kind of thing a model gets wrong by one.
Saying what comes next
journey.next is the one thing the agent cannot work out. step and of say
how far along someone is; asked "what happens after this?", an agent with only
a step number invents an answer — and on a setup flow an invented step is one
the customer then goes looking for. 0.29.0.
It follows them through the flow
The hook re-reports whenever what it describes changes, so a Save that moves
them to the next screen reports the new screen with its own fields, its own
step, and its own next — before the agent's following turn is composed. The
runtime replaces the block in place rather than appending, so nine screens
leave one description of the ninth, not nine contradictory ones.
What each field does
| Key | Why the agent wants it |
|---|---|
screen | A stable id. Keep it constant — your analytics group on it. |
title | What the agent calls the screen when it talks to the customer. |
journey | { name, step, of } — lets it say "one more step after this". |
fields | What is filled, empty, or rejected. |
error | A screen-level banner, as opposed to a per-field message. |
data | Anything else, app-specific and free-form. |
error is the most valuable key here
A field marked invalid tells the agent something is wrong. A field carrying
the message the customer is actually looking at tells it what is wrong, so
it can offer the fix instead of asking them to read it out.
The five statuses
status is what turns proactive help from "need a hand?" into naming the field
that is wrong, so it is worth picking the accurate one.
status | Means | What the agent does with it |
|---|---|---|
empty | Nothing entered yet | Offers to fill it, or asks for the value |
filled | Has a value | Leaves it alone; can still read it back |
invalid | Rejected — pair it with error | Names what is wrong and offers the fix |
pending | In flight; the answer is not known yet | Waits rather than reporting either outcome |
locked | Cannot be changed from here | Routes elsewhere instead of offering to type it |
locked is the one people skip, and it is the one that prevents a bad
conversation. A permission the customer has to grant in the operating system's
own settings is not empty — the agent cannot fill it and neither can they, on
this screen. Marked empty, the agent offers to enter a value and fails.
Marked locked, it tells them where to go.
optional — the blank that is not work left
A field nobody has to fill is still empty, and an empty field is counted as
work left. So a screen with an optional middle name tells the agent two things
are missing, the agent offers to fix one, and a customer who was already
finished is asked for something they do not have.
It stays in the frame — ask the agent to fill it and it will — but it is out
of the "N of M still need attention" count, out of the
incomplete-submit rule, and its own line reads
empty (optional) so the agent does not volunteer it.
A field carrying an error is outstanding whatever this says. optional
means they need not GIVE a value, never that a wrong one is fine.
Name every screen
The route the SDK reports for free is an identifier, not a name — kyc_pan,
who_calling, contacts_sync. Ship those and the agent is repeating your
routing table back to your customers, or, more often, avoiding the subject and
offering generic help to somebody who is already looking at the answer.
Give the agent the words that are printed on the screen:
Typing it as Record<ScreenName, string> over your own route union is what makes
this stick: the screen somebody adds next year fails the build instead of quietly
reaching a customer as a slug.
Screens that report fields name themselves through title. This map is the
floor under everything else — the screens you never got round to describing.
It cleans up after itself
The description lives as long as the component. On unmount it is withdrawn, so the agent can never discuss a screen the customer has already left.
Two different moments, deliberately: the description is re-sent whenever
its state changes and withdrawn only when the component unmounts. (An
earlier version withdrew on every change too, so each navigation and each
field edit put a screen-less frame on the wire first — under fast switching
that frame could be the last one applied, and the agent lost the screen.
Fixed in 0.26.2.) The withdrawal is also guarded: a navigation library keeps
the old screen mounted through its exit transition, so its cleanup can run
after the next screen has reported itself; it clears only if the screen it
described is still the current one.
If you drive the client yourself, mirror that: client.setScreen(ctx) on
mount, client.clearScreen(ctx.screen) on unmount — never setScreen(null)
from a cleanup.
Fast navigation
A customer who taps through four screens and then asks "where am I?" gets the fourth. Every change is relayed to the runtime immediately and in order (the 400ms coalescing applies only to the SDK's own text socket), every frame carries a monotonic sequence number so a late frame never replaces a newer one, and the newest screen is replayed whenever the connection or the screen channel (re)joins, and re-asserted every few seconds during a call and on return to the foreground — a frame lost in flight is repaired within seconds, not at the next change. The runtime applies the last-reported screen at every turn, so a language switch or a prompt rewrite mid-call cannot make the agent forget where the customer is.
Updates are batched and an unchanged screen is not resent, so calling this on every render — which is what happens, since the object is written inline — costs nothing.
Where to put the hook
Two rules, both of which produce a crash rather than a warning.
Below every piece of state it reads. The object is built during render, so a
const declared beneath the hook is in its temporal dead zone and throws. Action
handlers are the opposite — their bodies run when the agent calls them, so they
can close over anything. Expect that widening the hook to a new screen sometimes
means moving it rather than adding a branch.
Above every early return. if (loading) return <Spinner /> placed before the
hook changes the hook order between renders, which React treats as a bug in your
component. This is the most common way this integration breaks.
Report only a few fields per screen. The context is resent whenever it changes, and a screen that ships a fifty-row list crowds out the agent's own instructions — send the three rows the customer can see, not the whole table.
Detecting a stall — and more
Idle-on-a-step is one of five friction signals. The same field rejecting the customer twice, going back and forth between screens, rage-tapping a control and an error banner are the others — and every threshold, per screen, is configured in Agent → Embed & Widgets → Friction, not in code. See Friction detection.
(stallAfterMs still works and maps onto the idle rule.)
You can report your own moments, which is usually better than a timer because you know what actually went wrong:
Reporting an event is a request, not a command. The agent stays quiet while it is already talking, while a reply is on its way, for 20 seconds after the last time it spoke up, and after three unsolicited interventions in one session. Those limits are the difference between help and an assistant people switch off.
What never leaves the device
Values are masked before the frame is sent — PAN, Aadhaar, card and account numbers, phones, emails. The agent is told a field is filled, empty or rejected, never what is in it. See Privacy and security for the full list and how to add your own formats.
Next steps
Who the customer is
The screen says where; identity says who. Call it after sign-in, and again on account switch:
Traits become prompt variables, so a prompt can say greet {name}. Everything is
masked before it leaves the device, so a PAN passed as a trait by accident does
not leak.
Metadata before the first word
Two places, and the difference is when you know the value.
identify() wins over variables when both set the same key, because a
signed-in customer is more specific than a default.
It reaches the call record, not just the prompt
Traits are stored on the conversation as app_user, and the name is copied to
the row label. Without it a web call has no phone number to identify it by, and
Call History shows a session id — a page of uuids, in the product whose premise
is that it knows who it is talking to.
Taken from the LIVE identity rather than the init frame, so a customer who signs in halfway through is identified from that moment.
Nothing else is gated on this — skip it and every other feature still works, the agent just does not know their name.
What happened, without interrupting
Some things should inform the next answer rather than produce an unprompted one:
This does not make the agent speak. It lands in the context, so when the customer asks "why was my card refused" the agent already knows. Use it for what explains a conversation — a declined payment, a rejected document, an OTP retried three times.
The distinction matters. Send these as proactive events instead and the agent pipes up every time anything happens in your app, which is the version of this feature customers switch off. The last 10 are kept, so a burst does not crowd out the agent's own instructions.
When something should be spoken about
reportEvent is the one that may make the agent speak first. detail tells it
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 having to
ask: a save that did not reach the server, an upload that failed twice, a
payment declined. Not a tap, not a navigation, not a save that worked — those
belong in track(), shaping the next answer rather than producing one nobody
asked for.
The runtime still decides. It stays quiet while it is talking, while a reply is already generating, for 20 seconds after the last time it volunteered, and after three interventions in one session. Those limits are the difference between help and an assistant people switch off.

