Skip to documentation
Documentation

Governed actions

Approve and audit ask-gated developer tool calls.

Some tool calls are safe to run the moment the model asks for them. Others change a plan, charge a card, delete a record, or email a customer, and someone should see exactly what is about to happen and say yes first. Governed actions are how Keiki handles the second kind, for every tool the agent has, on every channel, without you building approval UI.

A governed action is a durable record, not a prompt convention. It is created when the model proposes an ask-gated dispatcher tool, shown as a card in Slack or a conversational question, decided by a person, executed by the platform from the stored record, and closed with an outcome the agent and webapp can see.

Governed actions are rolling out in phases. This page marks what is live and what is future; unsupported policy fields are rejected.

Tool policy

A tool opts in by declaring a policy beside its schema.

Live: requiredScopes

JSON
{ "requiredScopes": ["billing:write"] }

Scopes the caller must hold, as the identity webhook reported them. The dispatcher checks them before the tool's implementation runs, on every channel; a caller without them gets a refusal the model is told to relay. Accepted on:

Tool sourcePolicy comes from
SDK defineToolA policy property beside execute; the SDK ships it on kit.sync().
Custom tool (POST /developer/tools)A policy field in the upload body.
Deployment tool (POST /developer/deployments)A policy field on the matching entry in tools[].

Unknown policy keys are rejected, so a typo cannot silently leave a tool ungoverned. MCP tools use the same persisted action lifecycle. A turn that has no verified speaker at all — no identity webhook on the agent, or a harness that does not carry the sender — refuses every tool with requiredScopes, so declaring them is safe before the webhook is configured.

Live: approvals

JSON
{
  "approval": "ask",
  "requiredScopes": ["billing:write"]
}
Field
approvalallow (default): runs immediately. ask: a person decides first. Other values are rejected.
requiredScopesAs above; any approver must hold them too.
previewToolNames a tool in the same tool set that must run first with the same canonical arguments. The preview tool must be called with the write's arguments minus descriptionArg.
freshnessFieldsDot paths in the preview output to compare again at approval time. Requires previewTool.
descriptionArgArgument containing a model-authored walkthrough; it is shown on the card but excluded from the argument hash.

Preview pairing, freshness checks, material-field diffs, and walkthroughs are live. MCP tools do not add those fields during replay.

Lifecycle

plaintext
pending → executing → completed
                  ↘ failed
        ↘ declined
        ↘ superseded
StatusMeaning
pendingA card or text question is outstanding. Nothing has run.
executingApproval was claimed and the stored call is running. It is not yet settled and must not be treated as success. If the process crashes, the row remains executing; this phase has no recovery sweep.
completedThe stored call finished successfully.
failedThe tool errored. The error is on the record; nothing is retried without a new decision.
declinedSomeone said no.
supersededA newer proposal replaced this one, or its preview changed before approval. Nothing ran from this record.

The record keeps the tool name, exact arguments, who proposed it, who decided it, and what happened, so webapp readers can answer who approved it and what it did.

No drift

The card a person approves and the call Keiki makes are the same thing, twice over.

The arguments are the arguments. When the model calls a gated tool, its arguments are canonicalised (keys sorted and undefined values dropped), stored on the record and hashed. Approval executes that stored call. The model does not get another turn between "yes" and the tool running, so there is nothing to rephrase, and a later prompt change cannot alter what an already-approved action does.

If previewTool is configured, the dispatcher requires a matching preview from the same agent and conversation within 24 hours. The hash sorts keys recursively and excludes only descriptionArg; the original arguments, including that description, remain stored. At approval time, configured freshnessFields are re-read from a fresh preview. If any field differs, the action is marked superseded, a new pending action is created with the fresh preview, and the approver is asked to review again. A preview refresh failure fails closed and records Could not refresh preview: .... Re-proposing a pending action also supersedes the older proposal.

Groups

Actions proposed in one turn share a group id. When at least two are pending, the agent sees a group summary and Slack shows Approve all and Decline all. Approve-all runs actions in creation order and stops at the first failed or superseded action; later actions remain pending. Decline-all settles every pending action in the group. The web route POST /api/webapp/agents/:id/actions/groups/:groupId/decide accepts {"decision":"approve"} or {"decision":"decline"} and returns per-action results.

Deployments may configure action settings:

JSON
{
  "actions": {
    "view_url": "https://dashboard.example/actions/{actionId}",
    "webhook_url": "https://api.example/keiki/actions",
    "secret": "at-least-16-characters"
  }
}

At least one of view_url or webhook_url is required. A view URL must contain {actionId} or {groupId} and may also use {conversationId} and {toolName}. Each placeholder is substituted with a URL-encoded value. The webhook receives action.<status> events with agent_id, external_ref, and the public action record (never its API key). Requests use X-Keiki-Timestamp and X-Keiki-Signature headers, signed as described in the identity signing section. Webhook delivery uses a five-second timeout, no retries, and never blocks action execution.

Where actions show up

Slack. A card in the thread with the summary and buttons. Buttons check the presser's scopes; a second press on a decided card is a no-op, and a press by two people at once yields one execution. After the decision the card updates in place with the outcome. Actions expire after 24 hours.

Words. "yes", "go ahead", "no, leave it" in the thread can decide the action: the agent calls action_respond with the action id or group id, decision, and exact words. It never approves on someone's behalf.

Webapp. GET /api/webapp/agents/:id/actions?status=&limit= lists actions and POST /api/webapp/agents/:id/actions/:actionId/decide lets an owner or admin decide; POST /api/webapp/agents/:id/actions/groups/:groupId/decide decides every pending action in a group. A dashboard Actions UI is future work.

What the agent knows

Every turn starts with an Action state section listing the conversation's open and recently closed actions — status, summary, who decided, the result or error — so the agent can answer "did that go through?" without a tool call and does not propose the same change twice while one is pending. The section is written by the platform from the records.

The agent's instructions for gated tools are also the platform's: propose, then stop and wait — no "I've applied it" until the record says completed.

Approval window and stopping

A pending action waits for a decision independently of the turn that proposed it. The turn ends; the agent replies that it is waiting; the record stays pending until it is decided or expires. An approved call may remain executing if its process crashes; callers must treat that status as not-yet-settled, never as success. A stop command aborts a running turn and leaves pending actions where they are — stopping is never an approval or a decline.

MCP Actions

MCP ask tools use persisted agent actions. The MCP path is stored as the Action toolName, exact arguments are retained and replayed on the daemon after approval, and URL or structured-form elicitations remain daemon-owned through mcp_resume. MCP Actions currently do not add requiredScopes, previewTool, or freshnessFields; those fields are not fabricated during MCP replay. Personal-scope approvals must be made by the requester, because the action replays against that person's own MCP connection. The current MCP permission is checked again at approval time; if the preset is disconnected or the tool is denied, the action fails without calling the daemon.