Skip to Content

Tools

Write typed tools in TypeScript with defineTool.

Tools are loaded from their .tool.ts files. They can import sibling TypeScript modules and npm packages installed in the project. create-plico includes a package.json with type: "module" for ESM tool imports.

Wrap a tool in defineTool from @plico-ai/sdk to get editor types. defineTool infers the handler input from inputSchema and returns its argument unchanged.

tools/add.tool.ts
import { defineTool } from "@plico-ai/sdk";
import { z } from "zod";

export default defineTool({
  name: "add",
  description: "Add two numbers.",
  inputSchema: z.object({ a: z.number(), b: z.number().default(0) }),
  capabilities: [],
  approval: { required: false },
  handler: ({ a, b }) => a + b,
});

Fields

FieldMeaning
nameThe name the model calls. Unique across the project; list_files, read_file, write_file, edit_file and load_skill are reserved.
descriptionTells the model when to use the tool.
inputSchemaZod or JSON Schema for the arguments.
capabilitiesRequired list of labels for what the tool touches, such as ["ticket:write"]. Plico records it in tools.discovered and does not enforce it.
approvalRequired. { required: true, reason } holds each call for a person. See Approvals.
handler(input, ctx). input is the parsed arguments; ctx has artifacts, context, signal and an idempotency key.

A tool must export its definition as the file default.

Input schemas

Declare inputSchema with Zod 3, Zod 4, or JSON Schema. Plico validates tool calls with that schema and sends JSON Schema to the model. Parsed Zod values, including defaults and transforms, reach the handler.

Add zod to the project dependencies before importing it from a tool.

Validation rejects duplicate names declared across .tool.ts files, and readiness reports the duplicate under tool discovery.

When a tool call fails

In a run with a real model, a bad tool call does not end the run at once. An unknown tool name, arguments that fail inputSchema, or a handler that throws is returned to the model as the tool result, { "error": "<message>" }, and the event log records it as a tool.result. The model can then correct the call. This matters most for small local models, which often send numbers as strings.

A run allows 2 such failures. The third ends the run with the original error, in the same run.error event as before. Scripted runs (plico run --dry --script) and approval denials still fail immediately.

Writing artifacts

A tool can read, write, and list artifacts with ctx.artifacts: read(path), write(path, content, { mimeType }), and list(). See Artifacts.

Host context

When a host app starts a run with a context (see Threads), the handler receives it as ctx.context, such as { userId: "u-1" }. It is undefined when the host passed none, and it is the same value after a restart. Plico records it in the run's event log, so keep secrets out of it and look them up inside the tool.

On this page