shiftlabs Start free
Menu Approvals

Approvals

A workflow tool marked "approval": "always" never runs on the model's word alone, and a workflow can stop for an approver with step.waitForApproval. Each request waits until an admin, or the approvers it names, approves or denies it.

How a call is held

When the model calls a tool marked "approval": "always", nothing runs. The call becomes a request in the workspace's approvals, and the conversation reads waiting for approval. The agent tells the person it is waiting.

jsontool
{
  "workflow": "stripe-refund",
  "description": "Refund a paid invoice, in full or in part. Waits for an approver.",
  "parameters": {
    "type": "object",
    "properties": {
      "invoice": {"type": "string"},
      "amount": {"type": "integer", "minimum": 1}
    },
    "required": ["invoice"]
  },
  "approval": "always"
}
  • A request holds the exact arguments the model asked for. An approval runs those arguments.
  • Only workflow tools can wait for approval. Code tools have no network and never do.
  • Each template holds its tools that write to another service for approval, except the Slack post to your own channel.

Who approves

"approval": "always" goes to every admin. To send a tool's calls to certain people, name them with "approval": {"approvers": [...]}; a workflow names them with step.waitForApproval's approvers.

jsontool
{
  "workflow": "stripe-refund",
  "description": "Refund a paid invoice. Waits for Ana or Ben.",
  "approval": {"approvers": ["ana@acme.com", "ben@acme.com"]}
}
  • role:admin

    Every admin.

  • role:manager

    Every manager and admin. Until the manager role arrives, that is every admin.

  • A member's email

    The email they sign in with. An agent's tools can name only members who are admins when you deploy; a deploy that names anyone else is refused.

  • List up to 20 roles and emails. A developer never approves, so role:developer and role:viewer are refused.
  • Only the approvers a request names see it in the Approvals screen and in GET /v1/approvals, and only they get its notice. Anyone else, admins included, gets 403 when they try to answer it.
  • API keys have no email: a key answers a request only if it names a role the key holds, or names nobody.
  • A named member who leaves the workspace, or stops being an admin, drops out of the request. When nobody it named is left, it goes to every admin. If they come back as an admin, later requests name them again.
  • A workflow's request can name anyone. Within 15 minutes of the request, shiftlabs checks the members it names, whether or not approval notices are on. Anyone who isn't an admin of the workspace drops out, and if nobody is left the request goes to every admin. A member already found not to be an admin drops out at once.
  • In a workspace with no members, such as one used only through API keys, a request that names only members goes to every admin at once.

Workflow runs

A workflow asks with step.waitForApproval(name, { message, timeout, approvers }), and its run waits for the answer. See Approvals in workflows.

  • workflow · runId

    The workflow, and the run that waits.

  • name · message

    What the workflow called the request, and its message for the approvers.

  • The run gets { approved, by, note } once you answer. Unanswered by its timeout, 72 hours by default, it gets approved: false and by: null.
  • A key scoped to some workflows lists and answers only their runs' requests. A key whose scope names agents alone isn't limited here: it reaches every run's requests.
  • A run's requests are listed and answered in every workspace, with or without agents.
  • GET /v1/approvals lists a run's requests only with kinds=workflow or kinds=agent,workflow. Without kinds, it lists agents' requests only.
  • A run that ends while its request waits, for example when it is stopped, takes the request out of the inbox: it expires.

Approve or deny

Answer requests on the Approvals screen, in the conversation itself, or with the API. A request that names approvers is answered only by them; one that names none, by any admin.

shellcurl
# Requests waiting for an answer, agents' and workflow runs', newest first (admin key).
curl "https://api.shiftlabs.org/v1/approvals?kinds=agent,workflow" \
  -H "authorization: Bearer $SHIFTLABS_API_KEY"

# Approve: the workflow runs with the arguments in the request.
curl -X POST https://api.shiftlabs.org/v1/approvals/<id> \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" -H "content-type: application/json" \
  -d '{"decision": "approve"}'

# Deny, with a note the agent reads.
curl -X POST https://api.shiftlabs.org/v1/approvals/<id> \
  -H "authorization: Bearer $SHIFTLABS_API_KEY" -H "content-type: application/json" \
  -d '{"decision": "deny", "note": "Not this month."}'
  • kind

    agent for a held tool call, workflow for a workflow run.

  • tool · arguments

    The workflow, and the JSON the model asked to run it with.

  • agent · conversation

    The agent that asked, and the conversation it asked in.

  • person · personLabel

    Who was talking to the agent: their id, and their email or API key id.

  • status

    pending, approved, denied or expired.

  • requestedAt · expiresAt · decidedAt

    Milliseconds since the epoch.

  • decidedBy · note

    The approver's email or API key id, expiry for an expired request, and the approver's note.

  • approvers

    The roles and member emails that may answer it, or null for every admin. See Who approves.

  • Answering needs the approval.decide capability, which admins hold, and to be one of the request's approvers. A key scoped to some agents or workflows lists and answers only their requests. A kind the scope leaves out is not limited: a key scoped to some agents also lists and answers every workflow run's request, and one scoped to some workflows every agent's.
  • GET /v1/approvals?status= takes pending (the default), approved, denied, expired or all, and lists 100 at most. kinds takes agent (the default), workflow or agent,workflow.
  • A request is answered once. Answering it again, or after it expired, returns 409 {"error": "already decided"}.
  • A note is up to 500 characters. The agent reads it when you deny; a workflow run gets it with either answer.

What the agent hears

Each answer is posted into the conversation as a message that starts with Approval update:, and the agent replies to the person. People can't send a message that starts that way.

  • Approved

    The workflow runs with the approved arguments. The agent reads the run's output or error, as for any tool call.

  • Denied

    The workflow doesn't run. The agent reads your note, if there is one.

  • Expired

    Nobody answered within 72 hours. The workflow doesn't run.

  • An approved run counts like any run, and is listed with origin agent.
  • If posting the answer into the conversation fails, it is tried again every 10 minutes for 2 hours.

Expiry

A request waits 72 hours, or the timeout its workflow set. Unanswered, it expires and counts as denied.

  • The Approvals screen shows how long each request has left, and how many run out within 24 hours.
  • GET /v1/approvals?status=expired&kinds=agent,workflow lists expired requests. Their decidedBy is expiry.

Approval notices

Waiting requests are sent to the workspace's alert emails and alert webhook, as approvals.waiting. A request that names members goes to them instead.

  • A notice goes out a minute after the first request, then at most once every 15 minutes. It counts the requests still waiting, lists up to 3, and links the Approvals screen. Tool arguments and workflow messages are never included.
  • A request that names only members is emailed to those members, whether or not they get alert emails, and never to the webhook or other alert emails. One that also names a role goes to the alert destinations as well.
  • Notices are on by default. "approvals": false on PUT /v1/alerts turns them off. See Alert webhooks for the body and its signature.
  • Notice emails count toward the plan's alert emails per day: 50 on Free, 500 on Pro.

API

Admins list and answer requests with the /v1 API.

Routes
RouteMethodMinimum role
/v1/approvals ?status=pending (default), approved, denied, expired or all; ?kinds=agent (default), workflow or agent,workflow; newest first, 100 at mostGETadmin
/v1/approvals/:id {"decision": "approve" | "deny", "note"?}; the request, answeredPOSTadmin