Tools
A tool is a function the model may request and the runner executes. That split is the
point. The model does not run your database code. Shiro does — which is why the call can be
traced, approved, timed out, and attributed to a runId.
import { tool } from "@shiro-sdk/core";
import { z } from "zod";
const lookupInvoice = tool({
name: "lookupInvoice",
description: "Fetch invoice status and amount by id.",
parameters: z.object({
invoiceId: z.string(),
}),
execute: async ({ invoiceId }) => {
return invoices.get(invoiceId);
},
});Why tools belong in the runtime#
Tool calls fail, hang, and mutate state. If they live as ad-hoc functions in a route handler:
- Studio cannot show arguments or latency
- Approvals cannot pause before the side effect
- Retries become guesswork
The runner answers: this agent, on this run, called this tool with these args.
Anatomy#
| Field | Required | Purpose |
|---|---|---|
name | yes | Model-visible identifier |
parameters | yes | Schema with .parse() (often Zod) |
execute | yes | Implementation |
description | no | Helps the model choose the tool |
requiresApproval | no | Pause before execute — see HITL |
Attach tools on the agent:
const agent = new Agent({
name: "support",
instructions: "Use lookupInvoice for billing questions.",
provider: "openai",
tools: [lookupInvoice],
});When to use a tool#
Use a tool when the agent needs code: lookups, writes, calculations, internal APIs, workflow triggers.
Do not use a tool for pure language instructions. Prefer a clearer prompt or a structured output field.
Approval-gated tools#
const issueRefund = tool({
name: "issueRefund",
description: "Issue a refund for an order.",
requiresApproval: true,
approvalDescription: "Refunds money to the customer.",
parameters: z.object({
orderId: z.string(),
amount: z.number().positive(),
}),
execute: async ({ orderId, amount }) => refunds.issue({ orderId, amount }),
});Reviewers see the planned arguments. Rejection is a normal outcome. See Human-in-the-loop and the Approval center in Studio.
Best practices#
- Keep names stable and specific (
lookupInvoice, nothelper) - Return compact JSON — huge payloads waste tokens and clutter traces
- Make mutating tools idempotent (keys, upserts) before enabling retries
- Prefer one responsibility per tool
Common mistakes#
| Mistake | Fix |
|---|---|
Missing parameters | Always pass a schema with .parse() |
Calling execute from app code | Let the runner invoke the tool |
| Returning entire DB rows | Project fields the model needs |
| Retrying payments blindly | Idempotency + approval |