Skip to Content

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:

tools/close-ticket.tool.ts
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:

plico.config.ts
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
  },
});
KeyMeaning
default"none" (the default) or "required" for tools with no override.
toolsPer-tool override, "required" or "none".
timeoutMsHow 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

  1. The model calls the tool. Plico records tool.call, then approval.requested with a requestId, the tool name, its arguments and the reason.
  2. The run stops with run.waiting. Its status is waiting. No tool code has run.
  3. A decision arrives. Plico records approval.decided and, if approved is true, 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.

On this page