# Tools

> defineTool schema and execute(ctx), HITL approval predicates (always/once/never), tool auth and requireAuth, built-in overrides, and dynamic tool resolution.

- Repository: vercel/eve
- GitHub: https://github.com/vercel/eve
- Human docs: https://www.grok-wiki.com/public/docs/vercel-eve-759e1d74a10f
- Complete Markdown: https://www.grok-wiki.com/public/docs/vercel-eve-759e1d74a10f/llms-full.txt

## Source Files

- `docs/tools.mdx`
- `packages/eve/src/public/definitions/tool.ts`
- `packages/eve/src/public/tools/index.ts`
- `packages/eve/src/compiler/normalize-tool.ts`
- `apps/fixtures/weather-agent/agent/tools/get_weather.ts`
- `docs/guides/dynamic-capabilities.md`

---

---
title: "Tools"
description: "defineTool schema and execute(ctx), HITL approval predicates (always/once/never), tool auth and requireAuth, built-in overrides, and dynamic tool resolution."
---

A tool is a typed action the model can call — query a database, call an API, or trigger a business workflow. You author tools as TypeScript files under `agent/tools/`. They run in the **app runtime** with full access to `process.env` and your shared libraries, not inside the [sandbox](/sandbox). Built-in shell and file tools are the exception: they proxy work into the sandbox from the app runtime.

Eve never executes authored tools during discovery. The model sees descriptors first; only tools it actually calls run. Completed steps replay their recorded result on resume. A step interrupted mid-execution re-runs, so make non-idempotent side effects idempotent or gate them behind approval.

## Author a static tool

Place one tool per file under `agent/tools/`. The filename slug is the model-facing name — no `name` field on the definition.

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

Nested directories flatten into a single slug-safe segment: `agent/tools/billing/refund.ts` registers as `billing-refund`. Path separators never reach the model because most providers reject `/` in tool names.

### `defineTool` fields

<ParamField body="description" type="string" required>
  Model-facing summary of what the tool does.
</ParamField>

<ParamField body="inputSchema" type="Zod schema | Standard Schema | JSON Schema object" required>
  Validates and types the model's tool-call arguments. 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>`.
</ParamField>

<ParamField body="execute" type="(input, ctx) => TOutput | Promise<TOutput>" required>
  The implementation. May be sync or async.
</ParamField>

<ParamField body="outputSchema" type="Zod schema | Standard Schema | JSON Schema object">
  Optional schema describing the return value. With Zod or Standard Schema it also types the `execute` return. Code mode uses it to expose typed host-tool results to model-authored JavaScript.
</ParamField>

<ParamField body="needsApproval" type="(ctx) => boolean">
  Optional human-in-the-loop gate. See [Human-in-the-loop approval](#human-in-the-loop-approval).
</ParamField>

<ParamField body="auth" type="ToolAuthDefinition">
  Optional outbound authorization strategy. See [Tool auth](#tool-auth).
</ParamField>

<ParamField body="toModelOutput" type="(output) => ToolModelOutput">
  Optional projection controlling what the model sees as the tool result. See [Shape model output](#shape-model-output).
</ParamField>

`defineTool` stamps a brand that lifecycle code validates; raw object literals are rejected at compile time.

## The `ctx` parameter

`execute` receives a `ToolContext` — `SessionContext` plus token accessors when `auth` is declared.

| Member | Purpose |
| --- | --- |
| `ctx.session.id` | Active session identifier |
| `ctx.session.auth` | Caller snapshot from route auth (`current`, `initiator`) |
| `ctx.session.turn` | Active turn metadata |
| `ctx.session.parent` | Parent lineage when running inside a subagent |
| `ctx.getSandbox()` | Resolves the live sandbox handle |
| `ctx.getSkill(id)` | Reads a packaged skill's metadata and files |
| `ctx.getToken()` | Resolves the bearer for a declared `auth` strategy |
| `ctx.requireAuth()` | Forces the authorization flow before proceeding |

`getToken` and `requireAuth` throw when the tool does not declare `auth`. These APIs are live only inside active authored runtime execution (tools, hooks, channel handlers). Calling them at module top level throws.

Running in the app runtime is what lets a tool import shared code from `lib/`, read secrets from the environment, and participate in Eve's durable pause/resume model. See [State, hooks, and session context](/state-hooks-and-context) for the full context model.

## Human-in-the-loop approval

Gate sensitive tools with `needsApproval` and 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() }),
  needsApproval: always(),
  async execute(input) {
    return refund(input);
  },
});
```

| Helper | Behavior |
| --- | --- |
| `never()` | Never require approval (equivalent to omitting `needsApproval`). |
| `once()` | Require approval only the first time the tool runs in a session; auto-allow after an explicit approval. A denial leaves the tool unrecorded, so the next call prompts again. Keys off the bare tool name. |
| `always()` | Require approval before every call. |

For input-aware decisions, pass a custom predicate instead of a helper. It receives `{ toolName, toolInput, approvedTools }` and returns a boolean. `toolInput` can be undefined, so guard access:

```ts
needsApproval: ({ toolInput }) => (toolInput?.amount ?? 0) > 1000,
```

### How approval pause and resume works

Approvals and `ask_question` share one protocol:

1. The model requests input (approval or a question).
2. Eve emits an `input.requested` stream event carrying the pending requests.
3. The turn parks at `session.waiting`, durably.
4. The client answers with `inputResponses` (structured, keyed by `requestId`) or a normal follow-up `message`. A follow-up whose text matches an option label (case-insensitive) resolves automatically.

The run resumes exactly where it parked. Channels render the request — Slack turns approvals into buttons, Teams into Adaptive Cards. See [Sessions and streaming](/sessions-and-streaming) for the event and resume contract.

When a tool declares both `auth` and `needsApproval`, approval runs first. The user sees **approve, then sign in**. Eve records the approval before the sign-in park, so the tool is not re-prompted for approval after OAuth completes.

## Shape model output

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 a summary, project it with `toModelOutput`:

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

<ResponseField name="type" type="'text' | 'json'">
  `text` for a string summary; `json` for a smaller object.
</ResponseField>

<ResponseField name="value" type="string | unknown">
  The model-facing payload.
</ResponseField>

`toModelOutput` only affects what the model sees. Channel event handlers and hooks still receive the full output on `action.result`.

## Tool auth

When one tool calls a service behind OAuth or needs a bearer token, declare `auth` on the tool instead of wiring a separate connection. `auth` accepts the same shapes as connection auth:

- `connect("...")` from `@vercel/connect/eve` for Vercel Connect-backed OAuth
- A custom interactive definition via `defineInteractiveAuthorization`
- A plain `{ getToken }` object for static or pre-provisioned credentials

```ts title="agent/tools/list_okta_groups.ts"
import { defineTool } from "eve/tools";
import { connect } from "@vercel/connect/eve";
import { z } from "zod";

