--- title: Tools API description: The list of built-in tools and how to define custom tools. section: dev order: 50 updated: 2026-11-05 translation_of: zh source_sha: a64600d3ef5e --- # Tools API ## How a call runs - **Tools run in a sandbox** (Deno). A tool's code can read only its own gem's directory and the resource files it downloaded (see below); it cannot write files directly, cannot go on the network directly, and cannot read environment variables. Everything else (network, storage, files, knowledge base, models) goes through `ctx`. A gem's own storage (`ctx.data`, `ctx.storage`), files (`ctx.files`), and document extraction (`ctx.extract`) are available to every tool; the rest depends on the permissions requested in the manifest and approved by the user. Each gem's code runs in its own sandbox process and is not shared with other gems; the sandbox cannot load npm packages (any import starting with `npm:` is rejected). - **Do not expect anything in memory to survive between two calls.** The sandbox is sometimes reused by the next call and sometimes not. State you want to keep should be written to the gem's own [storage](/en/dev/storage.md). - **A call runs from start to finish and does not span an app restart.** When the app quits, or the user switches organizations, calls in progress are interrupted and do not continue afterwards. Anything that needs to wait, needs user approval, or has to continue after a restart should be written as a SOP: every step's result is recorded, so it can be suspended and resumed. See [Events and Triggers](/en/dev/events.md#after-the-trigger-a-run). - A tool called by a SOP may be rerun once after an app crash. Tools with external side effects should use `ctx.idempotencyKey` to deduplicate. - **Arguments are checked against `inputSchema`**: if the caller's arguments do not match your `inputSchema` (wrong type, a missing required field, an extra field when `additionalProperties: false`), the tool does not run, and the caller gets an error saying what is wrong. An `inputSchema` that is itself wrong and cannot be compiled is rejected at packaging and install. `pattern`, `patternProperties`, and `uniqueItems` are not supported, because checking them can make the app hang; check such requirements yourself in the tool's code. Passing `null` for an optional parameter is the same as not passing it. - **The return value, serialized as JSON, is at most about 1 million characters** (1,048,576). For more, write to a file (`ctx.files`) or to [storage](/en/dev/storage.md) and return a summary. - **`ctx.http.fetch` explains why when no credentials were attached**: if the request specified a `connection` but the credentials were not injected (the host or path is not in the grant, not https, and so on), the return value has `credentialDenied: { reason, host }`, and the other side usually answers 401 next. ## ctx.files Every tool has `ctx.files`, with nothing to request: it reads and writes the folder of this run and the folder of the conversation this call belongs to (for the rules see the [Permission Model](/en/dev/permissions.md#files-are-not-a-permission)). All methods are asynchronous. ```js export default async function (input, ctx) { // input.table is "sales.csv", input.target is "inbox/": what the AI passes in are relative paths inside the conversation folder const csv = await ctx.files.read('workspace:' + input.table) const dir = 'workspace:' + input.target for (const f of await ctx.files.list(dir)) { if (f.type === 'file' && f.name.endsWith('.pdf')) { await ctx.files.move(dir + f.name, dir + 'PDF/' + f.name) } } await ctx.files.write('output/report.md', '# 本月汇总\n…') return { ok: true } } ``` | Method | Description | | --- | --- | | `list(path?)` | The contents of a folder: `[{ name, type: 'file' \| 'folder', size, modified }]`. Without `path` it is the run folder itself, and returns an empty array if nothing has been written yet. | | `stat(path)` | Information about one item; returns `null` if it does not exist. | | `read(path, { encoding? })` | The file's content. `encoding` is `'utf8'` (default) or `'base64'`. | | `write(path, content, { encoding? })` | Write a file. Parent folders are created automatically if missing, and the write is atomic. When overwriting an existing file in the conversation folder, the old version is first saved into the file history, so it can be restored when the user rolls the conversation back. | | `mkdir(path)` | Create a folder. | | `move(from, to)` | Move or rename, including between the run folder and the conversation folder. If the target already exists it throws an error and does not overwrite. | | `delete(path)` | Move to the user's trash, and keep a copy in the file history (up to 100 MB), which can be restored when the user rolls the conversation back. In environments without a trash (the headless server build) it is a permanent delete, and the copy in the history can still be rolled back. | - **Paths**: an ordinary relative path is in the run folder; `workspace:<relative path>` is in the folder of the conversation this call belongs to (`workspace:`, `workspace:inbox/invoice.pdf`). A gem never sees absolute paths; going outside with `..` or a symbolic link in the folder is rejected. - **Files and folders starting with `.` in the conversation folder do not exist for a gem**: they cannot be listed, read, or written. - **Calls without a conversation** (calls started from a page, scheduled SOPs with no working folder chosen) get `E_PERM_DENIED` when they use `workspace:`. - **At most 20 MB per read or write**, since the content passes through the sandbox's channel. - There is no interface for generating PDFs; write reports as Markdown or HTML in `output/`. - A SOP does not read or write files itself; it hands the path to a tool. `.wasm` tools have no `ctx`. ## ctx.data and ctx.storage A gem's own storage. Every tool has it, with nothing to request (see the [Permission Model](/en/dev/permissions.md#storage-is-not-a-permission)): | Kind | Interface | What it is for | | --- | --- | --- | | `data` | `ctx.data` | Prefer this: collections declared in the manifest's [`data`](/en/dev/manifest.md#data) section, plus Logs, documents, Blobs, and references. | | `kv` | `ctx.storage.kv` | String key-value pairs such as small settings and sync cursors. | | `rawSqlite` | `ctx.storage.sql` | A private database you write SQL for yourself, with the schema in the package's `db/schema.sql`. Use only when `ctx.data` cannot do it. | ```js export default async function (input, ctx) { const notes = ctx.data.collection('notes') const { id } = await notes.insert({ title: input.title, status: 'open' }) return { id } } ``` - For the interface, query and index rules, transactions, migrations, limits, and error codes, see [Data and Storage](/en/dev/storage.md). - A tool declared `sideEffect: "read"` must not write to any storage: plan mode runs it before the user approves, and the platform does not check. See the [Permission Model](/en/dev/permissions.md#storage-is-not-a-permission). - A SOP has no `ctx.data`; it reads and writes through the gem's own tools. See [How SOPs access data](/en/dev/storage.md#how-sops-access-data). ## ctx.email A tool that has `"email": { "read": true }` in both the manifest's top-level `permissions` and the tool's own `permissions` gets `ctx.email`, for reading the user's email (in a Gem Studio draft it is the top-level `"email": true`). **It can read only the mailboxes the user allowed on the gem's page**: at install there are none, and the user can choose a few accounts, or "All accounts" (which also covers accounts added later). Passing an account that is not allowed throws an error. Read-only; it cannot send mail. ```js export default async function (input, ctx) { // Where we got to last time is kept in the gem's own storage const since = Number((await ctx.storage.kv.get('cursor')) ?? 0) const { messages, cursor } = await ctx.email.listSince({ since, limit: 20 }) for (const m of messages) { const full = await ctx.email.getMessage({ accountId: m.accountId, id: m.id }) // do something with full.body... } await ctx.storage.kv.set('cursor', String(cursor)) return { processed: messages.length } } ``` The example uses `ctx.storage.kv` to record where it got to; that is the gem's own storage, with nothing to request. | Method | Description | | --- | --- | | `listSince({ accountId?, since?, limit? })` | Messages newer than `since` (the message date, a millisecond timestamp, default 0), oldest first, at most `limit` messages (default 20, max 200). Without `accountId` it covers all allowed accounts. Returns `{ messages, cursor }`: each message is `{ accountId, id, subject, fromAddr, toAddrs, date }`, **without the body**; `cursor` is the latest date seen, to pass back as `since` next time. | | `getMessage({ accountId, id })` | Read the full text of one message (async): the fields above, plus `body` (the body as plain text) and `attachments` (each attachment's `filename`, `mimeType`, `sizeBytes`, without content). `id` comes from `listSince` or the [`email.onNewMessage` event](/en/dev/events.md#emailonnewmessage). | - The event a new-mail-triggered SOP receives has only the envelope, not the body: the SOP hands `accountId` and `id` to such a tool to read the body. See [Events and Triggers](/en/dev/events.md#emailonnewmessage). ## ctx.resources A gem whose manifest declares [`resources`](/en/dev/manifest.md#resources) gets `ctx.resources` in each of its tools. No permission needs to be requested. ```js export default async function (input, ctx) { const modelPath = await ctx.resources.path('ocr-model') const model = await Deno.readFile(modelPath) // do something with model... return { ok: true } } ``` `ctx.resources.path(id)` returns the absolute path of this file on the local machine; `id` is the one written in the manifest. - **If the file has not been downloaded yet, this call downloads it first and only then returns**; a large file may take a long time. For files that should be ready right after install, mark `"required": true` in the manifest so that the download starts early, after install. When several calls want the same file at once, it is downloaded only once. - **Once you have the path, read it yourself.** Use Deno's file interfaces (`Deno.readFile`, `Deno.readTextFile`, `Deno.open`). A tool can only read these files, not change them; it also cannot read another gem's resources. - **Ask for the path every time you use it; do not store it yourself.** If the file's content changes (a new version swapped the `sha256`), the path changes too. When the file is already on the machine, this call returns immediately. - **It throws an error on failure**: the `id` is not declared in the manifest; or neither the `url` nor any mirror could download a file that passes verification (the error message says why each address failed). The gem is not uninstalled or disabled because of this, and the next call tries the download again. - A SOP cannot read files itself and uses them through a tool. While a tool called by a SOP is waiting for a download, that time counts toward the run's active time (default limit 10 minutes). - `.wasm` tools have no `ctx` and cannot use resource files.
Notochord
简体中文English