Skip to main content
Back to Blog
Guide
2026-06-24

Maestro Mobile UI Testing Guide: YAML Flows for Android & iOS

Learn Maestro mobile testing with this hands-on tutorial. Write YAML flow files, run the CLI on Android and iOS, handle flakiness, and wire Maestro into CI.

Maestro Mobile UI Testing Guide: YAML Flows for Android and iOS

Maestro is the mobile UI testing framework that finally makes end-to-end mobile automation feel approachable. Instead of fighting with Appium servers, WebDriver capabilities, and brittle XPath selectors, you describe what a user does in a plain YAML flow file, and Maestro runs it against an Android emulator, a physical device, or an iOS simulator. The framework was built by the team at mobile.dev specifically to address the two problems that have plagued mobile automation for a decade: setup complexity and flakiness.

This Maestro mobile testing tutorial walks you through everything from installation to advanced patterns. You will learn how Maestro flow YAML works on both Android and iOS, how to run flows from the CLI, how to use selectors that survive UI changes, how to handle conditional logic and loops, and how to plug Maestro into a continuous integration pipeline. Every example is a real, runnable YAML flow or bash command you can paste into your own project.

If you have ever written a Selenium or Appium suite, the contrast will be immediate. A Maestro flow that logs a user in, navigates to a settings screen, and asserts a value is roughly eight lines of declarative YAML. The equivalent Appium test is often a hundred lines of imperative Java or Python with explicit waits scattered throughout. Maestro builds the waiting in: every command automatically retries against the latest view hierarchy until it succeeds or times out, which eliminates the single largest source of flakiness in mobile testing. By the end of this Maestro UI testing tutorial you will be writing Maestro flow YAML for Android and iOS with confidence.

What Is Maestro and Why It Matters

Maestro is an open-source mobile UI testing framework that uses a declarative YAML syntax to describe test flows. A "flow" is a single YAML file that lists a sequence of commands -- launch the app, tap a button, type text, assert that something is visible. Maestro reads the flow, talks to the device through the platform's own automation channels (UIAutomator on Android, XCUITest/XCTest on iOS), and executes each step.

The design philosophy centers on three ideas. First, tolerance to flakiness: every command retries automatically, so a button that takes 800ms to appear after an animation just works without an explicit wait. Second, simplicity: there is no compilation step, no driver server to manage, and no programming language to learn beyond YAML. Third, fast iteration: Maestro Studio and the maestro test continuous mode let you edit a flow and see it re-run instantly.

CapabilityMaestroAppiumEspresso/XCUITest
LanguageYAMLJava/Python/JSKotlin / Swift
Setup timeMinutesHoursModerate
Cross-platform flow reuseYesPartialNo
Built-in auto-retryYesNoPartial
Server/driver requiredNoAppium serverNo
Best forE2E user journeysLegacy/complexWhite-box unit-ish UI

Maestro is not a replacement for Espresso or XCUITest unit-level UI tests that need access to app internals. It is a black-box end-to-end tool: it sees what the user sees. That makes it ideal for smoke tests, critical-path regression, and acceptance testing across both platforms with the same flow file.

Installing Maestro and Setting Up Devices

Maestro ships as a single CLI binary. The install script handles the JVM-based runtime and puts the maestro command on your PATH. On macOS and Linux you run a one-line installer; on Windows you use WSL or the dedicated installer.

# Install Maestro on macOS or Linux
curl -fsSL "https://get.maestro.mobile.dev" | bash

# Verify the install
maestro --version

# On macOS you can also use Homebrew
brew tap mobile-dev-inc/tap
brew install maestro

For Android you need an emulator or a connected device with USB debugging enabled. Maestro discovers devices through ADB, so make sure the Android SDK platform-tools are installed.

# List connected Android devices/emulators
adb devices

# Start an emulator from the command line
emulator -avd Pixel_7_API_34 &

# Confirm Maestro can see it
maestro test --help

For iOS you need Xcode and at least one booted simulator. Maestro drives iOS simulators via Facebook's idb under the hood, which the installer sets up for you.

# List available iOS simulators
xcrun simctl list devices available

# Boot a simulator
xcrun simctl boot "iPhone 15 Pro"
open -a Simulator

Once a device is up, you are ready to write your first flow.

Your First Maestro Flow YAML

A Maestro flow always begins with an appId declaration that tells Maestro which app to launch, followed by three dashes, then the ordered list of commands. The appId is the Android package name (com.example.app) or the iOS bundle identifier (com.example.app). Using the same flow for both platforms is possible when the app IDs match and the UI is consistent.

