Actions
Let the agent fill a field, move to a step or retry an upload on the customer's behalf — and hear honestly whether it worked.
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 it says next depends on whether the field accepted the value.
In one minute: declare the action on the agent, write a handler with the
same name in your app, return ok, refused or error with one sentence.
Both halves are required.
1. Declare it
Agent → Embed & Widgets → Actions → What this agent may do in your app. Add a name, a description written for the model, and JSON-Schema parameters. Press Save — that is what pushes the catalogue to the agent.
| Key | Rules |
|---|---|
name | Lowercase, digits and underscores, starting with a letter. This is what the model calls. |
description | For the model. Say when to use it, not how it works. |
parameters | JSON Schema. An empty object when it takes no arguments. |
An enum is worth adding wherever the value is one of a known set: an action
declared with enum: ["light","dark","system"] is one the model cannot get
wrong.
An argument can be a list or an object too — a set of answers, a whole plan
with its days and items. In the panel, give that argument the type list or
object and write its shape as JSON Schema: a list needs items, an object
its properties. An imported file keeps every argument exactly as written, and
the panel names anything a model provider would refuse (a list with no items,
a shape with no type) before you save. Your handler receives lists and maps.
Names must match your useGraineAction handlers exactly. A name one
underscore out declares a tool your app refuses for the life of the release,
and the agent keeps trying it.
Already keep the list in source control? Actions → Import from JSON
takes a bare array, or an object with appActions or actions around one — the
SDK's convention is docs/agent-actions.json. Import replaces the panel
rather than merging, so the dashboard ends up matching the file rather than
keeping a stale action nobody implements.
2. Implement it
In the screen that performs it, not at the root:
It registers on mount and unregisters on unmount, 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.
Returning ok 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 often more useful than a success — "that field is
locked after submission" is exactly what they need to hear, and only your code
knows it. message is read by the agent, not shown to the customer; it decides
how to phrase it.
Answer quickly
Six seconds. A handler that has not resolved by then is answered as a
timeout, 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.
For anything slower, return immediately and report the outcome as screen state:
The next useGraineScreen update tells the agent how it went. A handler that
blocks for ten seconds produces an agent that has already apologised.
What people build with it
| Action | What the agent does |
|---|---|
| Fill a field | Completes details it can look up |
| Navigate | Moves them to the step they are asking about |
| Highlight | Draws attention to the control they cannot find |
| Retry | Re-runs a failed upload or mandate |
| Escalate | Hands over to a person, with context attached |
An action declared but missing from the running build is refused immediately and the agent explains what to tap instead — so 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.
availableActions in the screen context narrows what the agent is allowed to
do — one the current screen does not list is refused. It does not narrow the
tool list the model sees; that is built once per call from the whole declared
catalogue (capped at 24).
So a shared agent offers the model your website's tools inside your app. It spends tokens on tools that cannot work, picks worse because the real ones are buried, and occasionally calls one and has to recover mid-conversation.
| 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 |
Share a knowledge base. Never share a tool list.

