---
title: "Tools"
description: "Define typed actions the agent can call, and gate sensitive ones on human approval."
url: /tools
---

A tool is a typed action the agent can call, such as hitting an API, running a query, or writing a file. Define a tool when you implement the action in code you control. To give the model tools published by an external MCP or OpenAPI service, use a [connection](/docs/connections) instead. Authored tools run in your app runtime with full access to `process.env`, not in the [sandbox](/docs/sandbox).

## Define a tool

The filename is the tool name the model sees. A file at `agent/tools/get_weather.ts` is exposed as `get_weather`.

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

export default defineTool({
  description: "Get the current weather for a city.",
  inputSchema: z.object({ city: z.string().min(1) }),
  async execute({ city }, ctx) {
    return { city, condition: "Sunny", temperatureF: 72 };
  },
});
```

A tool definition needs:

- a filename slug under `agent/tools/`, the model-facing name.
- a `description`: what the tool does, written for the model.
- an `inputSchema`: a Zod schema (or any Standard Schema, or a plain JSON Schema object). Required. For no input, pass `z.object({})`. Zod and Standard Schema infer the `input` type in `execute`. Plain JSON Schema types it as `Record<string, unknown>`.
- an `execute(input, ctx)`: the implementation. May be sync, async, or an async generator.

When a tool returns structured data, add an optional `outputSchema`. With Zod or Standard Schema it also types the `execute` return.

eve validates the model's input against `inputSchema` before `execute` runs:

- A Zod or Standard Schema validates with its own library, so tools work with whichever Zod version your app installs.
- A plain JSON Schema object reaches the model as written, without its `$schema` keyword. eve validates input against it with JSON Schema 2020-12 rules, which also cover draft-07 keywords, then fills omitted properties from their `default` values.
- eve treats `format` as an annotation and does not reject input for it.
- When eve cannot evaluate a JSON Schema locally, for example because of an unknown `type` name or a `$ref` it cannot resolve, the model still receives the schema and eve passes the input to `execute` unvalidated.

Optional input fields remain optional for authored tools and connection tools. eve explicitly disables provider strict mode for function tools so providers such as OpenAI Responses do not implicitly require every property. Local input validation still follows the tool's schema.

### Hide a tool from delegated agents

Set `availableInSubagents: false` when a tool should be callable only in a top-level root session:

```ts title="agent/tools/coordinator.ts"
export default defineTool({
  availableInSubagents: false,
  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 defaults to `true` and works with `defineTool` and `defineWorkflowTool`. A root copy does not receive a tool with this option, and a tool authored inside a declared subagent is unavailable because that agent runs in a delegated session. This controls tool availability only; it does not add or remove entries from a workflow's `ctx.agents` registry.

### Keep a tool out of the tool list

Set `deferred: true` when the model needs a tool only occasionally, or when an agent has more tools than you want in every model 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);
  },
});
```

A deferred tool is not in the model's tool list, and eve doesn't name it in context. The model finds its name, description, and signature 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 })`. After that, the call runs exactly like a direct call: approvals, `endsTurn`, `toModelOutput`, hooks, and stream events see `export_report`, not `execute`.

The option works with `defineTool`, `defineWorkflowTool`, and dynamic tool entries. Provider tools such as `webSearch()` cannot be deferred, because the provider has to see their definition; the compiler rejects `deferred` on them. To defer an agent, set [`tool: "deferred"`](/docs/subagents#declared-subagents) on its `defineAgent` definition.

Compiled tools preserve Zod schema composition, including input and output schemas imported from helper modules. Durable replay metadata stays separate from the original schema, so adding it does not mutate shared schema objects.

### 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 after the call instead of calling the model again, so the turn completes without a final message and 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 };
  },
});
```

With `endsTurn: true`, eve tells the model that the call ends its turn by appending 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.
```

Write `description` for what the tool does and when to use it, and leave the turn-ending rule to eve. The appended sentence is not part of the tool definition, so `eve info` shows the description as you wrote it.

To decide from the result instead, pass a function. eve calls it with the output of `execute` after each successful call, and the call ends the turn only when the function returns `true` or resolves to `true`. If the function throws or rejects, eve logs a warning and the turn continues as though it returned `false`:

```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) };
  },
  endsTurn: (output) => output.posted,
});
```

Declare `endsTurn` after `execute`, as above, so TypeScript infers `output` from what `execute` returns. With an `outputSchema`, `output` takes that schema's type wherever `endsTurn` appears.

eve does not append the sentence for a function, because whether the call ends the turn is known only after it runs. Describe the behavior in `description` when the model needs to know it.