export default defineTool({
  description: "List the caller's Okta groups.",
  inputSchema: z.object({}),
  auth: connect("okta"),
  async execute(_input, ctx) {
    const { token } = await ctx.getToken();
    const res = await fetch("https://api.okta-proxy.internal/groups", {
      headers: { authorization: `Bearer ${token}` },
    });
    if (res.status === 401) ctx.requireAuth();
    return res.json();
  },
});
```

A `getToken`-only strategy defaults `principalType` to `"app"`. Interactive strategies use `principalType: "user"`. Set `displayName` on the `auth` definition to control the sign-in affordance label; it is presentation-only — the tool's path-derived name still keys the authorization scope, token cache, and callback URL.

`ctx.getToken()` checks the per-step token cache before invoking `getToken`. With an interactive strategy, a cache miss suspends the turn on a framework-owned callback URL, shows a sign-in affordance, and re-runs the tool after the OAuth callback completes.

`ctx.requireAuth()` throws `ConnectionAuthorizationRequiredError`, which the runtime converts into the same consent prompt. Map a downstream `401` to `requireAuth()` so Eve evicts the rejected token and re-challenges instead of returning a dead-token error to the model.

Route auth (who can reach your HTTP API) and tool auth (how your agent signs in to external services) are independent systems. See [Security model](/security-model) and [Connections](/connections) for trust boundaries and connection-level auth.

## Override or disable built-in tools

Every agent ships with built-in tools (`bash`, `read_file`, `write_file`, `glob`, `grep`, `web_fetch`, `web_search`, `todo`, `ask_question`, `agent`, `load_skill`, `connection_search`). See [Default harness](/default-harness) for the full set.

### Override

Author a tool at the same slug and it replaces the built-in. Spread the default from `eve/tools/defaults` to keep description, schema, and framework state (such as the `todo` tool's durable state key):

```ts title="agent/tools/write_file.ts"
import { defineTool } from "eve/tools";
import { writeFile } from "eve/tools/defaults";

export default defineTool({
  ...writeFile,
  async execute(input, ctx) {
    console.log("[write_file]", input.path);
    return writeFile.execute(input, ctx);
  },
});
```

Framework defaults are importable as `bash`, `readFile`, `writeFile`, `glob`, `grep`, `webFetch`, `webSearch`, `todo`, and `loadSkill`. To add a sandbox tool under a **new** name (without replacing a built-in), use the factory helpers: `defineBashTool`, `defineReadFileTool`, `defineWriteFileTool`, `defineGlobTool`, and `defineGrepTool` from `eve/tools`.

### Disable

Export a `disableTool()` sentinel from a file named after the tool slug:

```ts title="agent/tools/bash.ts"
import { disableTool } from "eve/tools";

