Approvals
Hold a tool call until a person approves it.
A tool that can do damage should wait for a person. When the model calls it, Plico records the request, parks the run, and runs the tool only after someone approves. The wait is part of the event log, so it survives a restart of the process.
Require approval
Declare it on the tool, with an optional reason that is shown to whoever decides:
import { defineTool } from "@plico-ai/sdk";
export default defineTool({
name: "close_ticket",
description: "Close a support ticket. This cannot be undone.",
inputSchema: {
type: "object",
properties: { ticketId: { type: "string" } },
required: ["ticketId"],
additionalProperties: false,
},
capabilities: ["ticket:write"],
approval: { required: true, reason: "Closing a ticket cannot be undone." },
handler: async ({ ticketId }) => ({ ticketId, status: "closed" }),
});Set a policy in the config
plico.config.ts can raise or lower the bar for the whole project:
export default defineConfig({
schemaVersion: 1,
name: "My agent",
model: { provider: "anthropic", name: "claude-sonnet-5-5" },
approvals: {
default: "required", // every tool waits unless it says otherwise
tools: { create_ticket: "none" }, // except this one
timeoutMs: 10 * 60 * 1000, // give up after 10 minutes
},
});| Key | Meaning |
|---|---|
default | "none" (the default) or "required" for tools with no override. |
tools | Per-tool override, "required" or "none". |
timeoutMs | How long a run waits for a decision. The default is 30 minutes. |
The config can add a requirement but never remove one: a tool that declares
approval: { required: true } still waits when the config says "none". The
Readiness view warns about overrides that name a missing tool or
conflict with a tool's own requirement.
What happens during a wait
- The model calls the tool. Plico records
tool.call, thenapproval.requestedwith arequestId, the tool name, its arguments and the reason. - The run stops with
run.waiting. Its status iswaiting. No tool code has run. - A decision arrives. Plico records
approval.decidedand, ifapprovedistrue, runs the tool and carries on.
Only an explicit approved: true lets the tool run. A denial, or anything else, ends the run
with run.error and run.failed in the approval phase, and the tool never runs. A timeout ends
it the same way, with no approval.decided event.
Decide
In Studio, open the run, read the arguments and the reason, and choose Approve tool call or Deny tool call. See Studio.
Over HTTP, send a signal named after the request:
curl -X POST http://127.0.0.1:3000/v1/runs/<run-id>/signal \
-H "content-type: application/json" \
-d '{ "name": "approval:<requestId>", "value": { "approved": true } }'The response is { "result": "accepted" }. "ignored" means the decision changed nothing: the run had already finished, or the request
was already decided. "unknown" means there is no such run.
From your backend, call plico.approvals.decide(runId, requestId, approved, { context }).
context records who decided on approval.decided. A client cannot supply it: Plico drops any
context on a signal that comes over HTTP, so only your backend can say who approved. See
Use Plico from your backend and Threads.
Try it without a key
A scripted run exercises the whole wait with no model. Save this as waiting.script.json next to a
project with an approval-gated tool such as close_ticket:
[{ "type": "tool.call", "toolName": "close_ticket", "arguments": { "ticketId": "T-1" } }]plico run --dry --persist --script waiting.script.json .The run stops with run.waiting, prints Persisted run ID: <run-id> and exits with code 2. Start
the server in another terminal, read the requestId from the events, and decide:
plico serve .
plico events <run-id> . # find the approval.requested event and its requestId
curl -X POST http://127.0.0.1:3000/v1/runs/<run-id>/signal -H "content-type: application/json" -d '{ "name": "approval:<requestId>", "value": { "approved": true } }'The run continues, writes approval.decided, tool.result and run.completed, and plico events
shows them. Without --persist the run is not saved, so there is nothing to decide. This runs the
tool's real handler.
To decide inside the script instead, add an approval.decide step right after the tool call. The run
no longer waits: it records approval.requested and approval.decided, then runs the tool
(approved: true) or ends with run.failed (approved: false). Evals use this to cover both paths;
see Evals.
Approvals in evals
Approval policy applies in evals the same way it does in a run. A case that calls a tool needing
approval is held unless an approval.decide step follows it. See Evals.
While a run waits
A waiting run keeps the instructions and skills it started with. Edit agent.md or a skill and
it continues with the old text; only new runs use the edit. A change to any TypeScript file in
tools/ ends the waiting run with run.failed before the tool runs, because Plico will not run
different code from what the person approved. See Project layout
and Upgrading.