Actions
Let the agent fill a field, move to a step, or retry an upload on the customer's behalf — and tell it honestly whether the change actually landed.
An action is something the agent can do inside your app. It calls one, your code runs, and it waits to hear what happened before saying anything.
That wait is the whole design. What the agent says next depends entirely on whether the field accepted the value.
Declare it first
Actions are declared on the agent, server-side. The app can only implement what has been declared — it can never invent one.
Or paste the file your app already ships
Embed & Widgets → App actions → Import from JSON takes the list your app
keeps in source control — the SDK's convention is docs/agent-actions.json —
and loads every action in one step.
It accepts a bare array, or an object with appActions or actions around one,
which is the shape you would send to the API anyway. enum constraints survive
the round trip: an action declared with enum: ["light","dark","system"] is one
the model cannot get wrong, and the same action without it invites a value your
app has to refuse in front of the customer.
Import replaces what is in the panel rather than merging. That is deliberate — the point is to make the dashboard match the file, and merging leaves a stale action declared here, implemented nowhere, and offered to the model anyway.
Nothing is live until you press Save, which is what pushes the catalogue to the agent.
Names must match your useGraineAction handlers exactly. This is the whole
reason to import rather than retype: a name one underscore out declares a tool
your app will refuse for the life of the release, and the agent will keep
trying it.
| Key | Rules |
|---|---|
name | Lowercase, digits and underscores, starting with a letter. This is what the model calls. |
description | Written for the model. Say when to use it, not how it works. |
parameters | JSON Schema. Use an empty object when the action takes no arguments. |
Declaring is not deploying. Changes reach live agents when the dashboard syncs them — saving the agent does this. Until then, agents serve the catalogue they last received.
Implement it where it belongs
The action exists while the component is mounted and is withdrawn when it unmounts, so the agent is never offered "retry upload" on a screen with no upload.
Answer honestly
The return value decides what the customer is told.
ok makes the agent tell the customer the thing is done. Returning it when the
field silently rejected the value is the worst failure this feature has — the
customer is told their form is complete and finds out later that it is not.
A refusal with a reason is usually more useful than a success. "That field is locked after submission" is exactly what the customer needs to hear, and only your code knows it.
Answer quickly
You have six seconds — ACTION_DEADLINE_MS. A handler that has not resolved
by then is answered as a timeout on the runtime side, and the agent tells the
customer it was not done rather than guessing. A handler that throws is
answered as error for the same reason.
Registration is per mounted screen: useGraineAction registers on mount and
unregisters on unmount, so the agent is offered only what the screen in front
of the customer can do. Register in the screen that performs the action, not
at the root.
For anything slower, return immediately and report the outcome as screen state when it finishes:
The next useGraineScreen update tells the agent how it went. A handler that
blocks for ten seconds produces an agent that has already apologised for failing.
What you can build
| Action | What the agent does |
|---|---|
| Fill a field | Completes KYC details it can look up |
| Navigate | Moves the customer to the step they are asking about |
| Highlight | Draws attention to the exact control they cannot find |
| Retry | Re-runs a failed upload or mandate without making them hunt for it |
| Escalate | Hands the conversation to a person, with context attached |
Not implemented yet? That is fine
An action declared on the agent but missing from the running app build is refused immediately and the agent explains what to tap instead. Your dashboard and your app releases do not have to ship together.
One agent per surface
Actions are declared on the agent, and the model is offered every one of them on every surface that agent serves.
This matters more than it sounds. available_actions in the screen context
narrows what the agent is allowed to do — an action the current screen does not
list is refused with not_on_this_screen. It does not narrow the tool list
the model sees. Those tools are built once when the call starts, from the whole
declared catalogue.
So an agent serving both a website and a mobile app offers the model both sets of tools in both places. The model spends tokens on tools that cannot work, picks worse because the real ones are buried among them, and occasionally calls one and has to recover from a refusal mid-conversation. The runtime caps the catalogue at 24 for exactly this reason.
Give each surface its own agent. AgentEmbed is unique per agent, so each one
gets its own publishable key, domain allowlist, appearance and action list —
and, just as importantly, its own prompt. A prompt written to guide someone
through KYC is not the prompt that should answer questions on a marketing page.
A useful split:
| Surface | Agent | Actions |
|---|---|---|
| Marketing site | site-assistant | scroll_to_section, open_faq, download_app |
| Mobile app | app-assistant | open_setting, set_language, set_app_lock, … |
Both can share a knowledge base. What they must not share is a tool list, because one of them can never use half of it.

