Skip to main content

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:
  1. Extracts the current game state
  2. Analyzes the board for the best move
  3. Executes the move
  4. 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

  1. Plan your workflow - Map out all steps before coding
  2. Extract incrementally - Get data at each step to inform next actions
  3. Add logging - Console logs help debug complex workflows
  4. Handle edge cases - Plan for errors and unexpected states
  5. Use schemas - Zod schemas ensure data consistency across steps
  6. 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

Build docs developers (and LLMs) love