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 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:
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.