# login.yaml -- a minimal Maestro flow
appId: com.example.shop
---
- launchApp
- tapOn: "Sign In"
- tapOn:
    id: "email_field"
- inputText: "qa@example.com"
- tapOn:
    id: "password_field"
- inputText: "Sup3rSecret!"
- tapOn: "Log In"
- assertVisible: "Welcome back"

Run it from the CLI:

# Run a single flow against whatever device is connected
maestro test login.yaml

# Run against a specific device
maestro --device emulator-5554 test login.yaml

Notice what is missing: there are no explicit waits, no driver setup, no try/catch around stale elements. When Maestro executes tapOn: "Sign In", it captures the current view hierarchy, searches for a matching element, and if it is not there yet, it waits and retries automatically up to the default timeout. This auto-waiting is the heart of why Maestro flows are so stable compared to hand-written Appium.

Selectors: Finding Elements Reliably

Maestro offers several selector strategies, and choosing the right one is the difference between a durable flow and a flaky one. The most common is matching by visible text, but you can also match by accessibility identifier, by index, by a regular expression, or by relative position to another element.

appId: com.example.shop
---
# Match by visible text (simplest, but localization-sensitive)
- tapOn: "Add to Cart"

# Match by accessibility id (most stable -- prefer this)
- tapOn:
    id: "checkout_button"

# Match by text with a regex
- tapOn:
    text: "Item.*in stock"

# Match the second matching element
- tapOn:
    text: "Delete"
    index: 1

# Match relative to another element
- tapOn:
    text: "Remove"
    below: "Premium Plan"

The strong recommendation is to give your views stable accessibility identifiers in the app code and select by id. Text changes with copy edits and translations; an accessibility id is a contract. The table below summarizes when to reach for each selector type.

SelectorStabilityWhen to use
idHighestDefault choice; requires app-side ids
textMediumQuick scripts, English-only screens
text + regexMediumDynamic strings like prices or counts
indexLowLists where order is guaranteed
relative (below/above)MediumRepeated controls in cards/rows

If you are coming from web automation, the principles mirror the selector discipline in our Playwright end-to-end testing guide: prefer semantic, intent-revealing locators over positional ones.

Assertions, Waiting, and Conditions

Beyond tapping and typing, real flows assert state and sometimes branch on it. Maestro provides assertVisible, assertNotVisible, and conditional execution with runFlow guarded by a when clause.

appId: com.example.shop
---
- launchApp
- assertVisible: "Home"

# Wait for an element to appear before continuing
- extendedWaitUntil:
    visible: "Daily Deals"
    timeout: 10000

# Assert something is NOT on screen
- assertNotVisible: "Error"

# Conditional step: only dismiss the banner if it shows up
- runFlow:
    when:
      visible: "Allow notifications?"
    commands:
      - tapOn: "Don't Allow"

# Assert with an accessibility id
- assertVisible:
    id: "cart_badge"
    text: "3"

The extendedWaitUntil command is your escape hatch for genuinely slow operations like a network fetch on a cold start -- it polls until the element is visible or the timeout elapses. The when guard on runFlow is how you handle non-deterministic dialogs (OS permission prompts, A/B-tested banners) without your whole flow failing when they do not appear.

Reusable Subflows and Parameters

As a suite grows you will repeat the login sequence in dozens of flows. Maestro supports composing flows by calling one from another with runFlow, and passing variables with env. This keeps flows DRY and readable.

# subflows/login.yaml
appId: com.example.shop
---
- tapOn: "Sign In"
- tapOn:
    id: "email_field"
- inputText: ${EMAIL}
- tapOn:
    id: "password_field"
- inputText: ${PASSWORD}
- tapOn: "Log In"
- assertVisible: "Welcome back"
# checkout.yaml -- composes the login subflow
appId: com.example.shop
env:
  EMAIL: "qa@example.com"
  PASSWORD: "Sup3rSecret!"
---
- launchApp
- runFlow: subflows/login.yaml
- tapOn: "Add to Cart"
- tapOn: "Checkout"
- assertVisible: "Order Confirmed"

Variables interpolate with ${VAR} syntax and can come from the flow's env block, the CLI, or the environment. To override at runtime:

# Pass variables from the CLI
maestro test -e EMAIL=other@example.com -e PASSWORD=Hunter2 checkout.yaml

This is how you parameterize the same flow across staging credentials, different test accounts, or locale-specific data without duplicating YAML.

