---
title: Tools
description: Define typed actions the agent can call, and gate sensitive ones on human approval.
url: "https://eve.dev/docs/tools"
docs_index: /llms.txt
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

A tool is a typed action the agent can call, such as hitting an API, running a query, or writing a file. Each tool is a file under `agent/tools/`, and the filename is the tool name the model sees. Define a tool when you implement the action in code you control.

## Define a tool

```text
agent/
├── agent.ts
└── tools/
    └── get_weather.ts   # the model calls this tool `get_weather`
```

```ts title="agent/tools/get_weather.ts"
import { defineTool } from "eve/tools";
import { z } from "zod";

export default defineTool({
  // required: what the tool does, written for the model
  description: "Get the current weather for a city.",
  // required: use z.object({}) for a tool with no input
  inputSchema: z.object({ city: z.string().min(1) }),
  // required: may be sync, async, or an async generator
  async execute({ city }, ctx) {
    return { city, condition: "Sunny", temperatureF: 72 };
  },
});
```

- Tools run in your app runtime, not in the [sandbox](/docs/sandbox). They can read `process.env` and import shared code from `lib/`.
- `inputSchema` is a Zod schema, any Standard Schema, or a plain JSON Schema object. Zod and Standard Schema infer the `input` type, and a plain JSON Schema types it as `Record<string, unknown>`.
- eve validates the input before `execute` runs. Zod and Standard Schema use their own library, so any installed Zod version works.
- eve sends a plain JSON Schema to the model without its `$schema` keyword. It validates with JSON Schema 2020-12 rules, which cover draft-07 keywords.
- For a JSON Schema, eve fills omitted properties from their `default` values. `format` is an annotation only.
- An unknown `type` name or an unresolvable `$ref` stops eve from checking a JSON Schema. The model still receives the schema, and `execute` gets the input unchecked.
- Optional input fields stay optional, because eve turns off provider strict mode for function tools.
- Tool outputs must be JSON-serializable. Convert `Date`, `Map`, `Set`, `NaN`, and cyclic values before you return them.
- Add an `outputSchema` for structured output. With Zod or Standard Schema, it also types the `execute` return.
- Schemas can come from helper modules and be composed with Zod.

### The `ctx` parameter

`execute` gets a `ctx` with these accessors:

