Skip to content

Stages

Organize automation into phases with step limits.

Stages help organize complex automations into logical phases. Each stage has its own step limit, making it easier to debug issues and track progress.

The Stage Pattern

Define your automation stages as an enum:

stages.ts
export enum Stage {
  Initialize = "Initialize",
  LaunchApp = "LaunchApp",
  HandleLogin = "HandleLogin",
  NavigateToMessages = "NavigateToMessages",
  SelectUnreadChat = "SelectUnreadChat",
  ProcessMessages = "ProcessMessages",
  NavigateToFeed = "NavigateToFeed",
  LikePosts = "LikePosts",
  Complete = "Complete",
}

Stage State Management

Track the current stage and step count:

Stage tracking
import { Stage } from "./stages.js";

const MAX_STEPS_PER_STAGE = 48;

let currentStage: Stage = Stage.Initialize;
let maxSteps = MAX_STEPS_PER_STAGE;

async function setCurrentStage(newStage: Stage) {
  currentStage = newStage;
  maxSteps = MAX_STEPS_PER_STAGE; // Reset step counter

  console.log(`[Stage] Entering: ${newStage}`);

  // Report progress to server (files ignored when finish=false)
  await agent.utils.job.submitTask(
    "running",
    { stage: newStage, timestamp: Date.now() },
    false  // Don't finish the task
  );
}

Max Steps Per Stage

Step limits prevent infinite loops and help identify stuck automations:

Main loop with step counting
async function runAutomation() {
  await setCurrentStage(Stage.Initialize);

  // Main automation loop
  while (maxSteps-- > 0) {
    const { state, screen } = await getCurrentScreenState();

    // Store screen for debugging
    await agent.utils.outOfSteps.storeScreen(
      screen,
      currentStage,
      state,
      maxSteps,
      state === ScreenState.Unknown
        ? ScreenshotRecord.HIGH_QUALITY
        : ScreenshotRecord.LOW_QUALITY
    );

    // Handle the current screen state
    await handleScreenState(state, screen);

    // Check if we've completed
    if (currentStage === Stage.Complete) {
      break;
    }
  }

  // Out of steps - submit for analysis
  if (maxSteps < 0) {
    await agent.utils.outOfSteps.submit("outOfSteps");
    throw new Error("OUT_OF_STEPS");
  }
}

Stage Transitions

Transition between stages based on progress:

Stage handler with transitions
async function handleScreenState(
  state: ScreenState,
  screen: AndroidNode
) {
  // Handle system states first (any stage)
  if (await handleSystemStates(state, screen)) {
    return;
  }

  // Stage-specific handling
  switch (currentStage) {
    case Stage.Initialize:
      await handleInitialize(state, screen);
      break;

    case Stage.LaunchApp:
      await handleLaunchApp(state, screen);
      break;

    case Stage.HandleLogin:
      await handleLogin(state, screen);
      break;

    case Stage.NavigateToMessages:
      await handleNavigateToMessages(state, screen);
      break;

    // ... other stages
  }
}

async function handleInitialize(state: ScreenState, screen: AndroidNode) {
  // Check if app is installed
  const apps = await agent.actions.listApps();
  if (!apps[APP_PACKAGE]) {
    throw new Error("App not installed");
  }

  // Set up callbacks
  agent.utils.setNetworkCallback(handleNetworkChange);

  // Move to next stage
  await setCurrentStage(Stage.LaunchApp);
}

async function handleLaunchApp(state: ScreenState, screen: AndroidNode) {
  if (state === ScreenState.HomeScreen) {
    // Already on home screen, skip to messages
    await setCurrentStage(Stage.NavigateToMessages);
    return;
  }

  if (state === ScreenState.SplashScreen || state === ScreenState.Loading) {
    // Wait for app to load
    await sleep(2000);
    return;
  }

  if (state === ScreenState.LoginScreen) {
    // Need to login first
    await setCurrentStage(Stage.HandleLogin);
    return;
  }

  // Launch the app
  await agent.actions.launchApp(APP_PACKAGE);
  await sleep(3000);
}

Out of Steps Handling

When the automation runs out of steps, submit data for analysis:

Out of steps submission
// Store screens during automation
await agent.utils.outOfSteps.storeScreen(
  screen,
  currentStage,           // Which stage
  screenState,            // What screen state
  maxSteps,               // Remaining steps
  ScreenshotRecord.LOW_QUALITY  // Screenshot quality
);