Handling Scroll, Swipe, and Gestures

Mobile UIs depend on gestures that web tests rarely need. Maestro exposes a full gesture vocabulary: scroll, scrollUntilVisible, swipe, longPress, and doubleTapOn. The most useful in practice is scrollUntilVisible, which scrolls a container until a target element comes into view -- essential for long lists and onboarding carousels.

appId: com.example.shop
---
- launchApp

# Simple scroll down
- scroll

# Scroll until a specific element is on screen, then tap it
- scrollUntilVisible:
    element:
      text: "Privacy Policy"
    direction: DOWN
    timeout: 20000
- tapOn: "Privacy Policy"

# Swipe a carousel left
- swipe:
    direction: LEFT

# Long press to open a context menu
- longPress: "Saved Item"

For pull-to-refresh and other custom gestures you can swipe between explicit coordinates or use the start/end point form. Because gestures also benefit from auto-retry, a scrollUntilVisible will keep scrolling and re-checking rather than failing the first time the target is off-screen.

Running Maestro from the CLI

The maestro CLI is the command center. Beyond maestro test, the most valuable subcommands are maestro studio for interactive authoring and maestro test --continuous for watch-mode development. You can run a whole directory of flows, generate reports, and target specific devices.

# Run every flow in a directory (alphabetical order)
maestro test flows/

# Run with a JUnit XML report for CI
maestro test --format junit --output report.xml flows/

# Continuous mode: re-runs on file save -- great while authoring
maestro test --continuous login.yaml

# Launch Maestro Studio to inspect the view hierarchy and build selectors
maestro studio

# Record a video of the run
maestro record login.yaml

Maestro Studio deserves special mention. It opens a browser-based inspector showing the live device, the full view hierarchy, and an interactive command builder. You click an element and Studio writes the exact tapOn selector for you, which removes the guesswork of finding the right id. It is the fastest way to author selectors that actually match. The same iterate-fast mindset applies across the mobile testing space -- see our broader mobile testing automation guide for how this fits a complete strategy.

Running Maestro in CI/CD

Maestro shines in continuous integration because a flow is just a file and the CLI exits non-zero on failure. The two common patterns are running against an emulator inside the CI runner, or offloading to Maestro Cloud (the hosted device farm) for parallel real-device runs.

Here is a GitHub Actions workflow that boots an Android emulator and runs the suite:

# .github/workflows/maestro.yml
name: Maestro E2E
on: [push, pull_request]
jobs:
  android:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Maestro
        run: |
          curl -fsSL "https://get.maestro.mobile.dev" | bash
          echo "$HOME/.maestro/bin" >> $GITHUB_PATH

      - name: Run on Android emulator
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 34
          target: google_apis
          arch: x86_64
          script: |
            adb install app-debug.apk
            maestro test --format junit --output report.xml flows/

      - name: Upload report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: maestro-report
          path: report.xml

To run on the hosted device cloud instead, you upload the build and the flows and let the cloud parallelize across real devices:

# Run flows on Maestro Cloud across real devices
maestro cloud \
  --apiKey "$MAESTRO_CLOUD_API_KEY" \
  app-release.apk \
  flows/

The cloud path gives you a matrix of real Android and iOS hardware without maintaining your own device lab, and it parallelizes flows so a fifty-flow suite finishes in minutes. For teams that already manage their own pipelines, the emulator approach keeps everything in-house.

Debugging Flaky and Failing Flows

Even with auto-retry, flows fail -- usually because of a selector that matches the wrong element, a network-dependent screen, or a timing assumption. Maestro gives you several debugging levers. The --debug-output flag dumps the view hierarchy and screenshots for every step, which is the fastest way to see what Maestro actually saw when a step failed.

# Dump screenshots + hierarchy for every step into ./debug
maestro test --debug-output ./debug login.yaml

# Inspect the live hierarchy interactively
maestro studio

A few hard-won debugging rules. If a tapOn hits the wrong element, your text matched something unexpected -- switch to an id selector. If a step times out, the element genuinely was not there: add an extendedWaitUntil for slow loads, or a when-guarded runFlow if the element is optional. If a flow passes locally but fails in CI, the emulator is usually slower or a different locale -- pin the emulator config and avoid text selectors. These flakiness patterns echo the framework-agnostic advice in our guide to fixing flaky tests; the root causes are the same on mobile and web.

Best Practices for Maintainable Maestro Suites

