Docs/Approvals
Concepts

Approvals

Two classes of approval, and how to answer each.

View as Markdown

Sai stops and asks before some actions: running a command, choosing between options, connecting a Google account, typing a site password, verifying a phone. Each ask is an approval request. Your loop must answer it or the task waits.

An approval arrives as an event of type data-approval-request, and GET /events reports status: "needs_approval" while it is pending.

The one field that matters

Look at isLinkOnly first. It splits approvals into two classes.

isLinkOnly: false: answer over the API

Command approvals (approvalType: "exec"), action approvals and choice questions (approvalType: "choice"). Answer with POST /approve.

json
{
  "type": "data-approval-request",
  "data": {
    "approvalId": "ap_7Qw",
    "title": "Command Approval Required",
    "description": "Sai wants to run a command.",
    "approvalType": "exec",
    "isLinkOnly": false,
    "approvalUrl": "https://sai.simular.ai/approval/3fA9kQ2xYz/ap_7Qw?from=api",
    "command": "powershell -c Get-ChildItem C:\\Users\\sai\\Desktop",
    "cwd": "C:\\Users\\sai"
  }
}
bash
curl -s "$SAI_API_URL/v1/agents/approve" \
  -H "Authorization: Bearer $SAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "approvalId": "ap_7Qw", "response": "yes" }'

response is one of:

ValueEffect
yesAllow this one request.
taskAllow this and stop asking for the rest of the current task.
noRefuse. Sai continues without it.

A choice question carries questions. Answer it with selections, one inner array per question:

json
{
  "type": "data-approval-request",
  "data": {
    "approvalId": "ap_9Lm",
    "title": "Which folder?",
    "description": "",
    "approvalType": "choice",
    "isLinkOnly": false,
    "approvalUrl": "https://sai.simular.ai/approval/3fA9kQ2xYz/ap_9Lm?from=api",
    "questions": [
      {
        "message": "Which folder should I save the report to?",
        "options": [
          { "value": "desktop", "label": "Desktop" },
          { "value": "documents", "label": "Documents" }
        ],
        "multiple": false,
        "allowOther": false
      }
    ]
  }
}
json
{ "approvalId": "ap_9Lm", "response": "yes", "selections": [["desktop"]] }

A value that was not offered is rejected with 422, unless the question has allowOther: true or no options, in which case free text is accepted.

isLinkOnly: true: open approvalUrl in a browser

Google connect (service_connect), a site password or other typed input (user_input) and phone verification (phone_verification). These need a human in a browser. The API cannot answer them.

json
{
  "type": "data-approval-request",
  "data": {
    "approvalId": "ap_2Vd",
    "title": "Connect your Google account",
    "description": "Sai needs access to Gmail to read your inbox.",
    "approvalType": "service_connect",
    "isLinkOnly": true,
    "approvalUrl": "https://sai.simular.ai/approval/3fA9kQ2xYz/ap_2Vd?from=api"
  }
}

Hand approvalUrl to the person who owns the account. Keep polling GET /events. When they finish, status returns to running.

Calling POST /approve on a link-only approval returns 409:

json
{
  "error": "link_only",
  "message": "This approval must be completed in a browser. Open approvalUrl.",
  "approvalUrl": "https://sai.simular.ai/approval/3fA9kQ2xYz/ap_2Vd?from=api"
}

Finding the pending approval

GET /events repeats the blocking approval in a top-level approval field whenever status is needs_approval, with the same shape as the event's data. Read that instead of scanning events; the event itself is delivered once, on the poll where it first appeared.

Expiry

Some approvals carry expiresAt, a Unix time in milliseconds. After it passes, POST /approve returns 409 because the request is no longer pending.

Timing

A free account's first request of the day can take about two minutes while its computer starts. Nothing is wrong. Keep polling with wait=30 and act on the first approval when it arrives.

Note

approvalUrl, expiresAt and the 409 link_only answer are available from October 1. The URL is https://sai.simular.ai/approval/{uid}/{approvalId}?from=api, where uid is the userId from GET /auth; on staging the host is the staging web app.