Automation API

Android device automation

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
TypeScript
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
TypeScript
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
TypeScript
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
TypeScript
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
TypeScript
// 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
TypeScript
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
TypeScript
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
TypeScript
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.

Next Steps