Mihe

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"]
}
FieldRequiredMeaning
nameyesThe action's id. What the model calls, and what the audit log shows.
descriptionyesShown to the model. Say when to use it, not only what it does.
labelnoThe title on the confirmation card, such as "Refund order". Falls back to a readable form of name.
riskyeslow, medium or high. See permissions and risk.
inputSchemayesA JSON Schema for the input. Every call is validated against it before it is proposed or run.
executionTypeyesfrontend runs your handler in the browser. backend calls your server.
endpointbackend onlyWhere the signed call goes. Its origin must be allowed in the dashboard.
permissionsnoEvery one must be in the user's token. Empty means any signed-in user.
screensnoScreen ids where it is offered. Empty means everywhere.
requireConfirmationnoAsk before running, for a low or medium action. high always asks.
requireUiConfirmationnoOnly an on-screen tap confirms. high always requires one.
enablednofalse 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.

mihe-registry.json
{
  "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.json

The 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

.github/workflows/deploy.yml
- 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.json

Publish 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.

On this page