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.
{
"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.
{
"workflow": "stripe-refund",
"description": "Refund a paid invoice. Waits for Ana or Ben.",
"approval": {"approvers": ["ana@acme.com", "ben@acme.com"]}
}
role:adminEvery admin.
role:managerEvery 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:developerandrole:viewerare 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, gets403when 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·runIdThe workflow, and the run that waits.
name·messageWhat 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 getsapproved: falseandby: 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/approvalslists a run's requests only withkinds=workfloworkinds=agent,workflow. Withoutkinds, 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.
# 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."}'
kindagentfor a held tool call,workflowfor a workflow run.tool·argumentsThe workflow, and the JSON the model asked to run it with.
agent·conversationThe agent that asked, and the conversation it asked in.
person·personLabelWho was talking to the agent: their id, and their email or API key id.
statuspending,approved,deniedorexpired.requestedAt·expiresAt·decidedAtMilliseconds since the epoch.
decidedBy·noteThe approver's email or API key id,
expiryfor an expired request, and the approver's note.approversThe roles and member emails that may answer it, or
nullfor every admin. See Who approves.
- Answering needs the
approval.decidecapability, 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=takespending(the default),approved,denied,expiredorall, and lists 100 at most.kindstakesagent(the default),workfloworagent,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,workflowlists expired requests. TheirdecidedByisexpiry.
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": falseonPUT /v1/alertsturns 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.
| Route | Method | Minimum role |
|---|---|---|
/v1/approvals ?status=pending (default), approved, denied, expired or all; ?kinds=agent (default), workflow or agent,workflow; newest first, 100 at most | GET | admin |
/v1/approvals/:id {"decision": "approve" | "deny", "note"?}; the request, answered | POST | admin |