Build Your First Tool
Step-by-step guide to creating a custom tool with typed input and output. Use tool() from the AI SDK, wire it into your agent, and access ToolContext.
This guide walks through creating a custom tool from scratch: defining parameters with zod, implementing the execute function, wiring it into the agent, and accessing the sandbox and context via ToolContext.
Understand Tools
Tools give agents the ability to take actions: read files, run commands, call APIs, search data. a2 includes 8 built-in tools (Read, Write, Edit, Grep, List, Bash, Skill, JavaScript) that run in the sandbox. Custom tools use tool() from the Vercel AI SDK (ai package), not from a2.
Create a Tool with tool()
Use tool() from ai and define:
- description — Tells the model when and how to use the tool
- parameters — Zod schema for typed input
- execute — Async function that runs when the tool is called
- outputSchema (optional) — Zod schema for typed output
import { tool } from "ai";
import { z } from "zod";
export const SearchDocs = tool({
description: "Search documentation by query. Returns matching pages with title, description, and slug.",
parameters: z.object({
query: z.string().describe("Search query to find relevant docs"),
limit: z.number().min(1).max(20).default(5).describe("Max number of results"),
}),
outputSchema: z.object({
results: z.array(
z.object({
title: z.string(),
description: z.string(),
slug: z.string(),
})
),
}),
execute: async ({ query, limit }) => {
const docs = [
{ title: "Quickstart", description: "Get started in 5 minutes", slug: "/docs/quickstart" },
{ title: "Tools", description: "Built-in and custom tools", slug: "/docs/concepts/tools" },
{ title: "Sessions", description: "Persistent conversations", slug: "/docs/concepts/sessions" },
];
const lower = query.toLowerCase();
const matches = docs
.filter(
(d) =>
d.title.toLowerCase().includes(lower) ||
d.description.toLowerCase().includes(lower)
)
.slice(0, limit);
return { results: matches };
},
});Wire It Into Your Agent
Add the tool to your agent's tools object:
import { agent } from "experimental-agent";
import { vercelSandbox } from "experimental-agent/sandbox";
import { SearchDocs } from "@/tools/search-docs";
export const myAgent = agent("my-agent", {
model: "anthropic/claude-opus-4.6",
system: "You are a documentation assistant. Use SearchDocs to find relevant docs for the user.",
sandbox: vercelSandbox(),
tools: {
SearchDocs,
},
});The agent now has access to both the 8 built-in tools and SearchDocs. The model decides when to call it based on the description and user request.
Access ToolContext in Execute
Custom tools receive ToolContext via experimental_context in the execute function:
type ToolContext<TContext, TState> = {
context: TContext; // From contextSchema, passed per-request
state: TState; // From stateSchema, mutable, persisted per-session
sessionId: string;
sandbox: SandboxInstance;
messages: UIMessage[];
};Example: use the sandbox to read files and context for auth:
import { type ToolContext } from "experimental-agent";
import { tool } from "ai";
import { z } from "zod";
export const FetchRepo = tool({
description: "Clone a GitHub repo into the sandbox",
parameters: z.object({
repo: z.string().describe("GitHub repo in owner/name format"),
}),
execute: async ({ repo }, { experimental_context }) => {
const ctx = experimental_context as ToolContext;
const { sandbox, context } = ctx;
const token = context.authToken;
const url = token
? `https://x-access-token:${token}@github.com/${repo}.git`
: `https://github.com/${repo}.git`;
const execResult = await sandbox.exec({
command: "git",
args: ["clone", url, "/app/repo"],
});
const { exitCode, stdout, stderr } = await execResult.result;
return {
success: exitCode === 0,
stdout,
stderr,
};
},
});Wire it into an agent with contextSchema:
export const myAgent = agent("my-agent", {
model: "anthropic/claude-opus-4.6",
contextSchema: z.object({ authToken: z.string().optional() }),
tools: { FetchRepo },
});Pass context when sending:
await session.send("Clone vercel/next.js", {
context: { authToken: req.headers.get("authorization") ?? "" },
});Test the Tool
Send a message that triggers the tool:
curl http://localhost:3000/api/chat/test-session \
-H "Content-Type: application/json" \
-d '{"message": "Search the docs for sessions"}'The stream will include:
- A tool call part with
SearchDocsand{ query: "sessions", limit: 5 } - A tool result part with
{ results: [...] } - The model's text response summarizing the results
Use session.history() to get assembled messages:
const { messages, streamingMessageId, usage } = await session.history();Add Approval Gating
Require human approval before the tool runs. Add needsApproval to the tool or the agent:
Per-tool (on the tool definition):
export const SearchDocs = tool({
description: "Search documentation by query.",
parameters: z.object({
query: z.string(),
limit: z.number().min(1).max(20).default(5),
}),
needsApproval: async ({ query }) => query.toLowerCase().includes("delete"),
execute: async ({ query, limit }) => {
// ...
},
});Agent-level (maps tool names to rules):
export const myAgent = agent("my-agent", {
model: "anthropic/claude-opus-4.6",
tools: { SearchDocs },
needsApproval: {
SearchDocs: true,
},
});Per-tool needsApproval takes priority over the agent-level map. When approval is needed, the stream emits tool-approval-request and the workflow suspends until session.send({ type: "approval", approval: { approvalId, approved } }) is called. See Approvals for the full flow.
Next Steps
- Custom Tools — Advanced patterns and best practices
- Extending Built-in Tools — Wrap or augment Read, Write, Bash, etc.
- Tools — Built-in tools, ToolContext, and
activeTools - Build Your First Agent — End-to-end agent setup