Tools
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
- Tools run in your app runtime, not in the sandbox. They can read
process.envand import shared code fromlib/. inputSchemais a Zod schema, any Standard Schema, or a plain JSON Schema object. Zod and Standard Schema infer theinputtype, and a plain JSON Schema types it asRecord<string, unknown>.- eve validates the input before
executeruns. 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
$schemakeyword. It validates with JSON Schema 2020-12 rules, which cover draft-07 keywords. - For a JSON Schema, eve fills omitted properties from their
defaultvalues.formatis an annotation only. - An unknown
typename or an unresolvable$refstops eve from checking a JSON Schema. The model still receives the schema, andexecutegets 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
outputSchemafor structured output. With Zod or Standard Schema, it also types theexecutereturn. - 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 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'sctx.messages()sees it.ctx.abortSignal: aborts when the turn is cancelled. Pass it to work that supports cancellation. Sandbox sessions fromctx.getSandbox()already use it.ctx.getSandbox(): the live sandbox handle.ctx.channel: the channel serving the session, as hooks and dynamic resolvers see it:kind,continuationToken, andmetadata.metadatais what the channel'smetadata(state)hook returns, such as a Slack thread'schannelIdandthreadTs.ctx.to(channel, target): starts or continues a session on one of the agent's channels. See Start channel sessions.
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. |
toModelOutput | full return | Changes what the model sees. |
label | readable tool name | Sets activity text. |
endsTurn | false | Ends the turn after success. |
deferred | false | Leaves the tool out of the tool list. |
availableInSubagents | true | Offers the tool in delegated sessions. |
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:
- Pass a stable idempotency key to the upstream API.
- Otherwise, record a unique application operation before writing, and check it on every attempt.
- 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.
For one-way provider notifications, see 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:
See 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:
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. Return { type: "error-text", value }, or toolOutput.errorText(value), to show the model a failure, such as a command that exited non-zero. Providers mark it as an error result, and so does the MCP channel (isError). The call still completes, so channels and hooks get the full output.
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:
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.
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:
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:
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, edit_file, 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 shows its own label. Other built-in labels:
- An
eve__searchcall showsSearch tools for “linear issues”, thenSearched tools for “linear issues”, found 5. - A skill load shows
Load skill release-notes, thenLoaded 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:
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.
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:
eve adds this sentence, after a blank line, to the description the model sees:
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:
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 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 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:
eve leaves a deferred tool out of the tool list and doesn't name it in context. The model finds it with eve__search 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" on its defineAgent definition.
Hide a tool from delegated agents
Set availableInSubagents: false when only a top-level root session should call a tool:
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. 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:
- 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
authyou pass. - A retried tool call runs the tool again, so pass an
idempotencyKeyand the target session gets the message once. Include the session id, because the target session dedupes keys from every sender andctx.callIdis unique only within the calling session. Built-in channels map a retry to the same session, including a Slack target with only achannelId. A platform post made before the session exists, such as aninitialMessage, 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 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 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. 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, 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() 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 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. For a written procedure the model loads only when needed, use a skill.
What to read next
- Human-in-the-loop: gate a tool on approval, or have the agent ask a question
- Built-in tools: the default and opt-in framework tools and how to override or disable them
- Dynamic capabilities: tools whose set is resolved per session with
defineDynamic - Authentication: authenticate a tool to an external service