Actions
Register what the assistant may do, and publish it from your build.
An action is one thing the assistant may do for a user. You describe it in the registry, which says what it is and who may run it, and you supply the code that runs it, either in the browser or on your backend.
Describing an action
{
"name": "refund_order",
"label": "Refund order",
"description": "Refund an order to the customer's original payment method. Use when the user asks to refund, or says a customer was charged twice.",
"risk": "high",
"inputSchema": {
"type": "object",
"properties": {
"orderId": { "type": "string" },
"reason": { "type": "string", "enum": ["duplicate", "returned", "other"] }
},
"required": ["orderId", "reason"],
"additionalProperties": false
},
"executionType": "backend",
"endpoint": "https://api.example.com/mihe/refund-order",
"permissions": ["orders.refund"],
"screens": ["orders"]
}| Field | Required | Meaning |
|---|---|---|
name | yes | The action's id. What the model calls, and what the audit log shows. |
description | yes | Shown to the model. Say when to use it, not only what it does. |
label | no | The title on the confirmation card, such as "Refund order". Falls back to a readable form of name. |
risk | yes | low, medium or high. See permissions and risk. |
inputSchema | yes | A JSON Schema for the input. Every call is validated against it before it is proposed or run. |
executionType | yes | frontend runs your handler in the browser. backend calls your server. |
endpoint | backend only | Where the signed call goes. Its origin must be allowed in the dashboard. |
permissions | no | Every one must be in the user's token. Empty means any signed-in user. |
screens | no | Screen ids where it is offered. Empty means everywhere. |
requireConfirmation | no | Ask before running, for a low or medium action. high always asks. |
requireUiConfirmation | no | Only an on-screen tap confirms. high always requires one. |
enabled | no | false publishes it switched off. Defaults to true. |
The label and description are what a user reads on the confirmation card,
and the fields under them are exactly the input the action will receive. None
of it is written by the model. Write them as you would the copy on a button
that moves money.
Tight schemas help twice: the model makes fewer mistakes, and an enum or a pattern refuses bad input before it reaches your code.
Scope actions to the screens they belong on. Every action the user may run
on the current screen is described to the model on every answer, in typed and
spoken conversations alike. An action with no screens is offered everywhere.
An app with forty unscoped actions sends about six thousand tokens of tool
definitions with each answer, most of them for screens the user is not on.
Scoping them makes each answer cheaper, and the model chooses better from a
shorter list. navigate still takes the user anywhere.
Frontend actions
Handle it with useAssistantAction on the page that can run it:
useAssistantAction({
name: "show_unpaid_orders",
handler: (input) => {
setFilter("unpaid");
return { shown: unpaidCount };
},
});Return something. The return value is the result the model reads, so
{ shown: 7 } tells it more than undefined. Throwing is fine: the assistant
tells the user it failed instead of claiming it worked.
Return within a few seconds. The runtime waits about ten seconds for a handler to answer, then tells the assistant the action failed. So a handler must never wait for the user: not for a confirmation dialog, a password prompt, a file picker or a form. Open what they need, return at once saying what you opened, and let them finish it there:
useAssistantAction({
name: "start_refund",
handler: ({ orderId }) => {
openRefundDialog(String(orderId)); // your dialog, with its own checks
return { opened: "refund dialog", nothingChangedYet: true };
},
});The assistant then says the dialog is open rather than that the refund is done. This is also how to keep your own safeguards in charge, such as re-authentication before a sensitive change: Mihe prepares, the user decides. A handler that runs past five seconds logs a warning in the browser console, because otherwise the first sign is an assistant reporting as failed something the user then completed.
After a confirmed action finishes, onActionComplete on the provider fires.
Invalidate your queries there, or the page goes on showing what was true before
the assistant changed it.
Backend actions
The runtime calls your endpoint with a signed POST and gives the model your
response. Verifying action calls says what arrives and
what your endpoint must check.
Before publishing one, add your backend's origin, such as
https://api.example.com, under Backend endpoints in the app's settings.
A new app allows none, and a publish naming any other origin is refused. That
is what stops anyone holding your publish key from sending your users' calls
somewhere else.
Publishing
The registry is published from your build, so it changes with the code that implements it and is reviewed with it. A publish replaces the whole registry: a screen or action you removed from the file stops being offered.
{
"screens": [],
"components": [],
"actions": [],
"workflows": []
}Every key is optional. components is only needed if a
workflow points at one. Otherwise components come from the
page at runtime.
Publish with the app's publish key (reg_…). Create it on the app's page
in the dashboard. It is shown once, and it publishes the registry and does
nothing else:
curl -X PUT https://runtime.mihe.dev/v1/registry \
-H "Authorization: Bearer $MIHE_PUBLISH_KEY" \
-H "Content-Type: application/json" \
--data @mihe-registry.jsonThe response says what was published, and whether anything changed:
{ "hash": "…", "screen_count": 4, "action_count": 9, "workflow_count": 1, "changed": true }A refused publish changes nothing. The error names the problem: an invalid input schema, an action scoped to a screen you did not publish, a workflow pointing at something that does not exist, or a backend endpoint on an origin you have not allowed.
From CI
- name: Publish the Mihe registry
run: |
curl --fail-with-body -X PUT https://runtime.mihe.dev/v1/registry \
-H "Authorization: Bearer ${{ secrets.MIHE_PUBLISH_KEY }}" \
-H "Content-Type: application/json" \
--data @mihe-registry.jsonPublish after the code that handles a new action is deployed, not before, so the assistant never offers something your app cannot yet run.
Settings changed in the dashboard (enabled, risk tier and the two confirmation flags) survive a publish. A deploy cannot quietly undo them.