Friction detection
The agent notices the customer is stuck — idle on a step, a field rejecting them, going back and forth, rage taps, moving on with blanks, an error banner — and speaks up first. Every rule is JSON, per screen, configured in the dashboard.
The promise of an in-app agent is that it speaks at the right moment. That moment is not "forty-five seconds on any screen": someone reading terms is not stuck; someone whose PAN field has rejected the same value twice is. Friction detection reads what your screens already report and turns it into six named signals the agent can act on.
The six signals
| Signal | Fires when | What the agent hears |
|---|---|---|
stalled_on_step | untouched for idle.afterMs | The customer has not touched the "Details" screen for 45s. The "PAN" field is showing: Invalid format. |
repeated_field_error | the same field shows an error count times in windowMs | The "PAN" field on the "Details" screen has rejected the customer 2 times — it is showing: Invalid format. |
looping_between_screens | back on the same screen visits times in windowMs | The customer has come back to the "Address" screen 3 times in the last 45s without getting past it. |
rage_taps | a tracked tap taps times in windowMs | The customer tapped "pay" 4 times in 1.5s on the "Checkout" screen — it may not be responding. |
incomplete_submit | a tap that means "carry on" while a field is blank or rejected | The customer pressed "Next" on the "Details" screen with 2 still to fill: PAN number, Date of birth. Tell them what is missing before they hit the error. |
screen_error | the screen reports an error banner | The "Upload" screen is showing an error: Upload failed. |
Each signal is sent to the agent as an app event. The agent still decides
whether to speak — never while it is already talking, never inside
nudge.cooldownSeconds of its last intervention, never past
nudge.maxPerSession — and your app hears the same signal as
friction_detected on useGraineEvents.
Nothing needs instrumenting beyond what screen context
already reports: fields[].error and status drive the first two rules,
screen ids drive the third, and taps drive the fourth — the ones you name with
useGraineTap, and, with fab or captureTaps on the provider, every tap
the SDK captures on its own (tracked as tap:screen; the sentence then says
"tapped the screen" rather than naming a control).
Configure it in the dashboard
Agent → Embed & Widgets → Friction. Every rule is a switch with its numbers under it; screens that are different get their own row; the pacing is its own section. Import or export the whole thing as JSON. Saved rules reach every app and site running the agent with its next session — no release.
- Any rule set to
falseis off.enabled: falseswitches everything off. - A screen listed under
screenskeeps every rule except what it changes;ignore: truemeans no detection on that screen at all. messagereplaces the built-in sentence. Placeholders:{title} {screen} {field} {error} {count} {seconds} {tap}.incompleteSubmit.tapsare matched as lowercase substrings of the tap name, sosubmitalso catchessubmit_application. Give it your own words if your buttons are called something else —agree,book,आगे. Which fields count as missing is not configured: a field you report withstatus: "empty"or"invalid", or one carrying anerror, is outstanding; anything else is not.cooldownMsis the minimum gap between two signals on the same screen.nudge.openLauncheris for apps that draw their own launcher: a signal outside a conversation should open it. The SDK carries the flag; your app decides what "open" means.
Overriding from the app
The dashboard's rules sit under the app's. Pass rules from the app only for the screens it knows better than the dashboard can — the ones with no task on them:
or, on a module-level client, client.configureFriction({...}) at any time.
The layering is defaults ← dashboard ← app; client.friction is the resolved
result. The older single knob stallAfterMs still works and maps onto idle.
Reacting in the app
What Call History shows
Every signal is on the call's timeline in Call History — with whether the agent spoke to it and, if not, why (it was mid-sentence, it had just spoken up, it had reached its limit). Beside it: the screens the customer moved through, the taps you tracked, and every action the agent took with the app's own answer. All of it exports to CSV.

