Custom Tools
Give an agent tools that run in your backend, with access to your APIs and secrets.
A custom tool is a function the model can call. It runs in the agent Actor on your worker, next to your server code, so it can use your APIs and secrets.
import { Type } from "@earendil-works/pi-ai";
import { defineTool } from "@earendil-works/pi-coding-agent";
export const getOrder = defineTool({
name: "get_order",
label: "Get order",
description: "Look up an order's status by its id.",
parameters: Type.Object({ orderId: Type.String() }),
async execute(_toolCallId, { orderId }, signal) {
const order = await fetchOrder(orderId, signal);
const text = `Status: ${order.status}. Total: $${order.total}.`;
return { content: [{ type: "text", text }], details: order };
},
});
async function fetchOrder(orderId: string, signal: AbortSignal | undefined) {
const response = await fetch(`https://api.example.com/orders/${encodeURIComponent(orderId)}`, {
headers: { authorization: `Bearer ${process.env.ORDERS_API_TOKEN}` },
signal,
});
if (response.status === 404) throw new Error(`No order with id ${orderId}.`);
if (!response.ok) throw new Error(`The orders API returned ${response.status}.`);
return (await response.json()) as { status: string; total: number };
}
import { pi } from "@rivet-dev/pi";
import { e2bProvider } from "@rivet-dev/sandbox-adapter/e2b";
import { setup } from "rivetkit";
import { getOrder } from "./get-order";
const agent = pi({
model: "anthropic/claude-opus-5-5",
sandbox: e2bProvider(),
customTools: [getOrder],
});
export const registry = setup({ use: { agent } });
registry.start();
The model reads the tool’s description and calls it with arguments that match parameters. It reads content as the result, while details goes to clients for your UI. A thrown error, like the 404 in fetchOrder, becomes an error result: the model reads the message and can try something else. The signal argument aborts when the run is cancelled, so a Stop from the client also cancels the request.
Secrets
The orders API token stays in your backend. The tool reads it from the worker’s environment as ORDERS_API_TOKEN, and nothing else sees it:
| Sees the token | |
|---|---|
| Your tool code | Yes, from process.env on the worker. |
| The model | No. It sees the tool’s name, description, parameters, and results. |
| The session and connected clients | No. They store and receive tool arguments and results. |
| The sandbox | No. Commands run with the sandbox’s own environment. |
Tool results and error messages are stored in the session and sent to every connected client, so keep secrets out of both. For model keys, see LLM API Keys.
Custom tools work without a sandbox too. This agent has one, so the model can also use the built-in tools.
Next: Session Lifecycle, what happens while the agent answers a prompt.