Docs
Tools
What a tool is, the ones chloe ships and what each is bound to, and how to write your own.
A tool is something a model may call: a description, an input schema and one
function. An agent hands its tools over in tools, keyed by the name the model
sees, and a job's agent step hands over its own.
What a tool can reach is fixed in your file, not chosen by the model. Each tool chloe ships is a function you call with a binding (the folder, the mail search, the address to send to), and the model fills in only what its input schema asks for. A search a model can write is a filter, and it widens the moment a turn goes wrong. A binding it cannot write is a boundary.
The tools that ship
Import a set as a whole and bind each tool you want:
import * as fs from "@chloejs/core/tools/fs", then
tools: { fsReadFile: fs.readFile({ root: "/srv/notes", what: "the shared notes" }) }.
| Set | Tools | Bound to | The model chooses |
|---|---|---|---|
fs |
listFiles, readFile, searchFiles, writeFile, editFile |
root, one folder, and what, its name in words. Writes take commit: true to commit each one. |
A path inside that folder. |
web |
readPage() |
nothing | Which public page to read. |
gmail |
readEmail, replyEmail, sendEmail |
A Gmail search, or the From and To of what it sends. See Connections. | How far back and how many; which listed message to answer. |
calendar |
listEvents, addEvent |
Which calendars. | The dates, and what an event says. |
drive |
searchFiles, readFile |
A Drive search, like one folder. | Words to look for; which listed file to read. |
resend |
sendEmail |
The From and To. | The subject and the body. |
email |
startConversation |
Which email channel. | Who, from the channel's allowFrom, and what to say. |
The memory, self and script tools are not imported: features on the agent
switches them on. See An agent.
Sending an email
gmail.sendEmail and resend.sendEmail take the same options, and the model
writes only the subject and the body.
| Option | Default | What it controls |
|---|---|---|
from |
required | The From line, like "Shop <orders@myshop.com>". Through Gmail, it must be the signed-in account or an alias Google verified for it. Through Resend, an address on a domain Resend sends for. |
to |
required | Who it goes to. The model cannot change it. |
when |
required | When to use it, in your words. Shown to the model. |
replyTo |
from |
Where a reply goes. |
tag |
none | Put in front of every subject, [tag], so an inbox can filter it. |
markdown |
off | Sends the body as HTML with a plain text copy. Off, plain text as written. |
keep |
none | A folder in its memory, like "outbox", that gets a copy of each email sent. |
To mail somebody and read their reply, use the email channel instead.
Your own tool
Made with the AI SDK's tool(), imported from "ai". The work is a plain
function in the agent's services/, so a job can call it too, and the tool is
only how a model reaches it:
// Where one of the customer's own orders is. The shop's site signs its
// customers in and asks for each chat's pass with their customer id as the
// visitor, so the runtime hands this tool who it is talking to, and the tool
// looks only at that customer's orders: a stranger who guesses an order number
// learns nothing. The order system is read through services/, the same as
// every job reads it.
import { tool } from "ai";
import { z } from "zod";
import { ordersBy } from "../services/ordersService.ts";
export const orderStatus = tool({
title: "Looking up the order",
description: "Where one of this customer's orders is: when it was placed, whether it has shipped, and the date they were promised. Takes the order number, like A-4417.",
inputSchema: z.object({ id: z.string().describe("The order number, like A-4417.") }),
execute: async ({ id }, { context }) => {
// "web:<their customer id>", set by the runtime from the pass, never by the model.
const user = (context as { user?: string } | undefined)?.user ?? "";
if (!user.startsWith("web:")) return "Only a customer signed in on the shop's site can look up an order here.";
const found = (await ordersBy(user.slice(4))).find((one) => one.id.toLowerCase() === id.trim().toLowerCase());
if (!found) return `None of your orders is ${id}.`;
return { id: found.id, placed: found.placed, paid: found.paid, shipped: found.shipped ?? "not yet", promised: found.promised };
},
});What chloe reads on a tool:
| Field | What it does |
|---|---|
title |
Shown while the tool runs, on a channel that shows progress: "Looking up the order". |
needsApproval |
The AI SDK's. In a job's agent step the run waits for a person's yes; in a turn the call is refused. |
needs |
The connection it works through, like Google. The dashboard says what that connection is missing, and chloe starts the sign-in when the tool finds nobody signed in. |
overview |
A function giving a few lines about what the tool reaches now (the folders, the tables), put at the top of every turn that has the tool. |
needs and overview are not AI SDK fields, so add them after:
Object.assign(tool({ ... }), { needs: crm }).
A tool's second argument holds context. agentOf(context) is the agent it
runs for, and in a turn context.user is who sent the message, as
<channel>:<id> (telegram:12345, web:<visitor>). The runtime sets both;
the model never does.
What happens when one fails
A missing tool, bad arguments, or a tool that throws goes back to the model as text, and the turn carries on. So a run that worked may still hold failures: read its steps, not only its reply.
A job never imports a tool. It calls the plain function from a step, and a job
that imports a tool fails npm run test.