The turn ends only when every tool call in the model's step can end the turn and succeeds, and every `endsTurn` function returns `true`. Otherwise the model receives the results and continues as usual. Text the model writes before the call is narration, so channels do not post it 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 return a reply to their caller, and on turns that request structured output; the model does not see the appended sentence there, and eve does not call an `endsTurn` function. `defineWorkflowTool` rejects `endsTurn` at build time. Dynamic tool entries accept `endsTurn: true` or `false`; eve rejects a function when the resolver returns the entry, because dynamic tool metadata is stored durably. To end a turn without taking any other action, add the opt-in [`no_reply`](/docs/concepts/built-in-tools#no_reply) tool.

### Label tool activity

Use `label.start(input)` to describe a call in user-facing activity. The callback receives the validated tool input, so it can select the useful fields without exposing the full argument object:

```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 is presentation only. It does not replace the path-derived tool name, enter model input, or change execution. eve records the bounded label on the corresponding public action event so activity renderers and other authorized stream consumers can use it without interpreting raw tool data. Built-in channel activity can show `Deploy storefront to production` instead of `Deploy`; if the callback throws or returns an empty label, activity falls back to a readable form of the tool name, such as `Sync accounts` for `sync_accounts`.

Provided tools define labels for `bash`, `glob`, `grep`, `read_file`, `web_fetch`, `web_search`, and `write_file`.

Calls the model makes through the [catalog](/docs/concepts/built-in-tools#eve__search-eve__tool-and-eve__skill) read the same way. A deferred tool, agent, or connection tool called through `eve__tool` shows its own label, exactly as a direct call would. An `eve__search` call shows `Search tools for “linear issues”`, then `Searched tools for “linear issues”, found 5` when it returns. A skill load shows `Load skill release-notes`, then `Loaded skill release-notes`, and a connection's sign-in entry shows `Sign in to Linear`.

### Stream preliminary tool results

For a `defineTool` executor, an async generator streams complete
output snapshots before it finishes. Each `yield` replaces the previous snapshot; the final
yield is the normal tool result the model receives:

```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,
    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 };
  },
});
```

eve publishes every earlier yield as an `action.partial` stream event. The
snapshot is visible to channels, hooks, and clients but never enters model
history or `toModelOutput`; only the final yield does. Treat snapshots as
last-write-wins by tool call id, not append-only progress. The durable runtime
can retry a step and replay overlapping snapshots. A generator `return` value is not used as
the result in this mode; yield the final output. Workflow generators have different result
rules, described under [yield and return](#yield-and-return).

Use `label.delta(input, partial)` to project preliminary snapshots into
user-facing activity, and `label.complete(input, output)` to describe successful
settlement differently. Both callbacks receive the validated input first and the typed value yielded by
`execute` second. Each non-empty update replaces the previous activity label; the
result projection replaces the latest update immediately before eve marks the
action complete.

eve normalizes and bounds projected text before rendering it. If either
callback throws or returns an empty string, eve keeps the existing activity
label. `label.complete` does not run for failed or rejected tool calls. The
full values remain available in `action.partial` and `action.result`; renderers
receive only the projected text.

### Durable waits

A tool call with an `execute` entry point finishes before the model continues. The definition
decides whether the executor can wait durably while the call is pending:

| Definition           | Durable waits inside `execute`                           | Result delivered to the model      |
| -------------------- | -------------------------------------------------------- | ---------------------------------- |
| `defineTool`         | No; the executor runs inside the initiating step.        | 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)` in place of `execute` runs each call as a
[task](/docs/tools/workflows#run-calls-as-tasks-task):
the call returns a receipt at once, and the workflow's output reaches the model later in a
`task.result` message. One that defines `serve(receive, ctx)` runs a
[resumable task](/docs/tools/workflows#resumable-tasks-serve), which the model can send more calls
by its `taskId`.

Built-in, declared local, and remote subagent tools run as resumable tasks: each call returns a
receipt at once, and the child's reply reaches the model later in a `task.result` message.
Inside 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.

The `TaskExec` argument and `task.postMessage` have been removed. Return the result. In a workflow
body, keep values needed after a wait in local variables; workflow replay reconstructs them across
durable waits. Use [`ctx.ask`](/docs/tools/workflows#ask-a-human-ctxask) when the body needs an
answer from the human.

### Yield and return

`yield` hands a value to eve's generator consumer, which processes it and advances the generator.
It does not by itself create a durable wait for a person, timer, or external event. The value's
meaning depends on the executor:

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

Progress snapshots do not enter model history as intermediate tool results. The final output
selected by the rules above becomes the tool result.

### Workflows as tools

Use `defineWorkflowTool` from `eve/tools` to run each call as a durable Workflow run. Its
executor must start with `"use workflow"` and receives `ctx.agent` and `ctx.ask`. The tool can
wait for a person, webhook, or timer without holding compute, then return its result to the model. See [Workflows as tools](/docs/tools/workflows) for the authoring rules and
examples.

### The `ctx` parameter

`execute` gets a `ctx` carrying the runtime accessors:

- `ctx.session`: session metadata, turn, auth, parent lineage.
- `ctx.callId`: the id of the current tool call, carried by the call's [stream events](/docs/concepts/sessions-runs-and-streaming) and approval context.
- `ctx.toolName`: the final runtime name the model called, including any namespace qualification.
- `ctx.messages`: the model input for the step that requested this call, the same messages a `step.started` [dynamic resolver](/docs/guides/dynamic-capabilities) reads.
- `ctx.abortSignal`: aborts when the active turn is cancelled. Pass it to cancellation-aware work; sandbox sessions from `ctx.getSandbox()` are already bound to it.
- `ctx.getSandbox()`: the live [sandbox](/docs/sandbox) handle.

Running in the app runtime is what lets a tool import shared code from `lib/`, read `process.env`, and take part in eve’s durable pause/resume model.

eve never runs authored tools during discovery. The model sees descriptors first, and only what it actually calls gets executed. Completed steps never re-run; eve replays the recorded result. A step interrupted mid-execution re-runs, so make non-idempotent side effects like charges or emails idempotent, or gate them with approval.

## When a tool throws

If an authored tool's `execute` function throws, eve records a failed `action.result` and gives the error to the model as a tool error. The model can respond, choose another action, or call the tool again. eve does not automatically call the tool again based on the exception type, an upstream HTTP status, or a `retryable` property.

Authored tools have no public terminal-error class or retry policy for thrown exceptions. Handle retry policy inside the tool when the operation is safe to retry, and return or throw an actionable error when it is not. Do not rely on that distinction to protect a write: an interruption before the durable step completes can re-run the step and execute the tool again even if the first request reached the upstream service.

Protect non-idempotent operations 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, not as a substitute for deduplication after an ambiguous network result.

See [Execution model and durability](/docs/concepts/execution-model-and-durability#resuming-after-a-crash) for step replay semantics. 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 sign off 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);
  },
});
```

Approval is one half of eve's [human-in-the-loop](./human-in-the-loop) model — the page covers the `auto`/`always`/`once`/`never` helpers, input-dependent policies, and how a gated call pauses and resumes durably.

## Shape what the model sees with `toModelOutput`

By default the model sees the full `execute` return. When a tool returns rich data a channel needs for rendering but the model only needs the gist, project it down with `toModelOutput`:

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

`toModelOutput` receives the final, typed `execute` return and only affects the model. Channel event handlers and hooks still get the full output on `action.result`, so a channel can render rich platform output (Slack Block Kit, say) the model never sees. Return `{ type: "text", value }` for a summary, or `{ type: "json", value }` for a smaller object.

Tool outputs must be JSON-serializable. Return plain objects, arrays, strings, numbers, booleans, or `null`; convert values like `Date`, `Map`, `Set`, `NaN`, and cyclic objects before returning them from `execute` or from a `{ type: "json" }` `toModelOutput`.

### Send images to the model with content parts

A tool that produces an image — a screenshot, a rendered chart — can hand the pixels to a vision-capable model by returning a `content` output from `toModelOutput`. Build outputs with the `toolOutput` helpers and parts with the `toolOutputPart` helpers, 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}:`),
      toolOutputPart.file(output.screenshotBase64, { mediaType: "image/png" }),
    ]);
  },
});
```

The `toolOutput.text` and `toolOutput.json` builders construct the other two output shapes; hand-written literals remain valid everywhere.

File payloads must be base64 strings — raw bytes (`Uint8Array`, `Buffer`) are rejected because they do not survive eve's durable JSON boundary. eve warns above 3 MiB. Sending image parts to a model without vision support fails with that provider's error, the same as image parts in user messages.

eve writes each returned file to the session sandbox and keeps only a reference in session history, so the file's bytes are not stored again on every workflow step. Every later model call sends the same file, which keeps it visible on later turns and keeps the provider's prompt cache valid. See [Inbound attachments](/docs/sandbox#inbound-attachments). Compaction counts an image at roughly what providers bill for it rather than its base64 length.

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 shows PNG, JPEG, GIF, and WebP images up to 3 MiB and PDFs up to 20 MiB to the model as files, so the agent can open a compacted image or PDF again.

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

## What to read next

- [Human-in-the-loop](./human-in-the-loop): gate a tool on approval, or have the agent ask a question
- [Skills](/docs/skills): on-demand procedures the model loads when relevant
- [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
