Tools

Typed side effects the model can request and the runner executes.

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#

FieldRequiredPurpose
nameyesModel-visible identifier
parametersyesSchema with .parse() (often Zod)
executeyesImplementation
descriptionnoHelps the model choose the tool
requiresApprovalnoPause 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, not helper)
  • 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#

MistakeFix
Missing parametersAlways pass a schema with .parse()
Calling execute from app codeLet the runner invoke the tool
Returning entire DB rowsProject fields the model needs
Retrying payments blindlyIdempotency + approval