Maestro Flow Development
Use when writing Maestro YAML test flows, understanding flow syntax, or needing command reference for mobile app automation.
Flow Structure
Every Maestro flow is a YAML file defining automated interactions with a mobile app. Structure flows with three essential components: app identifier, environment setup, and command sequence.
Basic Flow Anatomy
appId: com.example.app --- - launchApp - tapOn: "Login" - inputText: "user@example.com" - tapOn: "Submit" - assertVisible: "Welcome"
The appId header specifies the bundle identifier (iOS) or package name (Android). The triple dash --- separates metadata from commands. Commands execute sequentially, failing fast on errors.
Core Commands
Navigation Commands
launchApp - Launch the target application with optional arguments:
- launchApp:
appId: com.example.app # Override default appId
clearState: true # Clear app data before launch
clearKeychain: true # Clear iOS keychain (iOS only)
permissions:
all: allow # Grant all permissions
Use clearState for test isolation between runs. Grant permissions upfront to avoid runtime dialogs.
back - Navigate back (Android back button or iOS navigation):
- back
swipe - Directional swipe gestures:
- swipe:
direction: UP # UP, DOWN, LEFT, RIGHT
duration: 500 # Milliseconds
- swipe:
start: 50%, 80% # Start coordinates (x%, y%)
end: 50%, 20% # End coordinates
scroll - Scroll until element visible:
- scroll
- scrollUntilVisible:
element:
text: "Settings"
direction: DOWN
timeout: 10000
Interaction Commands
tapOn - Tap elements by various selectors:
# By text content
- tapOn: "Submit"
# By resource ID
- tapOn:
id: "login_button"
# By accessibility label
- tapOn:
accessibilityLabel: "Login Button"
# By index (when multiple matches)
- tapOn:
text: "Edit"
index: 0
# Long press
- tapOn:
id: "item"
longPress: true
# Tap coordinates
- tapOn:
point: "50%,80%"
Always prefer stable selectors (id, accessibilityLabel) over fragile ones (text, coordinates). Use index only when necessary.
inputText - Enter text into focused input:
- tapOn:
id: "email_field"
- inputText: "user@example.com"
# Input with variables
- inputText: "${EMAIL}"
# Input with special keys
- inputText: "password123\n" # \n sends Enter
Tap the input field before inputText. The command types into whatever has focus.
eraseText - Clear text from input:
- eraseText:
charactersToErase: 10 # Delete N characters
- eraseText # Clear entire field
Assertion Commands
assertVisible - Verify element exists and is visible:
- assertVisible: "Login Successful"
- assertVisible:
id: "success_message"
timeout: 5000
# Assert NOT visible
- assertNotVisible:
text: "Error"
Assertions fail the flow immediately if condition not met. Use timeout for elements appearing after async operations.
assertTrue - Evaluate conditions:
- assertTrue: ${USER_LOGGED_IN}
- assertTrue:
condition: "${RESPONSE_CODE} == 200"
timeout: 3000
Useful for validating state after complex interactions.
Wait Commands
waitForAnimationToEnd - Wait for UI animations:
- waitForAnimationToEnd:
timeout: 3000
Critical after transitions, before assertions on new screens.
extendedWaitUntil - Wait for conditions:
- extendedWaitUntil:
visible: "Loading complete"
timeout: 30000
- extendedWaitUntil:
notVisible: "Spinner"
Use for loading states, async data fetching.
Element Selectors
Maestro supports multiple selector strategies. Choose based on stability and reliability.
Selector Priority
- •Resource ID (most stable):
- tapOn:
id: "login_button"
- •Accessibility Label (stable, accessibility-friendly):
- tapOn:
accessibilityLabel: "Login Button"
- •Text Content (fragile, localization issues):
- tapOn: "Submit"
- tapOn:
text: "Submit"
- •Coordinates (most fragile, device-dependent):
- tapOn:
point: "50%,80%"
Selector Modifiers
index - Select from multiple matches:
- tapOn:
text: "Edit"
index: 1 # Zero-indexed
below/above/leftOf/rightOf - Relative positioning:
- tapOn:
text: "Delete"
below:
text: "John Doe"
enabled - Filter by enabled state:
- tapOn:
text: "Submit"
enabled: true
Variables and Environment
Define reusable values and environment-specific configuration.
Inline Variables
- inputText: "user@example.com"
- evalScript: ${output.email = "user@example.com"}
- assertVisible: "${output.email}"
Environment Files
Create .env file:
EMAIL=test@example.com PASSWORD=password123 API_URL=https://staging.api.example.com
Reference in flows:
- inputText: "${EMAIL}"
- inputText: "${PASSWORD}"
Run with environment:
maestro test --env=staging flow.yaml
Built-in Variables
# Device info
- assertVisible: "${maestro.deviceModel}"
- assertTrue: "${maestro.platform == 'iOS'}"
# Random data
- inputText: "${maestro.randomEmail}"
- inputText: "${maestro.randomNumber}"
# Output from previous commands
- copyTextFrom:
id: "confirmation_code"
- inputText: "${output.text}"
Control Flow
Conditional Execution
runFlow - Execute subflows conditionally:
- runFlow:
when:
visible: "Login Required"
file: login.yaml
Modularize common sequences (login, setup) into reusable flows.
repeat - Loop commands:
- repeat:
times: 3
commands:
- tapOn: "Next"
- waitForAnimationToEnd
Error Handling
retry - Retry commands on failure:
- retry:
maxAttempts: 3
commands:
- tapOn: "Sync"
- assertVisible: "Sync Complete"
optional - Continue on failure:
- tapOn:
text: "Close Tutorial"
optional: true
Use optional for dismissing inconsistent dialogs (permissions, tips).
Advanced Commands
JavaScript Evaluation
evalScript - Execute JavaScript:
- evalScript: |
var date = new Date();
output.timestamp = date.getTime();
- inputText: "${output.timestamp}"
Access full JavaScript runtime for calculations, data generation.
HTTP Requests
httpRequest - Make API calls:
- httpRequest:
url: "${API_URL}/users"
method: GET
headers:
Authorization: "Bearer ${TOKEN}"
timeout: 5000
- assertTrue: "${output.response.statusCode == 200}"
- evalScript: output.userId = json(output.response.body).id
Setup test data via API before UI interactions.
Screenshots and Recording
takeScreenshot - Capture screen:
- takeScreenshot: screenshots/login-success.png
startRecording / stopRecording:
- startRecording: recordings/test-flow - launchApp # ... test steps ... - stopRecording
Best Practices
Stability Patterns
Wait After Actions - Always wait after navigation:
- tapOn: "Next Screen" - waitForAnimationToEnd - assertVisible: "Screen Title"
Explicit Waits Over Sleeps - Use condition-based waits:
# Bad
- tapOn: "Load Data"
- sleep: 3000
# Good
- tapOn: "Load Data"
- extendedWaitUntil:
visible: "Data Loaded"
timeout: 5000
Idempotent Flows - Clear state between runs:
- launchApp:
clearState: true
permissions:
all: allow
Selector Resilience
Avoid Brittle Text - Text changes with localization:
# Fragile
- tapOn: "Submit"
# Resilient
- tapOn:
id: "submit_button"
Handle Dynamic Content - Use relative selectors:
- tapOn:
text: "Delete"
below:
id: "user_${USER_ID}"
Flow Organization
Modular Subflows - Extract reusable sequences:
login.yaml:
appId: com.example.app
---
- tapOn:
id: "email_field"
- inputText: "${EMAIL}"
- tapOn:
id: "password_field"
- inputText: "${PASSWORD}"
- tapOn:
id: "login_button"
- assertVisible: "Welcome"
Main flow:
- runFlow: login.yaml - tapOn: "Settings"
Descriptive Comments - Document complex interactions:
# Dismiss onboarding if shown
- tapOn:
text: "Skip"
optional: true
# Wait for background sync
- extendedWaitUntil:
visible: "Sync Complete"
timeout: 30000
Error Recovery
Graceful Degradation - Handle optional elements:
# Close promotional popup if present
- tapOn:
id: "promo_close"
optional: true
# Proceed with main flow
- tapOn: "Dashboard"
Retry Critical Paths - Retry flaky operations:
- retry:
maxAttempts: 3
commands:
- tapOn: "Refresh"
- waitForAnimationToEnd
- assertVisible: "Updated Data"
Performance Optimization
Minimize Waits - Use shortest safe timeouts:
# Default timeout often too long
- assertVisible:
id: "button"
timeout: 2000 # Override default
Parallel Environment Setup - Setup via API, not UI:
# Setup test data via API
- httpRequest:
url: "${API_URL}/seed"
method: POST
# Launch app with prepared state
- launchApp
Reuse App State - Skip repeated setup:
# Keep app state between flows
- launchApp:
clearState: false
Platform Differences
iOS Specifics
# Clear keychain (iOS only)
- launchApp:
clearKeychain: true
# iOS accessibility labels
- tapOn:
accessibilityLabel: "login_button"
Android Specifics
# Android resource IDs
- tapOn:
id: "com.example.app:id/login_button"
# Android back navigation
- back
Debugging Flows
Verbose Output - Run with debug logging:
maestro test --debug flow.yaml
Screenshot Checkpoints - Capture state at key points:
- takeScreenshot: debug/before-login.png - tapOn: "Login" - takeScreenshot: debug/after-login.png
Pause for Inspection - Add delays during development:
- sleep: 5000 # Time to inspect app state
Remove sleeps before committing flows.
Reference
For complete command syntax and advanced features, see:
- •
references/commands.md- Full command reference - •
examples/login-flow.yaml- Login flow example - •
examples/e2e-test.yaml- End-to-end test example
Master these patterns to create reliable, maintainable mobile test automation with Maestro.