Skip to main content

Tools and approval

A tool is a function an agent can call. You describe it (name, description, input schema) and register a handler. Mark it requiresApproval: true and the agent can't call it until a human says yes.

import { createGraph, InMemoryToolRegistry, model, type ToolId } from "@ailu-ai/graph-sdk";

const refunds: string[] = [];

// 1. Describe the tool. `jsonSchema` and `description` are what the model sees.
const tools = new InMemoryToolRegistry();
tools.register(
{
id: "refund" as ToolId,
name: "refund",
description: "Refund an order. Use only when the customer asks for a refund.",
jsonSchema: {
type: "object",
properties: { orderId: { type: "string" } },
required: ["orderId"]
},
inputSchema: { parse: (value: unknown) => value as { orderId?: string } },
outputSchema: { parse: (value: unknown) => value as { ok: boolean } },
permissions: ["payments:write"],
requiresApproval: true // a human must approve every call
},
async (input) => {
refunds.push(input.orderId ?? "unknown");
return { ok: true };
}
);

// 2. Give the tool to an agent that stops when it wants to call a gated tool.
const app = createGraph({ name: "support" })
.channel("request", { type: "string", default: "" })
.agentNode("assistant", {
model: model.anthropic("claude-sonnet-4-6"),
prompt: { system: "Help the customer. Use the refund tool when needed." },
tools,
suspendForApproval: true
})
.compile();

// 3. The run suspends before the refund runs.
const paused = await app.run({ request: "Please refund order ORD-8830." });
console.log(paused.status); // "suspended"
console.log(refunds.length); // 0: nothing ran yet

// 4. A named human approves; the agent resumes and the refund runs.
const done = await app.approveAndResume(paused.runId, {
approvedTools: ["refund"],
resolvedBy: "alice@example.com"
});
console.log(done.status); // "completed"
console.log(refunds.length); // 1

Describe a tool​

InMemoryToolRegistry.register(definition, handler) takes:

FieldPurpose
id, nameThe tool's identity. The model calls it by name.
descriptionWhen to use it. The model reads this.
jsonSchemaThe JSON Schema of the input. The model reads this to build the call.
inputSchema, outputSchemaObjects with a parse(value) method that validate the input and output in your code. A Zod schema fits.
permissionsLabels for your own audit, such as "payments:write".
requiresApprovaltrue makes every call wait for a human.

The handler receives the parsed input and returns a JSON value, which the agent sees as the tool's result.

Tools without requiresApproval run as soon as the agent calls them.

Approve a call​

With suspendForApproval: true, an agent that wants a gated tool stops the run:

  1. run() returns status: "suspended". The agent's result lists what it wants in approvalRequests, for example { subject: "tool:refund", reason: "..." }. app.explain(runId).summary says the same in one sentence.
  2. Show the request to a person in your app.
  3. When they approve, call approveAndResume(runId, { approvedTools: ["refund"], resolvedBy: "<their user id>" }). The agent runs again and can now call refund.

If they refuse, don't resume. The run stays suspended; you can discard it or keep it for the record.

Who approves

resolvedBy is required: take it from your authenticated session. It is recorded with the approval. The SDK refuses an empty value (AILU_APPROVER_REQUIRED), and the engine refuses a grant where the approver is the agent that asked.

Keep the handler honest​

The model chooses the tool's input. Treat it like user input:

  • validate it in inputSchema.parse;
  • check permissions in the handler (does this user own this order?);
  • keep handlers idempotent where you can: a resumed run may call a tool again.

Record approvals as evidence​

To sign each decision and let an auditor check it later, see Governance. To resume an approval in another process (after a deploy, or from a queue worker), see Long-running runs.

Next​