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:
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:
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:
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:
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:
// Store screens during automationawait agent.utils.outOfSteps.storeScreen( screen, currentStage, // Which stage screenState, // What screen state maxSteps, // Remaining steps ScreenshotRecord.LOW_QUALITY // Screenshot quality);
// Submit when out of stepsif (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:
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:
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
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.