// Submit when out of steps
if (maxSteps < 0) {
  const result = await agent.utils.outOfSteps.submit("outOfSteps");

  if (result.success) {
    console.log("Out of steps report ID:", result.id);
  }

  // Fail the task
  await agent.utils.job.submitTask("failed", {
    reason: "OUT_OF_STEPS",
    stage: currentStage,
    outOfStepsId: result.success ? result.id : null
  });

  // Always stop the automation
  stopCurrentAutomation();
}

Dynamic Step Limits

Some stages may need more steps than others:

Dynamic step limits
function getMaxStepsForStage(stage: Stage): number {
  switch (stage) {
    // Login might need more steps (captchas, 2FA, etc.)
    case Stage.HandleLogin:
      return 100;

    // Message processing depends on conversation length
    case Stage.ProcessMessages:
      return 60;

    // Most stages need fewer steps
    default:
      return 48;
  }
}

async function setCurrentStage(newStage: Stage) {
  currentStage = newStage;
  maxSteps = getMaxStepsForStage(newStage);

  console.log(`[Stage] ${newStage} (max steps: ${maxSteps})`);

  await agent.utils.job.submitTask(
    "running",
    { stage: newStage },
    false
  );
}

Same Screen Detection

Detect when stuck on the same screen:

Same screen detection
let lastScreenState: ScreenState | null = null;
let sameScreenCount = 0;
const MAX_SAME_SCREEN = 5;

async function checkSameScreen(state: ScreenState) {
  if (state === lastScreenState) {
    sameScreenCount++;

    if (sameScreenCount >= MAX_SAME_SCREEN) {
      console.warn(`Stuck on ${state} for ${sameScreenCount} iterations`);

      // Try recovery actions
      if (state === ScreenState.Loading) {
        // Wait longer for loading
        await sleep(5000);
      } else {
        // Try going back
        await agent.actions.goBack();
        await sleep(1000);
      }

      // Reset counter after recovery attempt
      sameScreenCount = 0;
    }
  } else {
    lastScreenState = state;
    sameScreenCount = 0;
  }
}

Complete Example

Full stage management
import { Stage } from "./stages.js";
import { ScreenState } from "./screenStates.js";
import { detectScreenState } from "./detection.js";

const MAX_STEPS_PER_STAGE = 48;

let currentStage = Stage.Initialize;
let maxSteps = MAX_STEPS_PER_STAGE;
let lastScreenState: ScreenState | null = null;
let sameScreenCount = 0;

async function setCurrentStage(newStage: Stage) {
  currentStage = newStage;
  maxSteps = MAX_STEPS_PER_STAGE;
  lastScreenState = null;
  sameScreenCount = 0;

  console.log(`=== Stage: ${newStage} ===`);

  await agent.utils.job.submitTask(
    "running",
    await collectData(),
    false
  );
}

async function collectData() {
  return {
    stage: currentStage,
    timestamp: new Date().toISOString(),
    // Add other data you want to track
  };
}

async function main() {
  try {
    await setCurrentStage(Stage.Initialize);

    while (maxSteps-- > 0) {
      // Get current screen
      const screen = await agent.actions.screenContent();
      const state = detectScreenState(screen);

      // Store for out-of-steps analysis
      await agent.utils.outOfSteps.storeScreen(
        screen,
        currentStage,
        state,
        maxSteps,
        state === ScreenState.Unknown
          ? ScreenshotRecord.HIGH_QUALITY
          : ScreenshotRecord.LOW_QUALITY
      );

      // Check for stuck state
      await checkSameScreen(state);

      // Handle the state
      await handleScreenState(state, screen);

      // Check completion
      if (currentStage === Stage.Complete) {
        await agent.utils.job.submitTask("success", await collectData());
        return;
      }

      // Small delay between iterations
      await sleep(500, 1000);
    }

    // Out of steps
    await agent.utils.outOfSteps.submit("outOfSteps");
    throw new Error("OUT_OF_STEPS");

  } catch (error) {
    console.error("Automation failed:", error);

    await agent.utils.job.submitTask("failed", {
      ...await collectData(),
      error: String(error)
    });

  } finally {
    // Always stop the automation
    stopCurrentAutomation();
  }
}

main();

Best Practices

Reset Step Counter on Stage Change

Always reset maxSteps when entering a new stage. This gives each phase its own budget of steps.

Report Progress on Stage Change

Call submitTask("running", ...) when changing stages. This lets you track progress in the dashboard.

Store Screens Continuously

Call storeScreen() on every iteration. This creates a trail for debugging when things go wrong.

Detect Stuck States

Track same-screen counts and implement recovery logic. Don't let the automation spin on the same screen forever.