Documentation Index
Fetch the complete documentation index at: https://mintlify.com/browserbase/stagehand/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Stagehand excels at complex, multi-step automation tasks. This example shows how to build an AI agent that plays the 2048 game by analyzing the board state and making strategic decisions.
2048 Game Bot Example
This example demonstrates a complete multi-step automation that:
- Extracts the current game state
- Analyzes the board for the best move
- Executes the move
- Repeats until game over
import { Stagehand } from "@stagehand/core";
import { z } from "zod";
async function example() {
console.log("๐ฎ Starting 2048 bot...");
const stagehand = new Stagehand({
env: "LOCAL",
verbose: 1,
});
console.log("๐ Initializing Stagehand...");
await stagehand.init();
const page = stagehand.context.pages()[0];
try {
console.log("๐ Navigating to 2048...");
await page.goto("https://ovolve.github.io/2048-AI/");
// Main game loop
while (true) {
console.log("๐ Game loop iteration...");
// Add a small delay for UI updates
await new Promise((resolve) => setTimeout(resolve, 300));
// Get current game state
const gameState = await stagehand.extract(
`Extract the current game state:
1. Score from the score counter
2. All tile values in the 4x4 grid (empty spaces as 0)
3. Highest tile value present`,
z.object({
score: z.number(),
highestTile: z.number(),
grid: z.array(z.array(z.number())),
}),
);
const transposedGrid = gameState.grid[0].map((_, colIndex) =>
gameState.grid.map((row) => row[colIndex]),
);
const grid = transposedGrid.map((row, rowIndex) => ({
[`row${rowIndex + 1}`]: row,
}));
console.log("Game State:", {
score: gameState.score,
highestTile: gameState.highestTile,
grid: grid,
});
// Analyze board and decide next move
const analysis = await stagehand.extract(
`Based on the current game state:
- Score: ${gameState.score}
- Highest tile: ${gameState.highestTile}
- Grid: This is a 4x4 matrix ordered by row (top to bottom) and column (left to right). The rows are stacked vertically, and tiles can move vertically between rows or horizontally between columns:
${grid
.map((row) => {
const rowName = Object.keys(row)[0];
return ` ${rowName}: ${row[rowName].join(", ")}`;
})
.join("\n")}
What is the best move (up/down/left/right)? Consider:
1. Keeping high value tiles in corners (bottom left, bottom right, top left, top right)
2. Maintaining a clear path to merge tiles
3. Avoiding moves that could block merges
4. Only adjacent tiles of the same value can merge
5. Making a move will move all tiles in that direction until they hit a tile of a different value or the edge of the board
6. Tiles cannot move past the edge of the board
7. Each move must move at least one tile`,
z.object({
move: z.enum(["up", "down", "left", "right"]),
confidence: z.number(),
reasoning: z.string(),
}),
);
console.log("Move Analysis:", analysis);
const moveKey = {
up: "ArrowUp",
down: "ArrowDown",
left: "ArrowLeft",
right: "ArrowRight",
}[analysis.move];
await page.keyPress(moveKey);
console.log("๐ฏ Executed move:", analysis.move);
}
} catch (error) {
console.error("โ Error in game loop:", error);
const isGameOver = await page.evaluate(() => {
return document.querySelector(".game-over") !== null;
});
if (isGameOver) {
console.log("๐ Game Over!");
return;
}
throw error; // Re-throw non-game-over errors
}
}
(async () => {
await example();
})();
Apartment Search Workflow
Hereโs another complex multi-step example that navigates through filters on an apartment search site:
import { Action, Stagehand } from "@stagehand/core";
async function apartmentSearch() {
const stagehand = new Stagehand({
env: "BROWSERBASE",
verbose: 1,
});
await stagehand.init();
const page = stagehand.context.pages()[0];
await page.goto("https://www.apartments.com/san-francisco-ca/");
let observation: Action;
// Step 1: Open filters
await new Promise((resolve) => setTimeout(resolve, 3000));
[observation] = await stagehand.observe("find the 'all filters' button");
await stagehand.act(observation);
// Step 2: Set bedroom requirement
await new Promise((resolve) => setTimeout(resolve, 3000));
[observation] = await stagehand.observe(
"find the '1+' button in the 'beds' section",
);
await stagehand.act(observation);
// Step 3: Set home type
await new Promise((resolve) => setTimeout(resolve, 3000));
[observation] = await stagehand.observe(
"find the 'apartments' button in the 'home type' section",
);
await stagehand.act(observation);
// Step 4: Open pet policy dropdown
await new Promise((resolve) => setTimeout(resolve, 3000));
[observation] = await stagehand.observe(
"find the pet policy dropdown to click on.",
);
await stagehand.act(observation);
// Step 5: Select dog friendly
await new Promise((resolve) => setTimeout(resolve, 3000));
[observation] = await stagehand.observe(
"find the 'Dog Friendly' option to click on",
);
await stagehand.act(observation);
// Step 6: View results
await new Promise((resolve) => setTimeout(resolve, 3000));
[observation] = await stagehand.observe("find the 'see results' section");
await stagehand.act(observation);
const currentUrl = page.url();
await stagehand.close();
if (
currentUrl.includes(
"https://www.apartments.com/apartments/san-francisco-ca/min-1-bedrooms-pet-friendly-dog/",
)
) {
console.log("โ
Success! we made it to the correct page");
} else {
console.log("โ Whoops, looks like we didn't make it to the correct page.");
}
}
(async () => {
await apartmentSearch();
})();
Key Concepts for Multi-Step Workflows
State Management
Keep track of your automationโs state:
- Extract data at each step
- Store intermediate results
- Use state to make decisions
Error Handling
Robust error handling is crucial:
try {
// Your automation steps
} catch (error) {
console.error("Error:", error);
// Check if it's an expected condition
const isGameOver = await page.evaluate(() => {
return document.querySelector(".game-over") !== null;
});
if (isGameOver) {
console.log("Game Over!");
return;
}
throw error; // Re-throw unexpected errors
}
Loops and Conditionals
Use standard JavaScript control flow:
while loops for continuous automation
if/else for conditional logic
for loops for iterating over elements
Timing and Synchronization
Add delays between steps to ensure UI stability:
await new Promise((resolve) => setTimeout(resolve, 3000));
Best Practices
- Plan your workflow - Map out all steps before coding
- Extract incrementally - Get data at each step to inform next actions
- Add logging - Console logs help debug complex workflows
- Handle edge cases - Plan for errors and unexpected states
- Use schemas - Zod schemas ensure data consistency across steps
- Test incrementally - Verify each step works before adding more
Design Patterns
State Machine Pattern
enum GameState {
NAVIGATING,
ANALYZING,
ACTING,
FINISHED
}
let state = GameState.NAVIGATING;
while (state !== GameState.FINISHED) {
switch (state) {
case GameState.NAVIGATING:
// Navigate to page
state = GameState.ANALYZING;
break;
case GameState.ANALYZING:
// Extract and analyze data
state = GameState.ACTING;
break;
case GameState.ACTING:
// Perform action
state = GameState.ANALYZING; // or FINISHED
break;
}
}
Pipeline Pattern
async function step1() {
const data = await stagehand.extract("get data");
return data;
}
async function step2(input: any) {
await stagehand.act(`process ${input}`);
return "result";
}
async function step3(input: any) {
// Final step
}
// Execute pipeline
const result1 = await step1();
const result2 = await step2(result1);
await step3(result2);
Next Steps