Overview
Stagehand agents can be extended with custom tools that provide additional capabilities beyond browser automation. This example shows how to integrate external APIs and services as tools that your agent can use.
You must enable experimental: true in your Stagehand configuration to use custom tools and MCP integrations.
Basic Custom Tool Example
Here’s how to create a custom weather tool and pass it to a Stagehand agent:
import { z } from "zod";
import { tool } from "ai";
import { Stagehand } from "@stagehand/core";
import chalk from "chalk";
// Mock weather API, replace with your own API/tool logic
const fetchWeatherAPI = async (location: string) => {
return {
temp: 70,
conditions: "sunny",
};
};
// Define the tool in an AI SDK format
const getWeather = tool({
description: "Get the current weather in a location",
inputSchema: z.object({
location: z.string().describe("The location to get weather for"),
}),
execute: async ({ location }) => {
// Your custom logic here
const weather = await fetchWeatherAPI(location);
return {
location,
temperature: weather.temp,
conditions: weather.conditions,
};
},
});
async function main() {
console.log(
`\n${chalk.bold("Stagehand 🤘 Computer Use Agent (CUA) Demo")}\n`,
);
// Initialize Stagehand
const stagehand = new Stagehand({
env: "LOCAL",
verbose: 2,
experimental: true, // You must enable experimental mode to use custom tools / MCP integrations
model: "anthropic/claude-sonnet-4-5",
});
await stagehand.init();
try {
const page = stagehand.context.pages()[0];
// Create a computer use agent
const agent = stagehand.agent({
mode: "cua",
model: {
modelName: "anthropic/claude-sonnet-4-5-20250929",
apiKey: process.env.ANTHROPIC_API_KEY,
},
systemPrompt: `You are a helpful assistant that can use a web browser.
You are currently on the following page: ${page.url()}.
Do not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toLocaleDateString()}.`,
tools: {
getWeather, // Pass the tools to the agent
},
});
// Navigate to the Browserbase careers page
await page.goto("https://www.google.com");
// Define the instruction for the CUA
const instruction = "What's the weather in San Francisco?";
console.log(`Instruction: ${chalk.white(instruction)}`);
// Execute the instruction
const result = await agent.execute({
instruction,
maxSteps: 20,
});
console.log(`${chalk.green("✓")} Execution complete`);
console.log(`${chalk.yellow("⤷")} Result:`);
console.log(chalk.white(JSON.stringify(result, null, 2)));
} catch (error) {
console.log(`${chalk.red("✗")} Error: ${error}`);
if (error instanceof Error && error.stack) {
console.log(chalk.dim(error.stack.split("\n").slice(1).join("\n")));
}
} finally {
// Close the browser
await stagehand.close();
}
}
main().catch((error) => {
console.log(`${chalk.red("✗")} Unhandled error in main function`);
console.log(chalk.red(error));
});
Using Tools with Non-CUA Agents
You can also use custom tools with standard (non-CUA) agents:
const agent = stagehand.agent({
systemPrompt: `You are a helpful assistant that can use a web browser.
You are currently on the following page: ${page.url()}.
Do not ask follow up questions, the user will trust your judgement. Today's date is ${new Date().toLocaleDateString()}.`,
// Pass the tools to the agent
tools: {
getWeather: getWeather,
},
});
Creating Multiple Custom Tools
You can provide multiple tools to your agent:
import { z } from "zod";
import { tool } from "ai";
import { Stagehand } from "@stagehand/core";
// Weather tool
const getWeather = tool({
description: "Get the current weather in a location",
inputSchema: z.object({
location: z.string().describe("The location to get weather for"),
}),
execute: async ({ location }) => {
// Your API call here
return { location, temp: 70, conditions: "sunny" };
},
});
// Stock price tool
const getStockPrice = tool({
description: "Get the current stock price for a ticker symbol",
inputSchema: z.object({
ticker: z.string().describe("Stock ticker symbol (e.g., AAPL)"),
}),
execute: async ({ ticker }) => {
// Your API call here
return { ticker, price: 150.25, change: 2.5 };
},
});
// Calculator tool
const calculate = tool({
description: "Perform mathematical calculations",
inputSchema: z.object({
expression: z.string().describe("Mathematical expression to evaluate"),
}),
execute: async ({ expression }) => {
// Your calculation logic
const result = eval(expression); // Use a safe evaluator in production!
return { expression, result };
},
});
async function main() {
const stagehand = new Stagehand({
env: "LOCAL",
verbose: 2,
experimental: true,
});
await stagehand.init();
const page = stagehand.context.pages()[0];
await page.goto("https://www.google.com");
const agent = stagehand.agent({
mode: "cua",
systemPrompt: "You are a helpful assistant with access to weather, stock, and calculator tools.",
tools: {
getWeather,
getStockPrice,
calculate,
},
});
const result = await agent.execute({
instruction: "What's the weather in Seattle, and what's the current price of Microsoft stock?",
maxSteps: 20,
});
console.log("Result:", result);
await stagehand.close();
}
main();
Tool Definition Format
Custom tools follow the AI SDK tool format:
const myTool = tool({
// Description helps the agent know when to use this tool
description: "What this tool does",
// Input schema defines and validates the tool's parameters
inputSchema: z.object({
param1: z.string().describe("Description of param1"),
param2: z.number().optional().describe("Optional parameter"),
}),
// Execute function contains your tool's logic
execute: async ({ param1, param2 }) => {
// Your custom logic here
// Can be API calls, database queries, calculations, etc.
return {
// Return structured data
};
},
});
Key Concepts
Experimental Mode
Custom tools require experimental mode:
const stagehand = new Stagehand({
experimental: true, // Required for custom tools
});
Tool Descriptions
Write clear descriptions so the agent knows when to use each tool:
- Be specific about what the tool does
- Mention what kind of inputs it expects
- Describe what data it returns
Input Schemas
Use Zod schemas to define and validate tool inputs:
- Add
.describe() to each field for context
- Mark optional parameters with
.optional()
- Use appropriate types (string, number, boolean, arrays, objects)
Execute Function
The execute function receives validated inputs and should:
- Perform the tool’s actual work (API calls, calculations, etc.)
- Handle errors gracefully
- Return structured, useful data
Integration with External Services
API Integration Example
const searchNews = tool({
description: "Search for recent news articles on a topic",
inputSchema: z.object({
query: z.string().describe("Search query"),
limit: z.number().optional().describe("Number of results (default 5)"),
}),
execute: async ({ query, limit = 5 }) => {
const response = await fetch(
`https://api.example.com/news?q=${encodeURIComponent(query)}&limit=${limit}`,
{
headers: {
'Authorization': `Bearer ${process.env.NEWS_API_KEY}`,
},
}
);
const data = await response.json();
return data.articles;
},
});
Database Integration Example
import { createClient } from '@supabase/supabase-js';
const queryDatabase = tool({
description: "Query user data from the database",
inputSchema: z.object({
userId: z.string().describe("User ID to query"),
}),
execute: async ({ userId }) => {
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_KEY!
);
const { data, error } = await supabase
.from('users')
.select('*')
.eq('id', userId)
.single();
if (error) throw error;
return data;
},
});
Best Practices
- Clear descriptions - Help the agent understand when to use each tool
- Validate inputs - Use Zod schemas to ensure data integrity
- Handle errors - Gracefully handle API failures and edge cases
- Return structured data - Make tool outputs easy for the agent to understand
- Keep tools focused - Each tool should do one thing well
- Add logging - Log tool calls for debugging and monitoring
Next Steps