---
title: Tools API
description: Define custom AI SDK tools with durable idempotency, and the built-in sandboxed file and shell tools.
icon: "wrench"
---

A tool is an [AI SDK](https://ai-sdk.dev) tool an agent may call. `defineTool`
wraps one with Smithers runtime context: a deterministic idempotency key,
side-effect metadata, and the side-effect snapshot hook. The five built-in
tools (`read`, `write`, `edit`, `grep`, `bash`) are themselves `defineTool`
results and run inside the task's sandbox.

```ts
import {
  defineTool,
  getDefinedToolMetadata,
  bash,
  read,
  write,
  edit,
  grep,
  tools,
} from "smithers-orchestrator";
```

## defineTool

Wrap a Zod-validated `execute` function into a durable AI SDK tool. Pass the
result to an agent's `tools` or a `<Task>`'s `tools` prop.

```ts
function defineTool<S extends ZodType>(options: DefineToolOptions<S>): Tool;
```

<ParamField path="name" type="string" required>
  Tool name surfaced to the agent and stamped onto `ctx.toolName`. Also the
  default `description`.
</ParamField>

<ParamField path="description" type="string">
  Human-readable description the agent sees. Defaults to `name`.
</ParamField>

<ParamField path="schema" type="ZodType" required>
  Zod schema for the tool's input. Validated by the AI SDK before `execute`
  runs; `execute` receives the parsed args.
</ParamField>

<ParamField path="sideEffect" type="boolean" default="false">
  Marks the tool as mutating external state. Opts the tool into Smithers
  side-effect tracking and runs the durability snapshot hook after `execute`.
</ParamField>

<ParamField path="idempotent" type="boolean" default="!sideEffect">
  Whether re-running with the same input is safe. Defaults to `true` when
  `sideEffect` is `false`. With `sideEffect: true, idempotent: false`, retries
  inject a warning that the tool was already called so the agent can verify
  external state first.
</ParamField>

<ParamField path="execute" type="(args, ctx) => Promise<unknown>" required>
  Runs the tool. `args` is the parsed input; `ctx` is the tool context. If
  `sideEffect: true, idempotent: false` and `execute` omits the second `ctx`
  parameter, Smithers logs a startup warning, since you almost certainly need
  `ctx.idempotencyKey` to handle retries safely.

  <Expandable title="ctx: ToolContext">
    <ParamField path="ctx.idempotencyKey" type="string">
      Deterministic for a given task + iteration; stable across retries and
      resumes. Pass it to external APIs that support idempotency (Stripe, AWS)
      to deduplicate.
    </ParamField>
    <ParamField path="ctx.toolName" type="string">
      The tool's `name`.
    </ParamField>
    <ParamField path="ctx.sideEffect" type="boolean">
      Resolved `sideEffect` flag.
    </ParamField>
    <ParamField path="ctx.idempotent" type="boolean">
      Resolved `idempotent` flag.
    </ParamField>
    <ParamField path="ctx.runId" type="string">
      Current run id (RUN_ID).
    </ParamField>
    <ParamField path="ctx.nodeId" type="string">
      Task node that invoked the tool (NODE_ID).
    </ParamField>
    <ParamField path="ctx.iteration" type="number">
      Loop iteration index.
    </ParamField>
    <ParamField path="ctx.attempt" type="number">
      Retry attempt number.
    </ParamField>
    <ParamField path="ctx.seq" type="number">
      Task-local tool-call sequence counter.
    </ParamField>
    <ParamField path="ctx.rootDir" type="string" default="process.cwd()">
      Sandbox root. Paths resolve against it; escapes are rejected.
    </ParamField>
    <ParamField path="ctx.allowNetwork" type="boolean" default="false">
      Whether network commands are permitted (used by `bash`).
    </ParamField>
    <ParamField path="ctx.maxOutputBytes" type="number" default="200000">
      Output truncation / size limit (200KB).
    </ParamField>
    <ParamField path="ctx.timeoutMs" type="number" default="60000">
      Process timeout for `bash` and `grep` (60s).
    </ParamField>
    <ParamField path="ctx.durabilitySnapshot" type="(name, toolUseId) => Promise<unknown>">
      Side-effect snapshot hook. No-op unless the engine supplies a real
      handle; snapshot failures never fail the tool.
    </ParamField>
  </Expandable>
</ParamField>

<ResponseField name="Tool" type="object">
  An AI SDK tool with attached Smithers metadata. Read the metadata with
  [`getDefinedToolMetadata`](#getdefinedtoolmetadata).
</ResponseField>

```ts
import { defineTool } from "smithers-orchestrator";
import { z } from "zod";

const placeOrder = defineTool({
  name: "wholefoods.place_order",
  description: "Place a grocery order",
  schema: z.object({ sku: z.string() }),
  sideEffect: true,
  idempotent: false,
  async execute(args, ctx) {
    return wholeFoods.placeOrder({
      sku: args.sku,
      idempotencyKey: ctx.idempotencyKey,
    });
  },
});
```

The `sideEffect` / `idempotent` pair drives retry behavior:

| `sideEffect` | `idempotent` | Behavior |
|---|---|---|
| `false` | `true` | Pure read. Replayed freely. No warnings. |
| `true` | `true` | Mutates, but replay-safe (e.g. an upsert or PUT). No warnings. |
| `true` | `false` | Mutates and not replay-safe (e.g. send email, charge payment). Retry warns the agent and supplies a stable `ctx.idempotencyKey`. |

The rule: if you cannot undo it with `git reset`, mark it `sideEffect: true`.

**Source** [`defineTool.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/defineTool.js) · [`context.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/context.js) · **Tests** [`tools-unit.test.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/tests/tools-unit.test.js), [`define-tool-durability.test.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/tests/define-tool-durability.test.js) · **See also** [Built-in Tools](/integrations/tools), [SDK agents](/integrations/sdk-agents)

## getDefinedToolMetadata

Read the Smithers metadata attached by `defineTool`. Returns `null` for plain
AI SDK tools or non-objects.

```ts
function getDefinedToolMetadata(value: unknown):
  | { name: string; sideEffect: boolean; idempotent: boolean }
  | null;
```

<ParamField path="value" type="unknown" required>
  A tool (or any value). Inspected for the `Symbol.for("smithers.tool.metadata")`
  property.
</ParamField>

<ResponseField name="metadata" type="object | null">
  <Expandable title="metadata">
    <ResponseField name="name" type="string">
      The tool's name.
    </ResponseField>
    <ResponseField name="sideEffect" type="boolean">
      Resolved side-effect flag.
    </ResponseField>
    <ResponseField name="idempotent" type="boolean">
      Resolved idempotency flag.
    </ResponseField>
  </Expandable>
</ResponseField>

```ts
import { getDefinedToolMetadata, write } from "smithers-orchestrator";

getDefinedToolMetadata(write);
// { name: "write", sideEffect: true, idempotent: false }
```

**Source** [`defineTool.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/defineTool.js) · **Tests** [`tools-unit.test.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/tests/tools-unit.test.js) · **See also** [`defineTool`](#definetool)

## Built-in tools

Five `defineTool` results, all sandboxed to `ctx.rootDir` (the workflow
directory by default). Paths resolve against the root; symlink escapes are
rejected. Output is truncated to `ctx.maxOutputBytes` (200KB). Import them
individually or as the `tools` bundle:

```ts
import { tools } from "smithers-orchestrator";
// tools === { read, write, edit, bash, grep }
const { read, write, edit, bash, grep } = tools;
```

| Tool | `sideEffect` | `idempotent` |
|---|---|---|
| `read` | `false` | `true` |
| `grep` | `false` | `true` |
| `write` | `true` | `false` |
| `edit` | `true` | `false` |
| `bash` | `true` | `false` |

### read

Read a UTF-8 file from the sandbox.

<ParamField path="path" type="string" required>
  File path, relative to `rootDir` or absolute within it.
</ParamField>

Returns the file contents. Throws if the file exceeds `maxOutputBytes`.

### write

Write content to a file, creating parent directories as needed.

<ParamField path="path" type="string" required>
  Destination path, relative to `rootDir` or absolute within it.
</ParamField>
<ParamField path="content" type="string" required>
  File contents.
</ParamField>

Returns `"ok"`. Throws `TOOL_CONTENT_TOO_LARGE` if `content` exceeds
`maxOutputBytes`; only the content's byte size and SHA-256 hash are logged,
never the content itself.

### edit

Apply a unified-diff patch to an existing file.

<ParamField path="path" type="string" required>
  File to patch. Must already exist.
</ParamField>
<ParamField path="patch" type="string" required>
  A unified diff. Applied against the current contents.
</ParamField>

Returns `"ok"`. Throws `TOOL_PATCH_TOO_LARGE` (patch over `maxOutputBytes`) or
`TOOL_PATCH_FAILED` (the patch context does not match the file).

### grep

Search for a pattern with `ripgrep` (`rg -n`). Requires `rg` on `PATH`.

<ParamField path="pattern" type="string" required>
  Regex to search for.
</ParamField>
<ParamField path="path" type="string" default=".">
  Directory or file to search, relative to `rootDir`.
</ParamField>

Returns matching lines as `file:line:text`. No matches returns an empty
string; an `rg` error (exit code 2) throws `TOOL_GREP_FAILED`.

### bash

Run an executable with arguments. There is no shell parsing: pass arguments via
`args`, and for pipes or redirects invoke a shell explicitly, e.g.
`{ cmd: "sh", args: ["-lc", "..."] }`.

<ParamField path="cmd" type="string" required>
  Executable name or path. Up to 8,192 characters.
</ParamField>
<ParamField path="args" type="string[]">
  Arguments. Up to 128 entries, each up to 8,192 characters.
</ParamField>
<ParamField path="opts" type="{ cwd?: string }">
  `cwd` sets the working directory (sandboxed under `rootDir`). Defaults to
  `rootDir`.
</ParamField>

Returns combined stdout and stderr, truncated to `maxOutputBytes`. A non-zero
exit code throws `TOOL_COMMAND_FAILED`. The process is killed with `SIGKILL`
after `ctx.timeoutMs` (default 60s; the hard cap is one hour).

<Note>
Network is blocked unless `allowNetwork` is enabled (via `RunOptions`,
`--allow-network`, or server config). When blocked, Smithers rejects commands
whose executable basename is `curl`, `wget`, `npm`, `bun`, or `pip`, any token
beginning with `http://` or `https://`, and `git` together with a `push`,
`pull`, `fetch`, `clone`, or `remote` token (throwing `TOOL_NETWORK_DISABLED`
or `TOOL_GIT_REMOTE_DISABLED`). Local git commands such as `git status` are
allowed.
</Note>

<Warning>
**This denylist is not isolation on Linux.** True OS-level network isolation is
enforced **only on macOS**, where a blocked `bash` runs under `sandbox-exec`
with a network-deny profile (when `sandbox-exec` is available). On Linux and
every other platform, including the environments Smithers runs in for CI,
cloud, and production, `allowNetwork:false` cannot enforce a kernel sandbox and
falls back to the token/basename denylist above. That denylist is best-effort
defense-in-depth, **not** a security boundary: it is trivially bypassable by a
shell, an interpreter (`python -c` assembling a URL from parts, bash
`/dev/tcp`), or a renamed binary. When isolation is unenforced, Smithers logs a
`TOOL_NETWORK_ISOLATION_UNENFORCED` observability warning. **Do not rely on
`allowNetwork:false` to sandbox untrusted code.** For untrusted workloads, run
them under a real [`<Sandbox>`](/components/sandbox) provider with egress
controls instead.
</Warning>

```tsx
import { ToolLoopAgent as Agent } from "ai";
import { openai } from "@ai-sdk/openai";
import { createSmithers, tools, Task } from "smithers-orchestrator";
import { z } from "zod";

const codeAgent = new Agent({
  model: openai("gpt-5.6-luna"),
  tools, // { read, write, edit, bash, grep }
});

const { Workflow, smithers, outputs } = createSmithers({
  result: z.object({ summary: z.string() }),
});

export default smithers((ctx) => (
  <Workflow name="refactor">
    <Task id="refactor" output={outputs.result} agent={codeAgent}>
      {`Refactor ${ctx.input.file} for readability and run the tests.`}
    </Task>
  </Workflow>
));
```

**Source** [`tools/index.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/index.js) · [`bash.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/bash.js), [`read.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/read.js), [`write.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/write.js), [`edit.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/edit.js), [`grep.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/src/tools/grep.js) · **Tests** [`tools-unit.test.js`](https://github.com/smithersai/smithers/blob/main/packages/smithers/tests/tools-unit.test.js) · **See also** [Built-in Tools](/integrations/tools), [Common External Tools](/integrations/common-tools), [SDK agents](/integrations/sdk-agents)