A Maestro suite stays healthy when you treat flows like production code. Organize flows by feature, factor common sequences into subflows, and prefer accessibility ids everywhere. Keep each flow focused on one user journey so a failure points at one feature, not a sprawling mega-flow that is impossible to diagnose.

PracticeWhy it matters
Select by id, not textSurvives copy and translation changes
One journey per flowFailures localize to one feature
Extract login/setup to subflowsDRY, single place to update
Parameterize with envReuse across accounts and locales
Guard optional dialogs with whenPermission prompts do not break runs
Run in CI on every PRCatch regressions before merge
Pin emulator/simulator versionsReproducible runs across machines

Add tags to flows so you can run subsets -- a fast smoke tag on every PR and a full regression tag nightly. Tags live in the flow's config block and are selected with --include-tags:

appId: com.example.shop
tags:
  - smoke
  - checkout
---
- launchApp
- assertVisible: "Home"
# Run only smoke-tagged flows on every PR
maestro test --include-tags smoke flows/

Frequently Asked Questions

Is Maestro better than Appium for mobile testing?

For end-to-end black-box testing, Maestro is dramatically simpler and more stable than Appium. It requires no driver server, uses declarative YAML, and retries every command automatically. Appium remains stronger for deeply customized interactions, older platforms, and teams that need to script in a full programming language. For most user-journey and smoke testing, Maestro wins on setup time and flakiness resistance.

Can the same Maestro flow run on both Android and iOS?

Yes, when the app's bundle id matches across platforms and the UI is consistent, a single Maestro flow YAML file runs unchanged on both Android and iOS. Selectors based on visible text or accessibility ids work cross-platform. You only need platform-specific flows when screens diverge significantly or when handling OS-level dialogs that differ between Android and iOS.

How does Maestro handle flaky tests automatically?

Every Maestro command captures the current view hierarchy, searches for the target element, and if it is not yet present, waits and retries until a timeout. This built-in auto-waiting eliminates the explicit sleep and wait calls that cause most flakiness in Appium and Selenium. You rarely write manual waits; for genuinely slow screens you add extendedWaitUntil with a longer timeout.

Do I need to know how to code to use Maestro?

No. Maestro flows are plain YAML files listing commands like tapOn, inputText, and assertVisible. There is no compilation, no programming language, and no driver setup. Maestro Studio even builds selectors for you visually. Engineers, QA analysts, and even product managers can author and read flows, which is a major reason teams adopt it over code-heavy frameworks.

How do I run Maestro tests in a CI pipeline?

Install the Maestro CLI in your CI runner, boot an emulator or simulator, install the app build, then run maestro test --format junit --output report.xml flows/. The CLI exits non-zero on failure so the job fails correctly. For parallel real-device runs, use maestro cloud with an API key to offload execution to the hosted device farm.

What is Maestro Studio and when should I use it?

Maestro Studio is a browser-based interactive inspector that shows the live device, the full view hierarchy, and an element picker. When you click an element, it generates the exact tapOn selector. Use it while authoring flows to find stable selectors quickly and to debug why a step matched the wrong element. It is the fastest way to build correct, durable selectors.

Can Maestro test React Native and Flutter apps?

Yes. Because Maestro drives the OS-level view hierarchy through UIAutomator and XCUITest, it works with React Native, Flutter, native, and hybrid apps. For Flutter, ensure widgets expose semantic labels or accessibility identifiers so selectors can find them. React Native components map cleanly to native views, so text and accessibility-id selectors generally work without extra configuration.

Conclusion

Maestro has changed what mobile UI testing feels like. By collapsing the setup, the driver management, and the manual waiting into a single CLI and a declarative YAML format, it lets teams write durable end-to-end tests for Android and iOS in minutes instead of days. The auto-retry model attacks flakiness at the root, the selector system rewards good app-side accessibility, and the CLI plus Maestro Studio make authoring and debugging genuinely fast. Whether you run flows against a local emulator in GitHub Actions or fan them out across the hosted device cloud, the same simple flow file is your single source of truth.

Start small: write one login flow, run it in continuous mode, and watch how rarely it flakes. Then factor it into a subflow, parameterize the credentials, and grow a smoke suite you trust on every pull request. To go deeper on mobile strategy, framework selection, and AI-assisted test authoring, explore the curated QA skills and ready-to-use testing playbooks at QASkills.sh and equip your AI coding agents with battle-tested mobile testing know-how.

Maestro Mobile UI Testing Guide: YAML Flows for Android & iOS | QASkills.sh