Graine AI

Screen context and actions

setScreen, fields, clearing, track, taps, reportEvent, noteInteraction, prompt variables, registering actions and checking them, components, pointing at an element, masking and friction rules — every platform's signature, parameters, returns, timing and threading.

Why and when to use these is on Screen context, Actions and Components. The structures — GraineScreen, GraineField, GraineActionResult, the component — are on Types.

setScreen

What the customer can see. Call it whenever the screen or a field changes.

useGraineScreen(context: ScreenContext | null): void      // inline in the screen; withdrawn on unmount
client.setScreen(context: ScreenContext | null): void
 
useGraineScreen({ screen: "order_details", title: "Your order",
  fields: [{ name: "delivery_date", label: "Delivery date", value: deliveryDate, status: "filled" }] });

With navigationRef on the provider, the route name is reported for every screen by itself.

Returns nothing. What happens: values are masked on the device; an unchanged screen is not sent again; the first change goes at once and later ones at most every 400 ms; the newest screen is sent again when the connection rejoins and when the app returns to the foreground. Each frame carries a sequence number, so a late one never replaces a newer one.

Fields and clearing

MemberReact NativeiOSAndroidFlutterKotlin Multiplatform
Update one fielduseGraineField(name, field), client.updateField(field)updateField(_:)updateField(field)updateField(f)updateField(field)
Remove oneclient.removeField(name)removeField(_:)removeField(name)removeField(name)removeField(name)
Withdraw a screenclient.clearScreen(screenId)clearScreen(_:)clearScreen(screenId = null)clearScreen([screenId])clearScreen(screenId = null)

A field merges into the current screen by name. Clearing with a screen id withdraws that screen only if it is still the current one, so a screen leaving during a transition cannot wipe the one that arrived.

track, taps and events

useGraineTrack(): (name: string, data?: Record<string, unknown>) => void
client.track(name: string, data?: Record<string, unknown>, opts?: { coalesce?: boolean }): void
useGraineTap(name, onPress): typeof onPress        // wraps a handler: the tap is tracked, rage taps detected
client.reportEvent(event: { name: string; detail?: string; cooldown_seconds?: number; max_per_session?: number;
                           quiet?: boolean; data?: Record<string, unknown> }): void
MemberWhat it does
trackA product event the agent knows about without speaking — the last 10 are keptcoalesce: a burst of the same name counts once
tapA tap on a named controlAt most two a second reach the agent
reportEventSomething worth speaking up about unprompted: a dead end the customer would want addresseddetail says what to offer, in plain words. In React Native, cooldown_seconds (default 20) and max_per_session (default 3) pace the agent; quiet: true records it without making the agent speak
noteInteractionThe customer did something that is not a field edit — the idle clock restarts

Automatic taps: with captureTaps (default on in iOS, Android, Flutter and Kotlin Multiplatform; on in React Native with fab or the captureTaps prop), taps on labelled controls are tracked by themselves. In React Native, <GraineTouchCapture enabled={true}> does the same for the tree it wraps: a touch that moves under 12 px and lifts within 400 ms is a tap.

Reading what will be sent (React Native)

MemberReturns
client.getScreen()ScreenContext | null — the screen as the next frame will carry it
client.getVariables()Record<string, string> — every variable the agent will receive, merged in precedence order (the dashboard's defaults, the provider's variables, setVariables, then identify): the other SDKs' currentVariables(). A copy
client.getRecentEvents()Array<{ name, at, data? }> — the recent tracked events, oldest first (the last 10)
client.getAvailableActions()string[] — declared actions this build implements
client.setAppVersion(version)void — which build of your app the conversation happened in; it lands on the call record

Prompt variables