export default disableTool();
```

If the filename matches no known framework tool, resolution fails at build time rather than silently doing nothing.

### Opt in to `Workflow`

The experimental `Workflow` tool stays off unless you re-export the opt-in marker:

```ts title="agent/tools/workflow.ts"
export { ExperimentalWorkflow as default } from "eve/tools";
```

With it enabled, the model can orchestrate subagents from model-authored JavaScript as one durable step.

## Dynamic tool resolution

When the right tool set is not known until runtime — it depends on the caller, tenant, feature flags, or external data — use `defineDynamic` instead of a static `defineTool` export. See [Context control](/context-control) for how dynamic capabilities fit the broader model.

```ts title="agent/tools/query.ts"
import { defineDynamic, defineTool } from "eve/tools";
import { z } from "zod";
import { listTables, runReadOnly } from "../lib/warehouse.js";

export default defineDynamic({
  events: {
    "session.started": async (_event, ctx) =>
      Object.fromEntries(
        (await listTables()).map((t) => [
          t.name,
          defineTool({
            description: `Query ${t.name}. Columns: ${t.columns.join(", ")}`,
            inputSchema: z.object({ sql: z.string() }),
            execute: ({ sql }) => runReadOnly(t.name, sql),
          }),
        ]),
      ),
  },
});
```

### Return shapes and naming

| Return shape | File | Tool name(s) |
| --- | --- | --- |
| Single `defineTool(...)` | `agent/tools/analytics.ts` | `analytics` |
| Map `{ export, query }` | `agent/tools/tenant.ts` | `tenant__export`, `tenant__query` |
| `null` | any | Contributes nothing for that event |

A map return always uses `slug__key`, even for a single entry, so adding a second tool later never renames the first.

### Resolver events

| Event | Resolver runs | Tools available for |
| --- | --- | --- |
| `session.started` | Once per session | Every model call in the session |
| `turn.started` | Once per turn | Every model call in the turn |
| `step.started` | Before each model call | That model call only |

When an event fires, the channel adapter handler runs first, then stream-event hooks, then dynamic resolvers. The tool loop reads the current set right before each model call. Resolvers across files run concurrently. The most recently fired event for a file replaces that file's earlier contribution.

### Inline `execute` requirement

Write `execute` as an inline function expression placed directly as the property value. The bundler transform does not detect `execute: myFn` or `execute: makeFn()`, so those tools work on the first step but do not survive replay after a crash or resume. On later steps the transform reconstructs each `execute` from stored closure variables.

`needsApproval` on dynamic tools is only honored for `step.started`-scoped entries whose live `execute` closures survive into the harness. Session- and turn-scoped dynamic tools replay from durable metadata and cannot carry a function across replay.

## Durability and idempotency

| Phase | Behavior |
| --- | --- |
| Discovery / `eve build` | Compiles descriptors; never runs `execute` |
| Model sees descriptors | Tool names, descriptions, and input schemas only |
| Tool call executes | `execute` runs in the app runtime |
| Step completes | Result is recorded durably |
| Resume / replay | Completed steps return the recorded result; interrupted steps re-run `execute` |

Treat charges, emails, and other irreversible side effects as idempotent, or gate them behind `needsApproval`.

## Verify tools are discovered

After authoring, confirm the tool appears in the agent manifest:

```sh
pnpm exec eve info
```

The output lists authored tools alongside framework defaults. Run `pnpm exec eve build` to surface compile-time errors such as invalid schemas, a `disableTool()` filename that matches no known default, or a dynamic resolver with a non-inline `execute`.

## Related pages

<CardGroup cols={2}>
  <Card title="Default harness" href="/default-harness">
    Built-in tools every agent ships with, and override or disable patterns.
  </Card>
  <Card title="Context control" href="/context-control">
    Dynamic capabilities, skills, and subagent context isolation.
  </Card>
  <Card title="Connections" href="/connections">
    MCP and OpenAPI tools with qualified names and connection-level auth.
  </Card>
  <Card title="State, hooks, and session context" href="/state-hooks-and-context">
    Full `ctx` model, `defineState`, and `defineHook` stream subscribers.
  </Card>
  <Card title="Sessions and streaming" href="/sessions-and-streaming">
    `input.requested` events, `inputResponses`, and reconnect behavior.
  </Card>
  <Card title="Security model" href="/security-model">
    App runtime vs sandbox trust boundaries and secret brokering.
  </Card>
  <Card title="Project layout" href="/project-layout">
    Path-derived naming rules and what compiles into `.eve/` artifacts.
  </Card>
  <Card title="TypeScript API" href="/typescript-api">
    Exported `define*` helpers, approval predicates, and built-in defaults.
  </Card>
</CardGroup>