- `ctx.session`: session metadata, turn, auth, and parent agent.
- `ctx.callId`: the call's id, also on its [stream events](/docs/concepts/sessions-runs-and-streaming) and approval context.
- `ctx.toolName`: the name the model called, with any namespace prefix.
- `ctx.messages`: the model input for the step that requested this call, as a [dynamic tool's](/docs/guides/dynamic-capabilities) `ctx.messages()` sees it.
- `ctx.abortSignal`: aborts when the turn is cancelled. Pass it to work that supports cancellation. Sandbox sessions from `ctx.getSandbox()` already use it.
- `ctx.getSandbox()`: the live [sandbox](/docs/sandbox) handle.
- `ctx.channel`: the channel serving the session, as [hooks](/docs/guides/hooks) and [dynamic resolvers](/docs/guides/dynamic-capabilities) see it: `kind`, `continuationToken`, and `metadata`. `metadata` is what the channel's `metadata(state)` hook returns, such as a Slack thread's `channelId` and `threadTs`.
- `ctx.to(channel, target)`: starts or continues a session on one of the agent's channels. See [Start channel sessions](#start-channel-sessions-ctxto).

## Options

| Field                  | Default            | What it does                                                                  |
| ---------------------- | ------------------ | ----------------------------------------------------------------------------- |
| `description`          | required           | Tells the model what the tool does.                                           |
| `inputSchema`          | required           | Validates and types the model's input.                                        |
| `execute`              | required           | Runs the tool.                                                                |
| `outputSchema`         | none               | Describes and types the return value.                                         |
| `approval`             | `never()`          | [Asks a person to approve calls](#gate-a-tool-on-human-approval).             |
| `toModelOutput`        | full return        | [Changes what the model sees](#shape-what-the-model-sees-with-tomodeloutput). |
| `label`                | readable tool name | [Sets activity text](#label-tool-activity).                                   |
| `endsTurn`             | `false`            | [Ends the turn after success](#end-the-turn-after-a-tool-call).               |
| `deferred`             | `false`            | [Leaves the tool out of the tool list](#keep-a-tool-out-of-the-tool-list).    |
| `availableInSubagents` | `true`             | [Offers the tool in delegated sessions](#hide-a-tool-from-delegated-agents).  |

## When a tool throws

If `execute` throws, the call ends with `call.settled` `outcome: "failed"`, and the model gets the error. It can respond, try something else, or call the tool again. eve never retries a tool itself, whatever the exception type, HTTP status, or `retryable` property.

Retry inside the tool when the operation is safe to repeat. Otherwise, return or throw an error that tells the model what to do. In-tool retries don't protect a write, because the model can make a fresh call with a new tool-call ID. Authored workflow steps have their own retries.

Protect non-idempotent operations, such as charges or emails, with the strongest mechanism the service supports:

1. Pass a stable idempotency key to the upstream API.
2. Otherwise, record a unique application operation before writing, and check it on every attempt.
3. Use human approval when a person must authorize each execution. Approval doesn't replace deduplication after an unclear network result.

After a crash, completed steps replay their saved result, and an interrupted step runs again. A call that the step already started doesn't run again. The model reads that the call may have taken effect and decides whether to repeat it. See [Crash recovery](/docs/concepts/execution-model-and-durability#resuming-after-a-crash).

For one-way provider notifications, see [Durable cross-channel notifications](/docs/patterns/durable-cross-channel-notifications).

## Gate a tool on human approval

A tool can require a person to approve each call before it runs. Set `approval` with the helpers from `eve/tools/approval`:

```ts title="agent/tools/refund_charge.ts"
import { defineTool } from "eve/tools";
import { always } from "eve/tools/approval";
import { z } from "zod";

export default defineTool({
  description: "Refund a charge.",
  inputSchema: z.object({ chargeId: z.string(), amount: z.number() }),
  approval: always(), // or auto() / once() / never() / a policy
  async execute(input) {
    return refund(input);
  },
});
```

See [Human-in-the-loop](./human-in-the-loop) for the `auto`, `always`, `once`, and `never` helpers, input-dependent policies, and how a call waits for approval.

## Shape what the model sees with `toModelOutput`

Omit `toModelOutput` to give the model the full `execute` return. When a channel needs rich data but the model needs only the gist, return a smaller value:

```ts
toModelOutput(output) {
  return { type: "text", value: `Report for ${output.domain}: score ${output.score}.` };
},
```

`toModelOutput` receives the typed `execute` return and changes only what the model sees. Channel handlers and hooks still get the full output on `call.settled`, so a channel can render Slack Block Kit the model never sees.

Return `{ type: "text", value }` for a summary or `{ type: "json", value }` for a smaller, JSON-serializable object.

Do not return secrets, credentials, unnecessary personal data, or unbounded sensitive content from tools. Filter, minimize, and redact tool outputs before you return them.

### Send images to the model with content parts

To give a vision-capable model an image, such as a screenshot or chart, return a `content` output from `toModelOutput`. Build outputs with `toolOutput` and parts with `toolOutputPart`, both from `eve/tools`:

```ts
import { defineTool, toolOutput, toolOutputPart } from "eve/tools";

export default defineTool({
  description: "Capture a screenshot of the current page",
  inputSchema: z.object({ url: z.string() }),
  async execute(input) {
    const png = await captureScreenshot(input.url);
    return { path: png.path, screenshotBase64: png.base64 };
  },
  toModelOutput(output) {
    return toolOutput.content([
      toolOutputPart.text(`Screenshot of ${output.path}:`),
      // file payloads must be base64 strings
      toolOutputPart.file(output.screenshotBase64, { mediaType: "image/png" }),
    ]);
  },
});
```

`toolOutput.text` and `toolOutput.json` build the other two shapes, and hand-written literals also work. eve warns above 3 MiB. A model without vision support fails with its provider's error.

eve writes each returned file to the session sandbox and keeps a reference in session history. Every later model call sends the file again, so it stays visible on later turns. See [Inbound attachments](/docs/sandbox#inbound-attachments).

When compaction drops older tool results, each file becomes a text reference such as `Attached file /workspace/.eve/attachments/<hash>/chart.png (image/png)`. The built-in `read_file` tool can open it again. It shows the model PNG, JPEG, GIF, and WebP images up to 3 MiB and PDFs up to 20 MiB.

#### Send a file by URL

For a large file in object storage, such as a PDF behind a presigned S3 link, return its URL with `toolOutputPart.fileUrl`:

```ts
toModelOutput(output) {
  return toolOutput.content([
    toolOutputPart.text(`Contract ${output.id}:`),
    // only https: URLs are accepted
    toolOutputPart.fileUrl(output.signedUrl, { mediaType: "application/pdf" }),
  ]);
},
```

eve keeps only the URL in session history and doesn't write the file to the sandbox. The provider fetches the URL when it supports that file type. Otherwise, your app runtime downloads the file before the call.

Every later model call sends the URL again, so it must stay readable for the whole session. The provider counts the file's input tokens on each call, as for an inline file.

When compaction drops the tool result, the file becomes `Attached file <filename> (<mediaType>)`, and the URL is gone. Also return a text part with an id or path, so the tool can fetch the file again later.

## More tool options

### Label tool activity

Use `label.start(input)` to describe a call in the activity users see. The callback receives the validated input:

```ts title="agent/tools/deploy.ts"
export default defineTool({
  description: "Deploy a project to an environment.",
  inputSchema: z.object({
    project: z.string(),
    environment: z.enum(["preview", "production"]),
  }),
  label: {
    start: ({ project, environment }) => `Deploy ${project} to ${environment}`,
  },
  async execute(input) {
    return deploy(input);
  },
});
```

The label changes only what users see, not the tool name, input, or behavior. eve records it on the call's public action event, so channels show `Deploy storefront to production` and stream consumers can read it. If the callback throws or returns an empty label, activity shows the tool name in readable form, such as `Sync accounts` for `sync_accounts`.

The provided `bash`, `glob`, `grep`, `read_file`, `web_fetch`, `web_search`, and `write_file` tools define labels. A deferred tool, agent, or connection tool called through [`eve__tool`](/docs/concepts/built-in-tools#eve__search-eve__tool-and-eve__skill) shows its own label. Other built-in labels:

- An `eve__search` call shows `Search tools for “linear issues”`, then `Searched tools for “linear issues”, found 5`.
- A skill load shows `Load skill release-notes`, then `Loaded skill release-notes`.
- A connection's sign-in entry shows `Sign in to Linear`.

### Stream preliminary tool results

A `defineTool` executor written as an async generator sends complete output snapshots before it finishes. Each `yield` replaces the previous snapshot, and the final yield is the tool result:

```ts title="agent/tools/build_report.ts"
export default defineTool({
  description: "Build a project report.",
  inputSchema: z.object({ project: z.string() }),
  label: {
    start: ({ project }) => `Build report for ${project}`,
    delta: (_input, partial) => partial.phase, // runs for each snapshot
    complete: (_input, output) => `Report ready with ${output.report.sections.length} sections`,
  },
  async *execute({ project }) {
    yield { phase: "Collecting sources", report: null };
    const report = await buildReport(project);
    yield { phase: "Complete", report }; // the tool result
  },
});
```

Channels, hooks, and clients see earlier yields as `call.progress` stream records. Only the final yield reaches model history and `toModelOutput`. eve ignores a generator's `return` value; see [Yield and return](#yield-and-return).

Keep the latest snapshot for each tool call id. eve may skip snapshots, and a retried step can send some again.

`label.delta(input, partial)` turns a snapshot into activity text, and `label.complete(input, output)` describes success. Each non-empty result replaces the current label. If a callback throws or returns an empty string, the label stays as it was. `label.complete` doesn't run for failed or rejected calls.

Renderers receive only the label text. The full values stay in `call.progress` and `call.settled`.

### Yield and return

`yield` doesn't make the tool wait for a person, timer, or external event. What a yielded value means depends on the executor:

| Executor             | Ordinary `yield value`                                                                 | Final output                                                                                                |
| -------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `defineTool`         | Earlier values are `call.progress` snapshots; the final yield becomes the tool result. | Last yielded value; a generator `return` value is ignored.                                                  |
| `defineWorkflowTool` | Every yield is a `call.progress` snapshot.                                             | Explicit return value; falls back to the last yield if the return is `null` or `undefined`, then to `null`. |

Progress snapshots never enter model history. Only the final output does.

### End the turn after a tool call

Set `endsTurn: true` when a successful call is the whole answer, such as reacting to a message. eve ends the turn without calling the model again, so channels post no reply:

```ts title="agent/tools/react.ts"
export default defineTool({
  description: "React to the current message. Use this when a reaction is the whole answer.",
  endsTurn: true,
  inputSchema: z.object({ emoji: z.string() }),
  async execute({ emoji }) {
    await addReaction(emoji);
    return { emoji };
  },
});
```

eve adds this sentence, after a blank line, to the description the model sees:

```text
Calling this tool ends your turn once it succeeds: do not write a reply or call other tools in the same step. If it fails, you will see the error and can continue.
```

Leave the turn-ending rule out of `description`. `eve info` shows the description as you wrote it, without the added sentence.

To decide from the result, pass a function. eve calls it with the output of `execute` after each successful call, and the turn ends only when it returns or resolves to `true`:

```ts title="agent/tools/react.ts"
export default defineTool({
  description:
    "React to the current message. When the reaction posts, your turn ends; otherwise, reply in text.",
  inputSchema: z.object({ emoji: z.string() }),
  async execute({ emoji }) {
    return { posted: await addReaction(emoji) };
  },
  // declare after execute so TypeScript infers `output`
  endsTurn: (output) => output.posted,
});
```

If the function throws or rejects, eve logs a warning and treats the result as `false`. With an `outputSchema`, `output` takes that schema's type wherever `endsTurn` appears. eve doesn't add the sentence for a function, so say in `description` when the call ends the turn.

The turn ends only when every tool call in the step can end the turn, succeeds, and passes any `endsTurn` function. Otherwise, the model gets the results and continues. Channels don't post text the model wrote before the call as the reply. While [tasks](/docs/tools/tasks) are still working, the turn waits for them and then calls the model again.

eve ignores `endsTurn` in delegated sessions, which must reply to their caller, and on turns that request structured output. There, the model doesn't see the sentence, and eve doesn't call the function.

Dynamic tool entries accept both forms of `endsTurn`. `defineWorkflowTool` rejects it at build time. To end a turn without any other action, add the opt-in [`no_reply`](/docs/concepts/built-in-tools#no_reply) tool.

### Keep a tool out of the tool list

Set `deferred: true` for a tool the model rarely needs, or when an agent has too many tools to send in every request:

```ts title="agent/tools/export_report.ts"
export default defineTool({
  deferred: true,
  description: "Export a finished report as a PDF.",
  inputSchema: z.object({ reportId: z.string() }),
  execute({ reportId }) {
    return exportReport(reportId);
  },
});
```

eve leaves a deferred tool out of the tool list and doesn't name it in context. The model finds it with [`eve__search`](/docs/concepts/built-in-tools#eve__search-eve__tool-and-eve__skill) and calls it with `eve__tool({ name: "export_report", input })`. Approvals, `endsTurn`, `toModelOutput`, hooks, and stream events then see `export_report`, as for a direct call.

The option works with `defineTool`, `defineWorkflowTool`, and dynamic tool entries. To defer an agent, set [`tool: "deferred"`](/docs/subagents#declared-subagents) on its `defineAgent` definition.

### Hide a tool from delegated agents

Set `availableInSubagents: false` when only a top-level root session should call a tool:

```ts title="agent/tools/coordinator.ts"
export default defineTool({
  availableInSubagents: false, // omit to offer the tool in every session
  description: "Coordinate work that only the top-level root agent should start.",
  inputSchema: z.object({ task: z.string() }),
  execute({ task }) {
    return coordinate(task);
  },
});
```

The option works with `defineTool` and `defineWorkflowTool`. Copies of the root agent from the built-in `agent` tool don't get the tool. A declared subagent's own tool with this option is never offered, because a subagent always runs delegated. The option doesn't change a workflow's `ctx.agents` registry.

### Start channel sessions: `ctx.to`

`ctx.to(channel, target).send(message, options)` starts a session on one of the agent's channels, or continues the session at that target. It's the same call, with the same options, as a route handler's [cross-channel hand-off](/docs/channels/custom#cross-channel-hand-off). It returns the delivery, whose `sessionId` names the session. Use it when work should continue where people can follow it, such as a new Slack thread:

```ts title="agent/tools/escalate.ts"
import { defineTool } from "eve/tools";
import { z } from "zod";
import slack from "../channels/slack";

export default defineTool({
  description: "Hand this investigation to the on-call channel in Slack.",
  inputSchema: z.object({ summary: z.string() }),
  async execute({ summary }, ctx) {
    const delivery = await ctx
      .to(slack, { channelId: "C0123ABC" })
      .send("Take over this investigation.", {
        auth: ctx.session.auth.current, // start the session as the caller
        history: [{ role: "user", text: summary }],
        idempotencyKey: `${ctx.session.id}:${ctx.callId}`, // a retried call sends once
      });
    return { sessionId: delivery.sessionId };
  },
});
```

- The new session belongs to the channel. People on that channel continue it, it has no parent, and it keeps running after the tool returns.
- It runs the root agent with the `auth` you pass.
- A retried tool call runs the tool again, so pass an `idempotencyKey` and the target session gets the message once. Include the session id, because the target session dedupes keys from every sender and `ctx.callId` is unique only within the calling session. Built-in channels map a retry to the same session, including a Slack target with only a `channelId`. A platform post made before the session exists, such as an `initialMessage`, repeats.
- `ctx.to()` throws in a delegated session, such as a subagent, a root copy, or a remote agent call. A session it started would outlive the delegation and act for the caller. Return the result instead, and send from a tool on the root agent.

See [Fork conversations into a new thread](/docs/patterns/fork-conversations-into-new-threads) for a tool that copies the current conversation into a new Slack thread.

### Workflows as tools

`defineWorkflowTool` from `eve/tools` runs each call as a Workflow run that survives restarts and redeploys. Its executor starts with `"use workflow"` and receives `ctx.agent` and `ctx.ask`. It can wait for a person, webhook, or timer without holding compute. See [Workflows as tools](/docs/tools/workflows) for the authoring rules and examples.

### Durable waits

A tool call with an `execute` function finishes before the model continues. The definition decides whether the executor can pause and wait:

| Definition           | Can wait inside `execute`                                | What the model receives            |
| -------------------- | -------------------------------------------------------- | ---------------------------------- |
| `defineTool`         | No; the executor runs inside the step that started it.   | Executor output after it finishes. |
| `defineWorkflowTool` | Yes; the tool call waits while the workflow can suspend. | Workflow output after it finishes. |

A workflow tool that defines `task(input, ctx)` instead runs each call as a [task](/docs/tools/workflows#run-calls-as-tasks-task). The call returns a receipt at once, and the output arrives later in a `task.result` message. One that defines `serve(receive, ctx)` runs a [resumable task](/docs/tools/workflows#resumable-tasks-serve), which takes more calls by its `taskId`.

Subagent tools, whether built-in, declared, or remote, run as resumable tasks, so the child's reply also arrives in `task.result`. In an authored workflow, awaiting a [`ctx.agent` session's `response.result()`](/docs/tools/workflows#delegate-work-ctxagent) waits for the child's turn to end instead.

In a workflow body, keep values needed after a wait in local variables, because workflow replay rebuilds them. Use [`ctx.ask`](/docs/tools/workflows#ask-a-human-ctxask) when the body needs an answer from a person.

## Troubleshooting

| What you see                                  | What to do                                                                                                |
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| The build rejects `deferred` on a tool        | Remove it from provider tools such as `webSearch()`, because the provider has to see their definition.    |
| eve rejects a file part                       | Pass the file as a base64 string. Raw bytes (`Uint8Array`, `Buffer`) can't be saved as JSON.              |
| Later model calls fail after a `fileUrl` part | Use a URL that stays readable for the whole session. A presigned URL that expires makes later calls fail. |

## When to use a connection or skill instead

To give the model tools from an external MCP or OpenAPI service, use a [connection](/docs/connections). For a written procedure the model loads only when needed, use a [skill](/docs/skills).

## What to read next

- [Human-in-the-loop](./human-in-the-loop): gate a tool on approval, or have the agent ask a question
- [Built-in tools](/docs/concepts/built-in-tools): the default and opt-in framework tools and how to override or disable them
- [Dynamic capabilities](/docs/guides/dynamic-capabilities): tools whose set is resolved per session with `defineDynamic`
- [Authentication](/docs/guides/auth-and-route-protection): authenticate a tool to an external service

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)