React NativeiOSAndroidFlutterKotlin Multiplatform
SetuseGraineVariables() → (vars) => void, client.setVariables(vars)setVariables(_:)setVariables(values)setVariables(values)setVariables(values)
Read the merged setclient.getVariables()currentVariables() -> [String: String]currentVariables(): Map<String, String>currentVariables()currentVariables(): Map<String, String> (each agent's too)

Merged in, later wins: the agent's defaults, the variables given at start-up, then these, then the customer's traits. null under a key removes it. Values become strings; everything is masked. The set reaches the next conversation; a conversation carries up to 12 variables, about 1 KB — past that, the biggest are left off first, who_called and call_reason last.

Actions

One handler per action declared on the agent.

useGraineAction(name: string, handler: (args: Record<string, unknown>) => ActionResult | Promise<ActionResult>): void
 
useGraineAction("set_language", ({ language }) => ({ status: "ok", message: `The app is now in ${language}.` }));

Registered on mount, removed on unmount.

Returns a function that unregisters (React Native's hook unregisters on unmount; the website has removeAction).

The handler gets the agent's arguments and answers a GraineActionResult — ok, refused or error, with a message for the agent. It runs on the main thread (iOS, Android). It has 6 seconds; after that the agent is told it did not finish. A throw is answered as error, an action this build has no handler for as refused, and every request gets exactly one answer.

{ "status": "ok", "message": "The app is now in Hindi.", "data": { "language": "hi" } }

To run one of your actions exactly as the agent would — same deadline, same answers — for a test or a screen of your own: client.executeAction(name, args) (React Native, Promise<ActionResult>) or executeAction(name, args) (Flutter, Future<ActionResult>). An unknown name answers refused; a throw or a timeout answers error.

checkActions compares what the build registered with what the agent declares:

{ "usable": ["set_language", "open_screen"], "undeclared": ["share_receipt"], "unhandled": ["apply_coupon"] }

Components

React NativeiOSAndroidFlutter
The component on screenuseGraineWidget() → widget, loading ({ id, kind } while one is on its way), answered, awaitingReply, closedGraineWidgetView; onWidgetGraineWidgetView; onWidgetGraineWidgetView, GraineComponents; onWidget
Answer itsubmit(data, summary)the view; answerComponent(widgetId:data:summary:)the view; answerWidget(widgetId, data, summary)chat.answerWidget(id, data, summary), the view
Close without answeringdismiss()the viewthe viewthe view
Put it backreopen()reopenComponent() -> Boolthe viewthe view

An answer is a turn: summary reaches the agent as the customer's sentence and data goes to your webhook — send both. A component closed without an answer is reported to the agent quietly.

React NativeAndroidFlutter
The component still waiting to be answeredclient.pendingWidgetpendingWidget: JSONObject?pendingWidget
Forget itclient.clearPendingWidget()clearPendingWidget()clearPendingWidget()
Answer from your own UIclient.submitWidget(widgetId, data, summary)answerWidget(widgetId, data, summary) on the instancechat.answerWidget(id, data, summary)
Draw one yourself — a preview, a testclient.presentWidget(frame), client.presentPendingWidget(hint)GraineWidgetView.present(frame), presentPending(hint)GraineComponents.present(frame), presentPending(hint)
What the view showsuseGraineWidget()GraineWidgetView.answered, closedWidget, isLoadingGraineComponents

The waiting component is kept because the view that draws it may not be on screen when it arrives; it is cleared when answered or dismissed, and when a new call starts.

React Native's timings, exported as constants: PENDING_WIDGET_MS (12,000 — how long a "component coming" loader may stand with nothing arriving), AWAITING_REPLY_MS (12,000 — how long the answered line waits for the agent's reply) and MIN_LOADER_MS (450 — a loader, once shown, stays at least this long).

Pointing at an element

React Native: useGraineHighlight(name: string): { highlighted: boolean }. The name is offered to the agent while the component is mounted; highlighted is true for up to 6 seconds when the agent points at it. Declare highlight_element on the agent; the SDK answers it.

Without the hook: client.registerHighlightable(name) offers a name and returns a function that withdraws it (names are cleared with their screen); client.getHighlightables() lists what can be pointed at now; and client.highlight(name, ttlMs = 6000) points, emitting highlight — null stops.

Masking your own formats

Platform
React NativeaddMaskRule(pattern: RegExp, token: string) from @graineai/inapp-react-native/mask
iOSGraineMasking.addRule(_ pattern: NSRegularExpression, token: String = "[REDACTED]")
AndroidMasking.addRule(pattern: Regex, token: String = "[REDACTED]")
FlutterMasking.addRule(RegExp pattern, [String token = '[REDACTED]'])
WebsiteGraineAgent.addMaskRule(pattern, token)

Once, at start-up. Rules apply to screens, variables and traits before they leave the device, after the built-in ones (PAN, Aadhaar, card and account numbers, phone numbers, email addresses).

React Native also exports the masking itself from the same module: maskString(input: string): string — the built-in rules, then yours — and maskDeep(value), which masks every string inside an object or array the same way, turns a Date into its ISO string (as JSON sends it), and leaves numbers and other values untouched.

Friction rules

Your app's layer over the dashboard's rules: friction on the React Native provider (and client.configureFriction(override)), and GraineConfig.friction on iOS, Android and Flutter. The rules and their defaults are on Friction detection.

On this page