# Maestro documentation

Overview of Maestro documentation with links to guides, tutorials, and community resources.

Maestro is the simplest and most effective framework for painless mobile and web UI automation using intuitive YAML flows.

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCbCMt5C3rawmE9oIus7f%2Fuploads%2F7kWqvBIcSqYfGW86X7cZ%2FGoogle%20Maps%20UI%20Test%20Automation%20with%20Maestro%20%E2%80%94%20Full%20YAML%20Tutorial.mp4?alt=media&token=1fbdc79b-cc4f-4603-a0d5-1035d9dc2bc1>" %}

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td>What is Maestro?</td><td>Learn what Maestro can do for you when testing applications</td><td><a href="/pages/yd3YLUukE5M8wZT1CE5D">/pages/yd3YLUukE5M8wZT1CE5D</a></td><td><a href="/files/T2fcJCUlI4DlCv5wqrZ4">/files/T2fcJCUlI4DlCv5wqrZ4</a></td></tr><tr><td>QuickStart</td><td>Get up and running with Maestro in just a few minutes</td><td><a href="/pages/IyVjUYlja4evcymb2uG3">/pages/IyVjUYlja4evcymb2uG3</a></td><td><a href="/files/oWLVxtnJWEbbqglV5J6e">/files/oWLVxtnJWEbbqglV5J6e</a></td></tr></tbody></table>

### Maestro Solutions

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><strong>Maestro Studio</strong></td><td>Create tests visually using the Desktop App for zero-IDE setup and instant device connection</td><td><a href="/spaces/eQi66gxHTt2vx4HjhM9V">/spaces/eQi66gxHTt2vx4HjhM9V</a></td><td><a href="/files/O7IAFuXOVkyVu2hJGD5U">/files/O7IAFuXOVkyVu2hJGD5U</a></td></tr><tr><td><strong>Maestro CLI</strong></td><td>Learn to Install the CLI, manage devices, and run tests from your terminal</td><td><a href="/spaces/kq23kwiAeAnHkGJYMGDk">/spaces/kq23kwiAeAnHkGJYMGDk</a></td><td><a href="/files/toW9PJRilCkCH5SKwfHb">/files/toW9PJRilCkCH5SKwfHb</a></td></tr><tr><td><strong>Maestro Cloud</strong></td><td>Scale your testing by integrating with CI Platforms like GitHub Actions to run parallel tests</td><td><a href="/spaces/ky7LkNoLfvcORtXOzzBs/pages/C54ANLVbWffSqhNsoSZU">/spaces/ky7LkNoLfvcORtXOzzBs/pages/C54ANLVbWffSqhNsoSZU</a></td><td><a href="/files/as25p3AxDs1IhAOf7zYn">/files/as25p3AxDs1IhAOf7zYn</a></td></tr></tbody></table>

### Learn How to Test Using Maestro

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><i class="fa-1">:1:</i></td><td><strong>How Maestro works</strong></td><td>Learn about Maestro’s architecture-agnostic approach, device control, and how flows simulate real human interactions</td><td><a href="/pages/P05NwN6Aox1FpJgtQRoy">/pages/P05NwN6Aox1FpJgtQRoy</a></td><td></td></tr><tr><td><i class="fa-2">:2:</i></td><td><strong>Maestro Flows</strong></td><td>Master modular testing with Nested Flows, Loops, Conditions, and Hooks</td><td><a href="/spaces/mS3lsb9jRwfRHqddeRXG">/spaces/mS3lsb9jRwfRHqddeRXG</a></td><td></td></tr><tr><td><i class="fa-3">:3:</i></td><td><strong>JavaScript</strong></td><td>Use JavaScript to handle complex conditions, data manipulation, and external API interactions</td><td><a href="/spaces/mS3lsb9jRwfRHqddeRXG/pages/N72iCVWiRWYBZ8BruPC7">/spaces/mS3lsb9jRwfRHqddeRXG/pages/N72iCVWiRWYBZ8BruPC7</a></td><td></td></tr><tr><td><i class="fa-4">:4:</i></td><td><strong>Manage your workspace</strong></td><td>Learn to configure global behaviors with <code>config.yaml</code>, organize repository architectures, and manage test execution and analysis.</td><td><a href="/spaces/mS3lsb9jRwfRHqddeRXG/pages/KFSFrrC0ZCkU35FT0Y7B">/spaces/mS3lsb9jRwfRHqddeRXG/pages/KFSFrrC0ZCkU35FT0Y7B</a></td><td></td></tr></tbody></table>

### Reference and Resources

Explore the technical specifications and community-proven strategies.

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>API Reference</strong></td><td>A complete guide to every Maestro command</td><td><a href="/spaces/HqSeOOzxPCLfnK9YzOkb">/spaces/HqSeOOzxPCLfnK9YzOkb</a></td></tr><tr><td><strong>Examples</strong></td><td>Proven recipes and real-world examples for you to explore</td><td><a href="/spaces/JjfcEdmJ9ojsT3Jtpsi8">/spaces/JjfcEdmJ9ojsT3Jtpsi8</a></td></tr><tr><td><strong>Troubleshooting</strong></td><td>Find answers in the FAQ and review Known Issues</td><td><a href="/spaces/htfynyR39703f5pJOF1P/pages/79oJhjYKEIqSnUmiko8K">/spaces/htfynyR39703f5pJOF1P/pages/79oJhjYKEIqSnUmiko8K</a></td></tr></tbody></table>

### Join the Community

See real workflows from other teams, and join the [Maestro Slack community](https://slack.maestro.dev/) to share and learn best practices.

<table data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><strong>Community Projects</strong></td><td>Discover VSCode extensions, plugins, and wrappers built by the Maestro community</td><td><a href="/spaces/htfynyR39703f5pJOF1P/pages/DlkzL5O0WJX77R2QnWF4">/spaces/htfynyR39703f5pJOF1P/pages/DlkzL5O0WJX77R2QnWF4</a></td><td><a href="/files/qK0cXfY4186M2aZxIpz8">/files/qK0cXfY4186M2aZxIpz8</a></td></tr><tr><td><strong>Articles</strong></td><td>Explore a list of blog posts, deep dives, and tutorials from industry experts</td><td><a href="/spaces/htfynyR39703f5pJOF1P/pages/1QHDegQRkO1BKEg341mL">/spaces/htfynyR39703f5pJOF1P/pages/1QHDegQRkO1BKEg341mL</a></td><td><a href="/files/pFMymPHQiTRWzoPibNoi">/files/pFMymPHQiTRWzoPibNoi</a></td></tr><tr><td><strong>Customer Showcase</strong></td><td>Check how teams like Doccla, Wahed, and Eneco revolutionized their mobile testing</td><td><a href="/spaces/htfynyR39703f5pJOF1P/pages/ONBmKZIrglKc4g8863cw">/spaces/htfynyR39703f5pJOF1P/pages/ONBmKZIrglKc4g8863cw</a></td><td><a href="/files/1Q0bewRwMiZ2Gs1WdcG9">/files/1Q0bewRwMiZ2Gs1WdcG9</a></td></tr></tbody></table>


# What is Maestro?

Maestro is an open-source UI automation framework for mobile and web with built-in tolerance, zero-wait intelligence, and declarative YAML syntax.

Maestro is the simplest and most effective open-source UI automation framework for mobile and web. It is designed to allow developers and testers to define and automate user journeys with a level of reliability and ease that traditional tools cannot match.

<figure><img src="/files/X1bhQ6R8DOGxKqLjKPwE" alt=""><figcaption></figcaption></figure>

Traditional testing tools often require deep instrumentation, access to the app's source code, and a strong coding ability. Maestro changes this by operating at "[arm's length](/get-started/how-maestro-works#the-arms-length-philosophy)," piloting the device, not the app, scrolling and tapping like a real user would through the same accessibility layer that real users rely on. This eliminates framework dependencies, allowing you to test any app, regardless of whether it was built with React Native, Flutter, or Native code.

### Why choose Maestro?

<table data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><i class="fa-shield">:shield:</i> <strong>Built-in Tolerance</strong></td><td>Maestro embraces the instability of mobile devices by automatically handling flakiness and UI settling.</td></tr><tr><td><i class="fa-timer">:timer:</i> <strong>Zero-Wait Intelligence</strong></td><td>No more manual <code>sleep()</code> calls. Maestro automatically waits for network content and animations to load.</td></tr><tr><td><i class="fa-file">:file:</i> <strong>Declarative Syntax</strong></td><td>Tests are defined in human-readable YAML files, removing the need for deep programming knowledge.</td></tr></tbody></table>

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><i class="fa-rocket-launch">:rocket-launch:</i> <strong>Blazingly Fast Iteration</strong></td><td>Tests run without compilation. Maestro can monitor your files and rerun flows instantly upon saving.</td></tr><tr><td><i class="fa-box">:box:</i> <strong>Single Binary Setup</strong></td><td>Maestro is a single tool that works anywhere, avoiding the "setup hell" associated with legacy drivers.</td></tr></tbody></table>

{% hint style="success" %}
🚀 **Running in the cloud**

Ready to wire into CI or scale up your testing? Start running your flows on [Maestro Cloud](https://maestro.dev/cloud). Check the Cloud documentation to run tests in the cloud and integrate with your CI pipeline.
{% endhint %}

### Maestro vs competitors

While tools like Appium or Selenium treat testing like unit tests inspecting internal APIs, Maestro treats your app as a black box. By simulating "human thumbs on a screen," Maestro validates the complete user experience stack, including interactions with system settings and notifications.

### Ready to start?

Explore the core components of the Maestro ecosystem and begin your journey:

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><strong>How Maestro works</strong></td><td>Understand the "arm's length" philosophy and accessibility-first element detection.</td><td><a href="/pages/P05NwN6Aox1FpJgtQRoy">/pages/P05NwN6Aox1FpJgtQRoy</a></td></tr><tr><td><strong>QuickStart guide</strong></td><td>Install Maestro Studio and run your first test in under five minutes.</td><td><a href="/pages/IyVjUYlja4evcymb2uG3">/pages/IyVjUYlja4evcymb2uG3</a></td></tr><tr><td><strong>Maestro solutions</strong></td><td>Compare the Maestro CLI, Maestro Studio (IDE), and Maestro Cloud to find the right tool for your workflow.</td><td><a href="/pages/epPDXejAMuAN8YhVc0iR">/pages/epPDXejAMuAN8YhVc0iR</a></td></tr><tr><td><strong>Supported platforms</strong></td><td>View the full list of supported frameworks, including Jetpack Compose, SwiftUI, and Web.</td><td><a href="/pages/Yji3ZRqXunrjvcov0BhZ">/pages/Yji3ZRqXunrjvcov0BhZ</a></td></tr></tbody></table>


# How Maestro works

Learn how Maestro simulates user interactions using the accessibility tree and device-level commands.

Maestro operates as a black-box testing framework that simulates user interactions at the device level. Unlike traditional tools that require access to an app's source code or internal APIs, Maestro leverages the operating system's built-in accessibility and input interfaces to treat the application as an opaque system.

### The "arm's length" philosophy

The central mechanic of Maestro is its "arm's length" operation, meaning the framework pilots the device itself rather than the internal app code.

* **Platform Agnostic**: Because Maestro interacts with the Accessibility Tree (the data layer used by screen readers), it provides a consistent experience across heterogeneous stacks like Native iOS/Android, React Native, and Flutter.
* **Human Simulation**: Maestro reproduces physical actions, like "thumbs on a screen", by sending low-level device commands for tap events, swipe gestures, and text input.
* **System-Wide Control**: Since it controls the device, Maestro can interact with elements outside the app, such as system settings, permission dialogs, and notifications.

### Declarative test definition (YAML)

Tests are defined in simple YAML files, allowing user journeys, referred to as Flows, to be readable as English instructions.

* **Built-in Intelligence**: Maestro features "idiomatic" commands designed to solve common automation hurdles. For example, `scrollUntilVisible` mimics human visual search by automatically scrolling until it finds an element, rather than requiring brittle coordinate-based logic.
* **Built-in Tolerance**: Maestro "embraces instability" by automatically waiting for the screen to "settle" or for content to load, removing the need for manual `sleep()` or artificial wait commands.

{% embed url="<https://vimeo.com/744398229?fl=pl&fe=cm>" %}

### Maestro ecosystem and workflow

The Maestro suite provides different interfaces to interact with the same core execution engine.

#### Maestro Studio (Visual IDE)

Maestro Studio is the recommended entry point for new users to build tests visually:

1. **Connection**: Studio connects to a device and mirrors the screen to your browser.
2. **Visual Interaction**: You click on elements of the mirrored screen, and Studio identifies them and suggests commands.
3. **Real-time Execution**: Commands are inserted into your YAML file and executed on the device instantly, allowing for step-by-step flow construction.

<figure><img src="/files/XRf3vgZSl0ZjSDQY3rRG" alt=""><figcaption></figcaption></figure>

#### Maestro CLI

The CLI is the open-source engine that interprets YAML files and sends instructions to the device. On both Android and iOS, it installs and communicates with a small companion driver app on the device to see what's on screen and carry out actions like taps and swipes. The CLI is the primary tool for automated execution in Continuous Integration (CI) systems.

#### Maestro Cloud

Maestro Cloud acts as the execution backend for large-scale testing. Teams upload their apps and Flows to run in parallel across multiple hosted virtual devices, ensuring deterministic results and faster feedback loops.

### Advanced logic and modularity

To handle complex scenarios without code duplication, Maestro utilizes modular logic and scripting:

* **Subflows**: Common sequences, such as a "Login" process, can be saved in separate files and called by multiple main tests to facilitate maintenance.
* **Hooks**: Commands like `onFlowStart` and `onFlowComplete` manage app state, such as clearing cache before a test or logging out after completion.
* **JavaScript**: For dynamic data needs (e.g., generating random emails), you can inject small JavaScript snippets that run in a restricted environment without access to the local file system.

### Next steps

To continue your journey with Maestro, choose one of the following options::

* Check the [QuickStart](/get-started/quickstart) guide to learn how to install Maestro Studio and run your first test.
* Explore each of the available [Maestro solutions](/get-started/maestro-solutions).


# QuickStart

Install Maestro Studio, set up your environment, and run your first automated test in five minutes.

You are starting your journey with Maestro. This guide will help you install [Maestro Studio](https://docs.maestro.dev/maestro-studio/), set up your environment, and execute your first automated test (called a **Flow**) in just five minutes.

{% stepper %}
{% step %}

#### Platform-specific setup

Maestro requires a running target device to execute your tests. Use the tabs below to configure your virtual environment.

{% tabs %}
{% tab title="Android" %}

1. Download the latest version of Android Studio from the [official site](https://developer.android.com/studio) and install it.
2. Open Android Studio, click **More Actions**, and select **Virtual Device Manager**.
3. Click **Create Virtual Device (+)**, select a modern device (e.g., Pixel 8), and download a system image (API 31 or higher is recommended).

{% hint style="info" %}
Maestro currently supports **API Levels 29, 30, 31, 33, and 34**. API 35 and 36 support is arriving in Q2 2026.
{% endhint %}

4. Finish the wizard and click the **Play** button to start the emulator.
   {% endtab %}

{% tab title="iOS" %}

1. Download Xcode from the [Mac App Store](https://apps.apple.com/us/app/xcode/id497799835?mt=12) and install it.
2. Open Xcode, go to `Settings > Locations`, and ensure the **Command Line Tools** are selected.
3. Open Xcode and go to `Xcode > Open Developer Tool > Simulator` to launch the simulator.
4. If no device is available, go to `Xcode > Settings > Platforms` and ensure an iOS runtime (iOS 16, 17, 18, or 24) is installed.

{% hint style="warning" %}
**Xcode is installed but no iOS simulators appear?**

If you installed Xcode without opening it (for example via [`xcodes`](https://github.com/XcodesOrg/xcodes) or a scripted install), macOS may not have installed Xcode's required system components - so the CoreSimulator framework is missing and simulators won't load. In Maestro Studio this shows up as no iOS devices in the device list.

Complete the one-time component install:

```bash
xcodebuild -runFirstLaunch

# If you're prompted to accept the license first, run:

sudo xcodebuild -license accept

# Then restart Maestro Studio
# iOS simulators should now appear.
```

{% endhint %}
{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Installation

Download the appropriate installer for your operating system:

* **Windows:** [MaestroStudio.exe](https://studio.maestro.dev/MaestroStudio.exe)
* **macOS:** [MaestroStudio.dmg](https://studio.maestro.dev/MaestroStudio.dmg)
* **Linux:** [MaestroStudio.AppImage](https://studio.maestro.dev/MaestroStudio.AppImage)

Follow the platform-specific installation prompts:

* **Windows:** Double-click the `.exe` and follow the setup wizard.
* **macOS:** Open the `.dmg` and drag Maestro Studio to your `Applications` folder.
* **Linux:** Make the `.AppImage` executable and run it with the `--no-sandbox` flag:

  ```bash
  chmod +x MaestroStudio.AppImage
  ./MaestroStudio.AppImage --no-sandbox
  ```

{% endstep %}

{% step %}

#### Create your first test

Once your device is running and Maestro Studio is open, you can create your first Flow.

1. Open Maestro Studio and click **Choose new workspace location** to define the directory on your computer to store your tests.

<figure><img src="/files/eHHGoJx0G5pH7ZHUZ56u" alt=""><figcaption></figcaption></figure>

2. Click the **No device connected** button at the top. Select your running Android Emulator or iOS Simulator from the list. The virtual device will pop up.
3. Click **Create a new test** to open the setup window.

<figure><img src="/files/OmG5AdwLtTcdwwLIDWy9" alt=""><figcaption></figcaption></figure>

4. On the **Add a new test to your workspace** window, select **Mobile Test** and enter the following:

{% tabs %}
{% tab title="Android" %}

* **Name**: Name for your YAML file.
* **App Id**: From the dropdown menu, select the App Id for testing. For this QuickStart, select **com.google.android.contacts** from the dropdown menu.

<figure><img src="/files/nZhvZp1ini7rXPEn4TKt" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="iOS" %}

* **Name**: Name for your YAML file.
* **App Id**: From the dropdown menu, select the App Id for testing. For this QuickStart, select **com.apple.MobileAddressBook** from the dropdown menu.

<figure><img src="/files/Ohat0wCtHgDBO2Zg7Z9Y" alt=""><figcaption></figcaption></figure>
{% endtab %}
{% endtabs %}

{% hint style="info" %}
You can also use the **Scan file for App Id** option to automatically detect the identifier from an `.apk` (Android) or `.app/.zip` (iOS) file.

You can also add tags to keep your tests organized.
{% endhint %}

6. Click **Create Test**. Maestro will generate a minimal YAML file to launch the app.

{% tabs %}
{% tab title="Android" %}

```yaml
appId: com.google.android.contacts
---
- launchApp:
    clearState: true
```

{% endtab %}

{% tab title="iOS" %}

```yaml
appId: com.apple.MobileAddressBook 
---
- launchApp:
    clearState: true
```

{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Run your first test

With your first YAML file created, let's add a few commands to perform a search.

In the Maestro Studio editor, copy and paste the example below for your platform.

{% tabs %}
{% tab title="Android" %}

```yaml
appId: com.google.android.contacts
---
- launchApp:
    clearState: true
- tapOn: Allow
- tapOn: Create contact
- tapOn: First name
- inputText: John
- tapOn: Last name
- inputText: Doe
- tapOn: Company
- inputText: Maestro
- tapOn: "+1"
- inputText: 111-111-1111
- tapOn: Save
- back
- takeScreenshot: All Contacts
```

The test launches the native Contacts app, creates a new entry, saves it, and captures a screenshot.

{% hint style="info" %}
To learn more about the commands you can use to create tests, access the [API Reference](https://docs.maestro.dev/reference/)page.

To learn about how you can structure tests, also referred to in Maestro as Flows, access the [Flows](https://docs.maestro.dev/maestro-flows/).
{% endhint %}

After pasting, click **Run Locally**. Watch your virtual device execute the steps automatically. Maestro Studio will highlight each step as it succeeds or provide a failure reason if an element cannot be found.

<figure><img src="/files/juNkGVxIAuPhWOk0cyQq" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="iOS" %}

```yaml
appId: com.apple.MobileAddressBook 
---
- launchApp
- tapOn: All iPhone
- tapOn: Add
- tapOn: First name
- inputText: John
- tapOn: Last name
- inputText: Doe
- tapOn: Company
- inputText: Maestro
- tapOn: John
- tapOn: add phone
- inputText: +1 111-111-1111
- tapOn: Done
- tapOn: Back
- takeScreenshot: All Contacts
```

The test launches the Address Book app, adds a new contact, and saves the result.

{% hint style="info" %}
To learn more about the commands you can use to create tests, access the [API Reference](https://docs.maestro.dev/reference/) page.

To learn about how you can structure tests, also referred to in Maestro as Flows, access the [Flows](https://docs.maestro.dev/maestro-flows/).
{% endhint %}

After pasting, click **Run Locally**. Watch your virtual device execute the steps automatically. Maestro Studio will highlight each step as it succeeds or provide a failure reason if an element cannot be found.

<figure><img src="/files/cXUBcec2N2iH8vYVeOMg" alt=""><figcaption></figcaption></figure>
{% endtab %}
{% endtabs %}
{% endstep %}
{% endstepper %}

{% hint style="success" %}
**Interactive Flow authoring**

While this QuickStart focuses on manual YAML, Maestro Studio offers three interactive ways to build your test:

* **Inspect Screen**: Click the **Inspect Screen** button to select elements visually on the device and receive recommended commands.
* **Insert Command**: Click the **Insert Command** button in the IDE to choose from a list of standard actions.
* **Manual Entry**: Type commands directly into the YAML editor for precise control.

<i class="fa-hand-point-right">:hand-point-right:</i> **Access** [Run tests with Maestro Studio](/maestro-studio/run-tests-with-maestro-studio) **for more information.**
{% endhint %}

### Video walkthrough

Watch this step-by-step video to see the first test creation process in detail:

{% embed url="<https://www.youtube.com/watch?v=E7qwFwo_nu0>" %}

### Next steps

Now that you’ve seen the power of "arm's length" automation, explore these resources to master the ecosystem:

* Visit the [Maestro Studio](https://docs.maestro.dev/maestro-studio/) to learn how to use visual element inspection and the Live REPL to build tests without writing code from scratch.
* If you prefer a programmatic approach or need to integrate tests into your CI/CD pipeline, head to the [Maestro CLI](https://docs.maestro.dev/maestro-cli/).
* To learn the best practices for logic, modularity, and nesting, or learn how to add JavaScript scripts to your tests check out the [Flows](https://docs.maestro.dev/maestro-flows/).


# Maestro solutions

Explore Maestro's ecosystem: Studio, CLI, Cloud, and Flows, plus how they fit into local automation and CI/CD workflows.

The Maestro ecosystem is a unified platform composed of three interconnected layers. Each tool is designed to solve a specific challenge in the mobile and web automation lifecycle:

* **Maestro Studio**: Desktop IDE for writing and running Maestro flows
* **Maestro CLI**: Command line tool for running Maestro flows
* **Maestro Cloud**: Hosted platform for consistent, parallel Maestro execution

### **Maestro Studio (The IDE)**

Maestro Studio is a visual interface built on top of the CLI, designed for rapid test creation and real-time element inspection. By mirroring your device screen and allowing you to build [Flows](https://docs.maestro.dev/maestro-flows/) through simple point-and-click interactions, it serves as the primary tool for zero-code authoring and interactive debugging.

You can start building your first Flows today by visiting the [Maestro Studio](https://docs.maestro.dev/maestro-studio/) documentation.

### **Maestro CLI**

The CLI is the open-source heart of Maestro and the core engine that powers both Maestro Studio and Maestro Cloud. It serves as the workhorse for developers and DevOps engineers, interpreting your YAML files to orchestrate test execution. Because everything else is built on top of the CLI, it acts as the foundational backbone for all local automation and CI/CD integration.

To learn more about its technical capabilities and orchestration features, check out the [Maestro CLI](https://docs.maestro.dev/maestro-cli/) documentation.

### **Maestro Cloud**

Maestro Cloud is a managed execution solution designed to scale your testing infrastructure without the overhead of managing local device farms. It leverages the Maestro CLI to run your tests on a distributed cloud of virtual devices, enabling massive parallelization and providing fast and reliable feedback loops for production-ready reliability.

Explore how to scale your regression suites in the [Maestro Cloud](/maestro-cloud) documentation.

### Which solution should I use?

Use the table below to determine which tool fits your current workflow:

| **Feature**     | **Maestro Studio**      | **Maestro CLI**         | **Maestro Cloud**                                                   |
| --------------- | ----------------------- | ----------------------- | ------------------------------------------------------------------- |
| **Best For**    | Authoring & Debugging   | Local execution & CI/CD | Reliable & scalable parallel test execution                         |
| **Interface**   | Visual (GUI)            | Terminal (Command Line) | Upload via CLI, see runs via web, notifications via Slack + Webhook |
| **Execution**   | Real-time / Interactive | Sequential (Local)      | Parallel (Simultaneous)                                             |
| **Target User** | Testers & Developers    | Engineers & DevOps      | Growth & Professional Teams                                         |
| **Environment** | Local Device/Emulator   | Local Device/Emulator   | Hosted Virtual Devices                                              |

### The typical learning path

Most teams follow a three-stage journey to automation success:

1. **Creation**: Start with [Maestro Studio](https://docs.maestro.dev/maestro-studio/) to visually build and debug your first [Flows](https://docs.maestro.dev/maestro-flows/).
2. **Automation**: Use the[ Maestro CLI](https://docs.maestro.dev/maestro-cli/) to run those Flows locally and integrate them into your basic development workflow.
3. **Scaling**: Once your test suite grows, transition to [Maestro Cloud](/maestro-cloud) to run those same tests in parallel for instant feedback on every Pull Request.

### Next steps

If you already know the Maestro solution you are going to use, access the desired documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)

If you don't know how to create tests with Maestro, access the [QuickStart](/get-started/quickstart) guide to get up and running in minutes.


# Supported platforms

Platforms supported by Maestro: Android, iOS, React Native, Flutter, and web applications via UI-layer automation.

<figure><img src="/files/C8efUjjvaaggaZ4WsEc1" alt=""><figcaption></figcaption></figure>

Maestro operates on the principle of UI-layer automation rather than code instrumentation. Like a test driver interacting with any vehicle through its controls (steering wheel, pedals), Maestro interfaces with applications through their visual and accessibility layers, regardless of the underlying technology stack.

### Mobile operating systems (core)

Maestro provides comprehensive automation support for major mobile platforms:

* **Android:** Full support for emulators and physical devices
* **iOS:** Full support for simulators

Both platforms pass validation at scale by enterprise teams at Microsoft, DoorDash, and others for complex automation scenarios.

### Framework agnosticism

Maestro decouples test automation from framework implementation by interacting directly at the UI layer. This removes the need for framework-specific instrumentation, which is a departure from tools like Detox or Appium, which depend on complex native drivers and in-app code injection.

Supported technology stacks include:

* **Native:** Swift, Kotlin, Java, Objective-C.
* **Cross-platform:** React Native, Flutter.
* **Other hybrid frameworks.**

### Web automation

* **Status:** Functional with ongoing development.
* **Approach:** Maestro extends support to web applications, enabling cross-platform test consolidation using consistent automation syntax.
* **Use Case:** Unified testing strategy across mobile and web surfaces.

### System-level interaction

Maestro supports device-level automation beyond app boundaries:

* **System Interaction:** Navigate to Settings, modify permissions, toggle Wi-Fi, and monitor app behavior in response to system state changes.
* **Real-world Scenarios:** Automated testing of notification handling, permission flows, and inter-app interactions.

### Next steps

Explore the details on how to use Maestro in each one of the supported operating systems and frameworks:

* [Android](/get-started/supported-platform/android)
* [Android Native](/get-started/supported-platform/android/android-native)
* [Jetpack Compose](/get-started/supported-platform/android/jetpack)
* [iOS](/get-started/supported-platform/ios)
* [SwiftUI](/get-started/supported-platform/ios/swiftui)
* [UIKit](/get-started/supported-platform/ios/uikit)
* [React Native](/get-started/supported-platform/react-native)
* [Flutter](/get-started/supported-platform/flutter)
* [Web Browsers](/get-started/supported-platform/web-browser)


# Android

Configure Maestro for Android app testing with emulators or physical devices.

<figure><img src="/files/ZkvJm2hjaXNVC5CuzPC7" alt=""><figcaption></figcaption></figure>

Maestro provides a high-level abstraction for Android testing by simulating end-user interactions at the presentation layer. Unlike other frameworks, Maestro doesn't live inside your app, it pilots the device from the outside.

### A UI-centric approach

Maestro operates through the Android display stack, ensuring your tests are framework-agnostic. Whether you use Kotlin, Java, Flutter, React Native, or Compose, Maestro interacts only with the rendered UI.

* **Human-Like Simulation**: Maestro translates declarative commands into system-level inputs, mimicking genuine user behavior on the Android input pipeline.
* **Zero Instrumentation**: You don't need to add test dependencies to your `build.gradle` or compile a special "test APK."

### System-level control

Maestro drives the entire device, not just your app. If you need to change WIFI settings, read a notification, create contacts, or manage anything else that depends on device state or data, you can do so in exactly the same way a real user would, using "real human thumbs."

To manage system settings mid-test, you can interact with the OS directly or use built-in commands:

```yaml
- toggleAirplaneMode
- tapOn: "Airplane mode"
- runFlow: toggle_wifi.yaml # Use a subflow for complex system interactions
```

Android apps often cache data that can lead to flaky tests. The `clearState` command ensures a reproducible environment by clearing app data (the equivalent of`adb shell pm clear <package-name>`) before the app launches, giving the app a "just installed" state.

```yaml
- launchApp:
    appId: "com.example.app"
    clearState: true  # Resets app data for a clean launch
```

### Execution and environment setup

Maestro connects to your target via ADB (Android Debug Bridge).

* **Emulators & Physical Devices**: Maestro automatically detects and connects to running emulators or physical devices. For physical hardware, ensure "USB Debugging" is enabled in Developer Options.
* **App Identification**: Maestro targets your application using the **Package** found in your `AndroidManifest.xml` (the `appId`).
* **Installation**: Maestro assumes the application is already installed on the target device/emulator before the test begins.

### Cross-platform configuration

If your Android and iOS applications use different identifiers, we recommend using [environment variables](/maestro-flows/flow-control-and-logic/parameters-and-constants) to keep your [Flows](https://docs.maestro.dev/maestro-flows/) cross-platform.

You can manage these variables in three primary ways:

1. **Maestro Studio**: Configured via the [Environment Manager](/maestro-studio/environments-and-variables).
2. [**Maestro CLI**](/maestro-flows/flow-control-and-logic/parameters-and-constants#passing-parameters-via-cli): Passed as arguments during execution.
3. **Flow Configuration**: Defined directly in the [config matter](/maestro-flows/flow-control-and-logic/parameters-and-constants#constants) at the top of an individual Flow file.

To run a single test suite against different platforms where the App ID varies, structure your Flow to use a variable:

```yaml
# In your Flow
appId: ${APP_ID}
---
- launchApp
```

When executing locally with the [Maestro CLI](https://docs.maestro.dev/maestro-cli/), use the `-e` or `--env` flag to inject the correct identifier for that specific run:

```bash
maestro test -e APP_ID=com.example.android flow.yaml
## or
maestro test --env APP_ID=com.example.android flow.yaml
```

### Maestro Cloud

When your suite grows, local sequential execution becomes a bottleneck. [Maestro Cloud](/maestro-cloud) spins up multiple virtual Android devices to run your tests in parallel.

| **Feature**         | **Local Android**  | **Maestro Cloud**                  |
| ------------------- | ------------------ | ---------------------------------- |
| **Parallelization** | 1 device at a time | Scale to as many devices as needed |
| **Speed**           | Slow (Sequential)  | Blazingly Fast (Parallel)          |
| **Cleanup**         | Manual             | Automatic Infrastructure Teardown  |

### Next steps

Explore the dedicated [Android Native](/get-started/supported-platform/android/android-native) and [Jetpack Compose](/get-started/supported-platform/android/jetpack) documentation for platform-specific patterns.

If you already know the Maestro solution you are going to use, access the desired documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)

If you are new to the platform, follow the [QuickStart](/get-started/quickstart) guide to get up and running in minutes.


# Android Native

Black-box testing for Android Views and Jetpack Compose via the Accessibility layer. Target elements by text, Resource ID, Content Description, or testTag.

<figure><img src="/files/y0tgG9VVQjMkqSwcdIhN" alt=""><figcaption></figcaption></figure>

Maestro provides a black-box testing approach for native Android applications. By interacting with the app through the Android Accessibility layer rather than internal code instrumentation, you can test production-ready binaries without modifying your source code.

### Technical advantages

* **Zero Instrumentation**: No custom Gradle configurations, `build.gradle` dependencies, or test-specific APK builds are required. You test the exact binary your users receive.
* **Refactoring Resilience**: Migrating your UI implementation will not break your tests as long as the user-visible text or semantic IDs remain consistent.
* **UI-Layer Interaction**: Maestro interacts with rendered UI elements, ensuring that tests validate the actual user experience rather than internal component states.

### Element interaction strategies

In Android development, Maestro leverages the accessibility layer of the device to identify and interact with elements via primary methods:

* **Text Selection**: Target any view with a `text` property (e.g., `Button`, `TextView`).

  ```yaml
  - tapOn: "Login"
  ```
* **Resource ID**: Access views by their `android:id`. This is ideal for disambiguating identical text elements.

  ```yaml
  - tapOn:
      id: "login_button"
  ```
* **Content Description**: The `android:contentDescription` attribute is surfaced as a text property, making it the "gold standard" for automating icons and image-based buttons.

  ```yaml
  - tapOn: "Settings Icon"
  ```
* **Hints**: For input fields that have not yet been filled, the `android:hint` attribute is also exposed to the text selector.

#### Handling lists and dynamic content

Native Android often uses `RecyclerView` or `LazyColumn` for long lists. Maestro abstracts the complexity of view recycling through intelligent scrolling.

Instead of calculating scroll offsets, use the built-in intelligence of Maestro to find elements that are currently off-screen:

```yaml
- scrollUntilVisible:
    element:
        text: "Item #50"
    direction: DOWN
```

Maestro will automatically swipe down, wait for the next items to load, and stop only when the target element is detected in the view hierarchy.

### Known limitations

While Maestro can detect and tap on views containing Unicode characters, direct inputting (typing) of Unicode text via the `inputText` command is currently not possible.

### Next steps

If you don't know how to create tests with Maestro, access the [QuickStart](/get-started/quickstart) guide to get up and running in minutes.

To learn how to create tests, refer to the [Flows](https://docs.maestro.dev/maestro-flows/) documentation. If you want to explore Maestro solutions, consult the appropriate documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)


# Jetpack Compose

Black-box testing for Jetpack Compose via AccessibilityService. Prioritizes text-based matching and semantics over testTag for refactoring resilience.

<figure><img src="/files/w1xWBjwBcDR8HYuyjNWD" alt=""><figcaption></figcaption></figure>

Maestro follows a fundamental principle: **UI-level automation without framework intrusion**. Unlike instrumentation-based tools such as Espresso or Robolectric, which execute inside the app process and depend on framework-specific APIs, Maestro operates externally using Android’s system automation interfaces. This allows Maestro to perform reliable black-box testing regardless of whether your UI is implemented with XML Views, Jetpack Compose, or other UI frameworks.

### Architecture-agnostic testing philosophy

Maestro interacts with your application the same way the Android system and users do, using accessibility metadata and system-level automation. This provides several key advantages:

* **Framework independence:** Maestro does not depend on Compose internals such as composition state, recomposition cycles, or render tree structure. Tests remain valid regardless of UI implementation details.
* **System-level element discovery:** Maestro locates elements using accessibility metadata, visible text, resource identifiers, and other stable attributes exposed by the Android system. This ensures compatibility with Compose, XML Views, and hybrid applications.
* **Stable selector model:** Element identification relies on externally observable properties such as visible text, accessibility labels (`contentDescription`), and resource IDs, rather than internal framework constructs.

### Refactoring resilience

Maestro tests are designed to validate user workflows rather than internal implementation details. This provides strong resilience during UI refactoring, including migration from XML-based layouts to Jetpack Compose.

* **Semantic stability:** As long as visible text, accessibility metadata, or resource identifiers remain consistent, existing Maestro tests typically continue to work without modification.
* **Implementation independence:** Tests verify behavior from the user’s perspective, protecting against failures caused by internal architectural changes that do not affect the user experience.

### Element interaction patterns

Maestro interacts with UI elements using selectors based on system-exposed properties rather than framework-specific APIs.

#### Element selection strategy

Maestro supports multiple stable selector types, including:

* **Visible text**

  ```yaml
  - tapOn: "Login"
  ```

  Matches elements displaying the specified text.
* **Accessibility labels**

  Elements with accessibility metadata such as:

  ```kotlin
  Modifier.semantics {
      contentDescription = "Login Button"
  }
  ```

  can be targeted directly:

  ```yaml
  - tapOn:
      description: "Login Button"
  ```
* **Resource identifiers**

  When available, resource IDs provide highly stable selectors:

  ```yaml
  - tapOn:
      id: "login_button"
  ```

These selector strategies work consistently across Compose and View-based interfaces.

### Compatibility with Jetpack Compose semantics

Jetpack Compose exposes accessibility metadata through its semantics system. Maestro automatically uses this information when available, including:

* Visible text from composables such as `Text` and `Button`
* Accessibility descriptions defined with `contentDescription`
* Resource identifiers when provided

This enables reliable interaction without requiring Compose-specific test APIs or instrumentation.

### Comparative advantages

| Aspect                     | Maestro                             | Instrumentation Tools  |
| -------------------------- | ----------------------------------- | ---------------------- |
| Execution model            | External (black-box)                | In-process (white-box) |
| Framework dependency       | None                                | High                   |
| Compose internals required | No                                  | Yes                    |
| Test infrastructure in app | Not required                        | Required               |
| Refactoring resilience     | High (when semantics remain stable) | Moderate to low        |
| Accessibility usage        | Recommended for reliability         | Optional               |

Maestro eliminates the need for app-side test infrastructure code, lowering barriers for QA practitioners lacking native Android development expertise.

### Next steps

If you don't know how to create tests with Maestro, access the [QuickStart](/get-started/quickstart) guide to get up and running in minutes.

To learn how to create tests, refer to the [Flows](https://docs.maestro.dev/maestro-flows/) documentation. If you want to explore Maestro solutions, consult the appropriate documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)


# iOS

Black-box iOS testing via the Accessibility layer. Run on Xcode Simulators, handle permissions, test multi-app journeys, and parallelize with Cloud.

<figure><img src="/files/ymQRGDocGwtsn3xNeOyt" alt=""><figcaption></figcaption></figure>

Maestro provides a high-level abstraction for iOS testing by simulating end-user interactions at the presentation layer. Unlike traditional testing tools that require deep instrumentation, Maestro interacts with the iOS Accessibility layer, allowing you to test your app exactly as a user would.

### Black-box approach

Maestro analyzes the rendered frames of the iOS device, ensuring your tests are framework-agnostic. Whether your app is built with Swift, Objective-C, Flutter, React Native, or SwiftUI, Maestro interacts only with the visual output.

* **Physical Input Simulation**: Declarative commands are translated into native touch events. When you use `tapOn`, Maestro triggers the same iOS input pipeline that a physical touch would.
* [**Arm's Length**](/get-started/how-maestro-works): Maestro doesn't require access to your source code or bytecode. You test the same `.app` bundle (the Simulator version) that runs on your virtual testing environment.

### System-level control

Maestro’s architecture allows it to pilot the entire device, not just your application process. This enables testing for complex real-world scenarios.

iOS is known for its strict permission dialogs (Location, Camera). However, Maestro can interact with these system prompts directly:

```yaml
- launchApp:
    appId: "com.example.app"
    permissions:
      location: allow
      notifications: allow
```

Maestro also allows you to create multi-app journeys. You can test flows that leave your app, such as opening a link in Safari or checking an email, and then return to your application:

```yaml
- tapOn: "Open Website"
# Maestro follows the link into Safari
- assertVisible: "Welcome to our site"
- tapOn:
    id: "breadcrumb" # Native iOS 'Back' button to return to your app
```

### Execution and environment setup

Maestro connects to your target via native Apple development tools.

* **Simulators**: Run tests on any iOS Simulator managed by Xcode. Ensure you have the Xcode Command Line Tools installed (`xcode-select --install`).
* **App Identification**: iOS apps are targeted using the Bundle ID (e.g., `com.example.app`).

### Cross-platform configuration

If your Android and iOS applications use different identifiers, we recommend using [environment variables](/maestro-flows/flow-control-and-logic/parameters-and-constants) to keep your [Flows](https://docs.maestro.dev/maestro-flows/) cross-platform.

You can manage these variables in three primary ways:

1. **Maestro Studio**: Configured via the [Environment Manager](/maestro-studio/environments-and-variables).
2. [**Maestro CLI**](/maestro-flows/flow-control-and-logic/parameters-and-constants#passing-parameters-via-cli): Passed as arguments during execution.
3. **Flow Configuration**: Defined directly in the [config matter](/maestro-flows/flow-control-and-logic/parameters-and-constants#constants) at the top of an individual Flow file.

To run a single test suite against different platforms where the App ID varies, structure your Flow to use a variable:

```yaml
# In your Flow
appId: ${APP_ID}
---
- launchApp
```

When executing locally with the [Maestro CLI](https://docs.maestro.dev/maestro-cli/), use the `-e` or `--env` flag to inject the correct identifier for that specific run:

```bash
maestro test -e APP_ID=com.example.app.ios flow.yaml
## or
maestro test --env APP_ID=com.example.app.ios flow.yaml
```

### Parallelization for iOS

Scaling iOS tests locally can be difficult due to macOS hardware requirements. [Maestro Cloud](/maestro-cloud) provides instant access to a fleet of iOS Simulators, allowing you to run your entire suite in parallel.

* **Speed**: Reduce test time drastically.
* **Reliability**: Eliminate "flaky" results caused by local machine resource contention.
* **CI/CD Integration**: Automatically trigger parallel iOS runs on every Pull Request.

### Next steps

Explore the dedicated [UIKit](/get-started/supported-platform/ios/uikit) or [SwiftUI](/get-started/supported-platform/ios/swiftui) documentation, or access the [QuickStart](/get-started/quickstart) guide to get up and running in minutes if you do not know how to create tests with Maestro.

If you already know which Maestro solution you are going to use, access the relevant documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)


# SwiftUI

Configure SwiftUI apps for Maestro testing with proper accessibility identifiers.

<figure><img src="/files/FqvNwDphuDkPOwMKqP91" alt=""><figcaption></figcaption></figure>

Maestro offers transparent support for SwiftUI applications. Because SwiftUI is declarative and content-centric, Maestro’s architecture-agnostic approach is perfectly suited for it—treating components like `List`, `LazyVStack`, and `Picker` as interactive visual elements rather than complex code objects.

### The SwiftUI workflow

While Maestro can find SwiftUI elements by their displayed text, using identifiers is the most resilient way to build your test suite.

#### **Implementing accessibility identifiers**

For icons, custom controls, or to avoid breaking tests when text is translated, apply the `.accessibilityIdentifier()` modifier in your Swift code. By adding the `.accessibilityIdentifier` modifier, you are explicitly tagging a component in the underlying iOS Accessibility Tree.

```swift
// Step 1: In your SwiftUI View
NavigationLink(value: Panel.donutEditor) {
    Label("Donut Editor", systemImage: "slider.horizontal.3")
}
.accessibilityIdentifier("donut_editor")
```

This ID is invisible to the end-user but is broadcast to any tool interacting with the system's accessibility layer. Thus, Maestro can interact with it using the `id` selector.

```yaml
# Step 2: In your Maestro Flow
- tapOn:
    id: "donut_editor"
```

### Examples

These snippets demonstrate how Maestro handles common SwiftUI patterns and system behaviors.

#### **Native controls (pickers and toggles)**

SwiftUI controls like `Picker` often require specific targeting, especially when multiple options are present.

```yaml
appId: com.swiftui.kit
---
- launchApp:
    clearState: true
- tapOn:
    id: "controls_item"
- tapOn:
    point: "84%,23%" # Precise coordinate tap for complex toggles
- tapOn:
    text: "Chocolate"
    index: 0
- tapOn:
    id: "flavor_picker_segmented_Strawberry" # Target specific segments by ID
```

The examples above use a mix of IDs, Text, and Coordinates to navigate complex SwiftUI layouts. While the `id` selector is the most reliable way to target elements, you can also use `index` to handle duplicate text or `point` for precise taps on small system controls like Toggles.

#### **System navigation and app switching**

Maestro can handle "Inter-App" communication, such as following a link into Safari and using the system breadcrumb to return.

```yaml
appId: com.swiftui.kit
---
- launchApp
- tapOn: "link_item"
# Maestro follows the link into the Browser...
- tapOn:
    id: "breadcrumb" # Returns to your app via the native iOS 'Back' link
```

#### Refactoring resilience

A major advantage of using Maestro with SwiftUI is migration safety. If you rewrite a legacy UIKit screen entirely in SwiftUI, as long as the visual output and accessibility identifiers remain the same, your Maestro tests will require zero changes.

#### Tips and known issues

* **Hierarchy Quirks**: Some specific styles (like `WheelPickerStyle`) may not return a full hierarchy to the accessibility layer, making text-based selection preferred over ID selection in those cases.
* **Merged Elements**: When a `Toggle` is initialized with text, iOS often merges the text and the switch into a single accessibility element.
* **Maestro Studio**: Always use [Maestro Studio](https://docs.maestro.dev/maestro-studio/) to inspect SwiftUI views. It helps you see exactly how the nested view composition is resolved by the accessibility tree before you write your YAML.

### Next steps

If you don't know how to create tests with Maestro, access the [Quickstart](/get-started/quickstart) guide to get up and running in minutes.

To learn how to create tests, refer to the [Flows](https://docs.maestro.dev/maestro-flows/) documentation. If you want to explore Maestro solutions, consult the appropriate documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)


# UIKit

Black-box testing for UIKit via the accessibility layer. Use accessibilityLabel for text and accessibilityIdentifier (gold standard) for id selectors.

<figure><img src="/files/ymQRGDocGwtsn3xNeOyt" alt=""><figcaption></figcaption></figure>

Maestro provides native, transparent support for iOS applications built with UIKit. Operating at the visual interaction layer rather than the implementation layer, Maestro interacts with rendered UI components regardless of underlying class hierarchies such as `UIButton`, `UITableView`, and `UILabel`.

### Technical advantages

* **Zero Instrumentation**: Maestro does not require test libraries, delegates, or ViewController exposure. You test the built `.app` binary, nothing else.
* **Implementation Agnostic**: Because Maestro validates user-facing features rather than internal code, you can refactor UIKit components to SwiftUI without breaking your tests, provided the visual output remains consistent.
* **Black-Box Model**: Maestro adopts an [arm's length](/get-started/how-maestro-works) approach, simulating authentic user interactions with the rendered UI without needing access to internal source code.

### Element interaction strategies

Maestro interacts with UIKit components by simulating authentic user interactions through the accessibility layer.

#### **Interacting with views by text**

Primary interaction in UIKit is often done via visible UI text. Any view with text content (like a `UIButton` title) can be targeted directly.

```swift
// In your UIKit Swift code
let button = UIButton()
button.setTitle("Submit Order", for: .normal)
```

You can tap this button in your Flow using the visible text:

```yml
- tapOn: "Submit Order"
```

#### **Using accessibility labels and IDs**

For non-textual elements like icons, or for disambiguating duplicate elements, leverage iOS accessibility metadata. Maestro translates these properties into specific selectors:

* `accessibilityLabel`: Maestro translates this to the `text` selector.
* `accessibilityIdentifier`: Maestro translates this to the `id` selector. This is the gold standard for reliable tests.

```swift
// In your UIKit Swift code
let button = UIButton()
button.accessibilityIdentifier = "login_button_id"
```

The corresponding tap command in your Flow would use the `id`:

```yaml
- tapOn:
    id: "login_button_id"
```

### Handling lists and complex components

Maestro abstracts away the complexity of coordinate calculations and cell enumeration in `UITableView` and `UICollectionView`.

#### **Intelligent scrolling**

Instead of manual index or offset calculations, use `scrollUntilVisible`. Maestro combines visibility detection with continuous swiping to find elements that are currently off-screen.

```yaml
- scrollUntilVisible:
    element:
        text: "List Item 50"
    direction: DOWN
```

### Known limitations

* **Simulators**: Full support for local execution on iOS Simulators.
* **Physical Devices**: Executing tests on physical iOS devices is not supported yet.

### Next steps

If you don't know how to create tests with Maestro, access the [QuickStart](/get-started/quickstart) guide to get up and running in minutes.

To learn how to create tests, refer to the [Flows](https://docs.maestro.dev/maestro-flows/) documentation. If you want to explore Maestro solutions, consult the appropriate documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)


# React Native

Configure React Native apps for Maestro testing on both Android and iOS platforms.

<figure><img src="/files/xx2tiTJ9467BEMWrb5Mi" alt=""><figcaption></figcaption></figure>

Maestro provides full support for React Native applications on both Android and iOS. By operating at the accessibility layer, Maestro enables cross-platform testing with a single test suite, requiring zero instrumentation or modifications to your JavaScript/TypeScript source code.

### Technical advantages

* **Zero Library Dependencies**: You don't need to install any npm packages (like Detox or Appium drivers) inside your app. Maestro tests the final bundled binary.
* **Fast Feedback Loops**: Maestro interacts directly with the native views rendered by React Native, providing faster execution than traditional bridge-based testing tools.
* **Expo and EAS Ready**: Full compatibility with Expo Go, development builds, and EAS Workflows for CI/CD integration.

### Element interaction strategies

In React Native, you can target components using visible text or unique identifiers.

#### **Interacting by Text**

Maestro can interact with any component that displays text content, such as `Button` titles or `Text` components.

If you have a `Go` button, for example:

```javascript
<Button
  title="Go"
  onPress={() => Alert.alert('Success!')}
/>
```

You can create a test instruction using that element text:

```yaml
- tapOn: "Go"
```

#### **Interacting by testID**

Using visible text is easy but brittle, tests break if you change the label or translate the app. The best practice is to use the `testID` property, which Maestro maps to a unique `id`.

If you assign a `testID` to any component (Button, View, TextInput, etc.), such as in the example:

```javascript
<TextInput
  placeholder="Username"
  testID="username_input"
/>
```

When creating the test, you can target the identifier directly. This remains stable even if the placeholder or language changes.

```yaml
- tapOn:
    id: "username_input"
- inputText: "maestro_user"
```

### Platform-specific tips

#### **Expo Go vs. Standalone Builds**

When testing with Expo Go, you cannot use `launchApp` with a custom `appId` because your app runs inside the Expo container. Instead, use the `openLink` command with your development URL.

```yaml
# For Expo Go development
- openLink: exp://127.0.0.1:19000
```

For EAS builds or standalone apps, use the standard `launchApp` with your Bundle ID or Package Name.

#### **Handling nested components (iOS)**

On iOS, React Native sometimes "swallows" touch events if components are deeply nested. If you can't tap an inner element, you can resolve these issues by enabling accessibility for the inner component and disabling it for the outer container.

Consider the following example where you need to tap on the nested text component. The `accessible` for the outer element was disabled and enabled for the inner element.

```javascript
<TouchableOpacity 
  style={{ borderWidth: 1, margin: 5, padding: 10, backgroundColor: '#ddd' }} 
   accessible={false}>
  <Text>This is the wrapper button </Text>
  <TouchableOpacity 
    style={{ backgroundColor: 'red', padding: 5, width: '50%', marginTop: 10 }} 
     accessible={true}>
    <Text>I'm a small button</Text>
  </TouchableOpacity>
</TouchableOpacity>
```

This way, you can target the inner element using the following command to tap on the Text component.

```yaml
- tapOn: "I'm a small button"
```

### Next Steps

If you don't know how to create tests with Maestro, access the [Quickstart](/get-started/quickstart) guide to get up and running in minutes.

To learn how to create tests, refer to the [Flows](https://docs.maestro.dev/maestro-flows/) documentation. If you want to explore Maestro solutions, consult the appropriate documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)


# Flutter

Test Flutter apps via the Semantics Tree. Use semanticLabel, Semantics widget, or identifier (Flutter 3.19+) instead of Keys for stable automation.

<figure><img src="/files/NJlBxI0H5eATCEB2nHrb" alt=""><figcaption></figcaption></figure>

Maestro treats Flutter as a first-class citizen, supporting both pure and hybrid ([add-to-app](https://docs.flutter.dev/add-to-app)) mobile applications. Unlike internal tools that inject Dart code, Maestro interacts with the device elements that Flutter exposes through its Semantics Tree, ensuring your tests reflect the actual experience of an end-user.

### Black-box approach

Maestro operates externally, orchestrating input events and validating outcomes based on the actual pixels and accessibility labels rendered on the device.

* **Zero Framework Dependencies**: No `pubspec.yaml` integration is required. Maestro tests the compiled APK or IPA directly.
* **Stability Across Upgrades**: Your tests remain stable across Flutter version updates because Maestro does not depend on the internal widget tree.
* **System-Level Automation**: Since Maestro lives at the OS level, it can handle system-level flows, like push notifications or permission dialogs, that in-app Dart frameworks cannot reach.

### Element interaction strategies

To interact with a widget, Maestro needs it to have semantic information. By default, widgets that display text (like `Text` or `TextField`) provide this implicitly.

#### **Interacting by semantics label (text)**

Maestro can target any widget that displays text content. For widgets without implicit text, such as an `Icon`, you can manually provide a `semanticLabel`.

To target Icon, for example, in this Dart code, you need to add a label to an Icon so Maestro can "see" it. For more details, see the official [Flutter Icon documentation](https://api.flutter.dev/flutter/widgets/Icon-class.html). In the following example, `semanticLabel` is being used to add a label.

```dart
FloatingActionButton(
  onPressed: _incrementCounter,
  child: Icon(Icons.add, semanticLabel: 'fabAddIcon'),
)
```

This way, you can easily identify the element when creating a flow:

```yaml
- tapOn: "fabAddIcon"
```

#### **The semantics widget**

You can wrap any layout component, such as a `Container` or `SizedBox`, with a [Semantics widget](https://api.flutter.dev/flutter/widgets/Semantics-class.html) to make it inspectable by Maestro.

When you add semantics to a layout, it allows you to perform actions such as tapping on a visual area that does not inherently contain text. The following example adds a label using semantics, allowing Maestro to target the element during testing.

```dart
Semantics(
  label: 'yellow_box',
  child: Container(color: Colors.yellow, width: 100, height: 100),
)
```

This way, you can easily interact with the element by referring to the label:

```
- tapOn: "yellow_box"
```

### Semantics identifiers

As your app adds multi-language support or A/B tests, text-based labels can become brittle. The best practice is to use Semantic Identifiers.

{% hint style="info" %}
This feature was [contributed by the Maestro team to Flutter](https://github.com/flutter/engine/pull/47961) and is available in Flutter 3.19+.
{% endhint %}

This pattern creates a permanent link between your Dart code and your YAML [Flow ](https://docs.maestro.dev/maestro-flows/)that never changes, even if you translate your app into 20 languages.

To use the pattern, the developer needs to assign a unique `identifier` that is invisible to the user but exposed to Maestro.

```dart
Semantics(
  identifier: 'login_button',
  child: ElevatedButton(onPressed: _login, child: Text('Sign In')),
)
```

This way, the test can target that identifier using the `id` [selector](/maestro-flows/flow-control-and-logic/how-to-use-selectors).

```yaml
- tapOn:
    id: "login_button"
```

### Why not use Flutter Keys?

Flutter [Keys](https://api.flutter.dev/flutter/foundation/Key-class.html) are designed for the Flutter engine to manage widget identity during state changes (like reordering a list). They are not exposed to the system's accessibility layer. Because Maestro lives outside the Flutter runtime, it cannot "see" Keys. Always use Semantics for automation.

### Known limitations

* **Flutter Desktop**: Maestro does not yet support Flutter for Desktop.
* **Flutter Web**: It is fully supported by Maestro. It works identically to standard [Web Testing](/get-started/supported-platform/web-browser). However, Flutter web renders to `<canvas>` and does **not** enable the DOM accessibility/semantics overlay by default. Maestro relies on this overlay to find elements, so you must explicitly enable it in your app's `main()`:

```dart
import 'package:flutter/rendering.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  SemanticsBinding.instance.ensureSemantics(); // required for Maestro on web
  runApp(const MyApp());
}
```

Without this call, Maestro cannot find any elements — `assertVisible` and `tapOn` will fail silently even though the app renders correctly. Once semantics are enabled, use Semantics widgets to make your web elements addressable.

### Next steps

If you don't know how to create tests with Maestro, access the [Quickstart](/get-started/quickstart) guide to get up and running in minutes.

To learn how to create tests, refer to the [Flows](https://docs.maestro.dev/maestro-flows/) documentation. If you want to explore Maestro solutions, consult the appropriate documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)


# Web Browsers

Test web applications in desktop browsers using Maestro's web automation capabilities.

<figure><img src="/files/5zqbYvfbKeRhuiutHTIs" alt=""><figcaption></figcaption></figure>

Maestro extends its one framework to rule them all philosophy to the desktop browser. By using the same declarative YAML syntax you use for mobile, you can automate web applications, enabling unified end-to-end testing across your entire product surface.

{% hint style="warning" %}
**Beta Status**

Web support is currently in Beta. It is functional for Chromium-based testing and is ideal for teams looking to consolidate their mobile and web automation into a single toolset.
{% endhint %}

### Technical approach

Maestro maintains its "Arm's Length" philosophy for web testing. Instead of directly manipulating the DOM or injecting JavaScript, Maestro interacts with the browser as a user would.

* **Unified Syntax**: The same commands like `tapOn`, `inputText`, and `assertVisible` work identically on Web as they do on Android and iOS.
* **Framework Agnostic**: Whether your site is built with React, Vue, Angular, or plain HTML, Maestro interacts with the rendered output.

### Execution Workflow

For web tests, you replace the `appId` with a `url`. Behind the scenes, Maestro treats the URL as the unique identifier for the application session.

```yaml
# example.yaml
url: https://maestro.mobile.dev
---
- launchApp
- tapOn: "Installing Maestro"
- assertVisible: "Installing the CLI"
```

On the first run, Maestro will automatically download a managed version of Chromium. Subsequent runs will launch instantly. To run the test with [Maestro CLI](https://docs.maestro.dev/maestro-cli/), just run:

```bash
maestro test example.yaml
```

### Maestro Studio for Web

[Maestro Studio](https://docs.maestro.dev/maestro-studio/) is fully compatible with web testing. It allows you to visually inspect web elements and generate YAML commands through a point-and-click interface.

### Platform specifics and tips

* **Flutter Web**: Just like Flutter Mobile, Flutter Web renders elements differently. You should use Semantics to make elements addressable. Refer to the [Flutter](https://docs.maestro.dev/platform-support/flutter) documentation for best practices.
* [**Selectors**](/maestro-flows/flow-control-and-logic/how-to-use-selectors): Maestro prioritizes user-visible text. For complex web apps, using unique text labels or stable accessibility attributes is recommended to ensure your tests remain "refactoring resilient."

### State Management

By default, browser state (cookies, local storage, etc.) is retained between flows in the same test run. State can be cleared by [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) by using the [clearState](/reference/commands-available/clearstate) command or the `clearState` option of the [launchApp](/reference/commands-available/launchapp) command.

### Known limitations

As this feature is in Beta, certain advanced browser configurations are not yet supported:

* **Browser Engines**: The current default and only supported browser is Chromium.
* **Localization**: The default locale is set to `en-US`.
* **Screen Dimensions**: Custom screen size and viewport configuration are currently preset.

### Next steps

If you don't know how to create tests with Maestro, access the [QuickStart](/get-started/quickstart) guide to get up and running in minutes.

To learn how to create tests, refer to the [Flows](https://docs.maestro.dev/maestro-flows/) documentation. If you want to explore Maestro solutions, consult the appropriate documentation:

* [Maestro Studio](https://docs.maestro.dev/maestro-studio/)
* [Maestro CLI](https://docs.maestro.dev/maestro-cli/)
* [Maestro Cloud](/maestro-cloud)


# Maestro MCP Server

Use Maestro's MCP server to let your coding agent write, run, and debug mobile and web UI tests.

Maestro implements the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs/getting-started/intro), enabling direct integration coding agents like Claude Code, Claude Desktop, Cursor, GitHub Copilot, Codex, Codex Desktop, Gemini, Windsurf, and JetBrains AI Assistant ([full list](https://modelcontextprotocol.io/clients)). For more information on MCP see [#what-is-mcp](#what-is-mcp "mention").

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCbCMt5C3rawmE9oIus7f%2Fuploads%2FmkNE7cEla6mI7Ikp0aH5%2Fmaestro-mcp.mp4?alt=media&token=b63eb192-ce07-4614-878c-69cf447eae2e>" %}

## How to Install Maestro MCP

{% stepper %}
{% step %}

### Prerequisites

* Install [Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli)
* Any coding agent that supports MCP ([full list](https://modelcontextprotocol.io/clients))
  {% endstep %}

{% step %}

### Install the Maestro MCP on your coding agent

<details>

<summary><i class="fa-claude">:claude:</i> Claude Code CLI</summary>

1. [Install the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli).
2. Run:

   ```bash
   claude mcp add maestro -- maestro mcp
   ```

{% hint style="info" %}
See the [Claude Code MCP docs](https://docs.claude.com/en/docs/claude-code/mcp) for scope options (`--scope user`, `--scope project`, etc.).
{% endhint %}

</details>

<details>

<summary><i class="fa-claude">:claude:</i> Claude Desktop</summary>

1. [Install the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli).
2. In Claude Desktop, open **Settings → Developer → Edit Config** and merge the following into `claude_desktop_config.json`:

   ```json
   {
       "mcpServers": {
           "maestro": {
               "command": "<full path to maestro binary>",
               "args": ["mcp"],
               "env": {
                   "JAVA_HOME": "<full JAVA_HOME directory>"
               }
           }
       }
   }
   ```

   The config file lives at:

   * **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
   * **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

   Claude Desktop launches from a minimal shell, so pass `JAVA_HOME` explicitly and use the full path to the `maestro` binary (run `which maestro` in your terminal to find it).

{% hint style="info" %}
The **Connectors** UI in Claude Desktop only supports remote MCP servers that use OAuth. Local stdio servers like Maestro must be added by editing `claude_desktop_config.json` directly.
{% endhint %}

</details>

<details>

<summary><i class="fa-chatgpt">:chatgpt:</i> Codex CLI</summary>

1. [Install the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli).
2. Run:

   ```bash
   codex mcp add maestro -- maestro mcp
   ```

   Or add to `~/.codex/config.toml` manually:

   ```toml
   [mcp_servers.maestro]
   command = "maestro"
   args = ["mcp"]
   ```

See the [Codex MCP docs](https://developers.openai.com/codex/mcp) and the [config reference](https://developers.openai.com/codex/config-reference).

</details>

<details>

<summary><i class="fa-chatgpt">:chatgpt:</i> Codex Desktop App</summary>

The Codex Desktop App shares its MCP config (`~/.codex/config.toml`) with the Codex CLI. If Maestro is already set up there, the Desktop App will pick it up automatically.

Otherwise:

1. [Install the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli).
2. In the Codex Desktop App, click the **Settings** button, then **Settings** again, and select **MCP servers** in the sidebar.
3. Click **Add server** and fill in the form:

   * **Name:** `maestro`
   * **Type:** `STDIO`
   * **Command to launch:** `maestro`
   * **Arguments:** `mcp`

   Then click **Save**.

See the [Codex MCP docs](https://developers.openai.com/codex/mcp) and the [config reference](https://developers.openai.com/codex/config-reference).

</details>

<details>

<summary><i class="fa-github">:github:</i> GitHub Copilot CLI</summary>

1. [Install the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli).
2. In a Copilot CLI session, run `/mcp add` and follow the interactive form with:

   * Name: `maestro`
   * Type: `local`
   * Command: `maestro mcp`

   Or add to `~/.copilot/mcp-config.json` manually:

   ```json
   {
       "mcpServers": {
           "maestro": {
               "type": "local",
               "command": "maestro",
               "args": ["mcp"]
           }
       }
   }
   ```

See the [Copilot CLI MCP docs](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers).

</details>

<details>

<summary><i class="fa-cursor">:cursor:</i> Cursor IDE</summary>

1. [Install the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli).
2. Install the MCP. The quickest option is the one-click button:

   [![Install MCP Server](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=maestro\&config=eyJjb21tYW5kIjoibWFlc3RybyBtY3AifQ%3D%3D)

   Or set it up manually: open **Cursor Settings → Tools & MCPs → Add Custom MCP**. Cursor opens `~/.cursor/mcp.json` for editing. Merge the following into it (or into a project-scoped `.cursor/mcp.json` in your repo root):

   ```json
   {
       "mcpServers": {
           "maestro": {
               "command": "maestro",
               "args": ["mcp"]
           }
       }
   }
   ```

See the [Cursor MCP docs](https://cursor.com/docs/context/mcp) for more.

</details>

<details>

<summary><i class="fa-cursor">:cursor:</i> Cursor CLI</summary>

The Cursor CLI shares its MCP config with the Cursor IDE. If Maestro is already set up in the IDE, there's nothing else to do.

Otherwise:

1. [Install the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli).
2. Add Maestro to `.cursor/mcp.json` in your project root:

   ```json
   {
       "mcpServers": {
           "maestro": {
               "command": "maestro",
               "args": ["mcp"]
           }
       }
   }
   ```

See the [Cursor CLI MCP docs](https://cursor.com/docs/cli/mcp).

</details>

<details>

<summary><i class="fa-google">:google:</i> Gemini CLI</summary>

1. [Install the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli).
2. Run:

   ```bash
   gemini mcp add maestro maestro mcp
   ```

   Or add to `~/.gemini/settings.json` manually:

   ```json
   {
       "mcpServers": {
           "maestro": {
               "command": "maestro",
               "args": ["mcp"]
           }
       }
   }
   ```

{% hint style="info" %}
See the [Gemini CLI MCP docs](https://geminicli.com/docs/tools/mcp-server/) for scope options (`-s user`, `-s project`, etc.).
{% endhint %}

</details>

<details>

<summary><i class="fa-rectangle-terminal">:rectangle-terminal:</i> Other Coding Agents / Manual Installation</summary>

If your agent isn't listed above, the generic stdio config is:

```json
{
    "mcpServers": {
        "maestro": {
            "command": "maestro",
            "args": ["mcp"]
        }
    }
}
```

{% hint style="info" %}
This assumes `maestro` is on your `PATH`. If it isn't, replace `"maestro"` with the full path to the Maestro CLI executable (e.g. `/opt/homebrew/bin/maestro`).
{% endhint %}

If you run into PATH or JAVA\_HOME issues, you can specify them explicitly:

```json
{
    "mcpServers": {
        "maestro": {
            "command": "<full path to maestro binary>",
            "args": ["mcp"],
            "env": {
                "JAVA_HOME": "<full JAVA_HOME directory>"
            }
        }
    }
}
```

MCP documentation for other IDEs:

* [Windsurf](https://docs.windsurf.com/windsurf/cascade/mcp)
* [JetBrains IDEs](https://www.jetbrains.com/help/ai-assistant/configure-an-mcp-server.html)
* [VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server)

</details>
{% endstep %}

{% step %}

### Restart and start building

Restart your coding agent, then let your agent handle checking its own work against a live mobile emulator or simulator. And when you're ready, generate repeatable end to end tests to ensure your feature doesn't break going forward.

<figure><img src="/files/NlAW7h4hjdhijbS3XjG1" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}

## How to Use the Maestro Viewer

Maestro MCP ships with Maestro Viewer - a web app that embeds an iOS simulator or Android emulator or physical device into your coding agent or browser. It shows the exact commands that Maestro MCP runs in real time, and enables full interactions with the embedded mobile device, allowing you to iterate end to end completely within the coding agent app of your choice.

The Maestro Viewer is exposed via the `open_maestro_viewer` MCP tool, so to open it simply ask your coding agent:

> open the maestro viewer

Here's an example of the Maestro Viewer running in the embedded Cursor IDE browser:

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FCbCMt5C3rawmE9oIus7f%2Fuploads%2F2eQdX7WY0CrBEJ8iIOr3%2Fmaestro-viewer.mp4?alt=media&token=a063e0d0-5e0c-41ab-8415-f253e96f64dc>" %}

## How to Update Maestro MCP

The Maestro MCP is bundled inside the Maestro CLI, so upgrading the CLI upgrades the MCP server. After upgrading, your agent needs to reload the MCP connection to pick up the new binary.

{% stepper %}
{% step %}

### Update the Maestro CLI

Maestro MCP ships as part of the Maestro CLI, so to update Maestro MCP:

* [Update the Maestro CLI](https://docs.maestro.dev/maestro-cli/how-to-install-maestro-cli/update-the-maestro-cli)
  {% endstep %}

{% step %}

### Reload the Maestro MCP in your coding agent

<details>

<summary><i class="fa-claude">:claude:</i> Claude Code CLI</summary>

Run `/mcp`, select **maestro**, then **Reconnect**.

</details>

<details>

<summary><i class="fa-claude">:claude:</i> Claude Desktop</summary>

Restart Claude Desktop.

</details>

<details>

<summary><i class="fa-chatgpt">:chatgpt:</i> Codex CLI</summary>

Restart the Codex CLI.

</details>

<details>

<summary><i class="fa-chatgpt">:chatgpt:</i> Codex Desktop App</summary>

Restart the Codex Desktop App.

</details>

<details>

<summary><i class="fa-github">:github:</i> GitHub Copilot CLI</summary>

Restart the Copilot CLI.

</details>

<details>

<summary><i class="fa-cursor">:cursor:</i> Cursor IDE</summary>

**Cursor Settings → Tools & MCPs** and toggle the Maestro server off and on, or restart Cursor.

</details>

<details>

<summary><i class="fa-cursor">:cursor:</i> Cursor CLI</summary>

Restart `cursor-agent`. No in-session reload command exists.

</details>

<details>

<summary><i class="fa-google">:google:</i> Gemini CLI</summary>

Restart the Gemini CLI.

</details>

<details>

<summary><i class="fa-rectangle-terminal">:rectangle-terminal:</i> Other Coding Agents</summary>

Restart your coding agent.

</details>
{% endstep %}
{% endstepper %}

## MCP tools

| Tool                   | Description                                                                                                                                                                                                                 |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list_devices`         | List all available local devices (Android emulators, iOS simulators, and Chromium for web) that can be targeted for automation.                                                                                             |
| `inspect_screen`       | Get the current screen's view hierarchy as compact JSON. Call this before targeting elements, and re-call after any UI change.                                                                                              |
| `take_screenshot`      | Take a screenshot of the current device screen. Useful when a visual helps disambiguate elements.                                                                                                                           |
| `run`                  | Execute Maestro flows. Accepts exactly one of `{ yaml }` (inline YAML, preferred for exploration), `{ files }` (specific `.yaml` files), or `{ dir, include_tags, exclude_tags }`. Syntax is validated as part of the call. |
| `cheat_sheet`          | Return the Maestro cheat sheet covering common commands, flow syntax, and best practices. The agent should call this before authoring unfamiliar commands.                                                                  |
| `list_cloud_devices`   | List valid `{ device_model, device_os }` pairs available on Maestro Cloud. Call before `run_on_cloud`. OS versions must be passed verbatim (e.g. `iOS-17-5`, `android-34`).                                                 |
| `run_on_cloud`         | Submit a flow (or folder of flows) to Maestro Cloud. Returns `upload_id`, `project_id`, and a dashboard URL immediately.                                                                                                    |
| `get_cloud_run_status` | Poll the status and per-flow results of a Cloud run. Poll every 60s until the status is terminal (`SUCCESS`, `ERROR`, `CANCELED`, `WARNING`).                                                                               |
| `open_maestro_viewer`  | Returns the running Viewer URL.                                                                                                                                                                                             |

{% hint style="info" %}
The `list_cloud_devices`, `run_on_cloud`, and `get_cloud_run_status` tools require Maestro Cloud authentication. Run `maestro login` (recommended) or set `MAESTRO_CLOUD_API_KEY` for non-interactive environments.
{% endhint %}

## What is MCP?

The Model Context Protocol is an open standard that provides a uniform interface for connecting Large Language Models to external data sources, tools, and services. MCP defines a client-server architecture where:

* **MCP Servers** expose resources like data sources, APIs, and tools through a standardized interface.
* **MCP Clients**, such as AI apps, consume these resources via the protocol.
* **Transport Layer** handles communication using JSON-RPC 2.0 over stdio, HTTP with Server-Sent Events (SSE), or WebSocket connections.

The Maestro MCP server ships inside the Maestro CLI and exposes Maestro's authoring, device, and Cloud capabilities to your agent over stdio.


# Maestro Studio overview

Maestro Studio is the desktop app for writing, running, and debugging mobile UI tests visually, using simple YAML files and a live connection to your device.

With Maestro Studio you can connect a device, build your tests by interacting directly with your app, and run them without switching tools.

### Learn how to use Maestro Studio

To start using Maestro Studio, Explore the following path:

1. Visit [Run tests with Maestro Studio](/maestro-studio/run-tests-with-maestro-studio) to install the Studio and create and run your first test.
2. Explore [Environments and variables](/maestro-studio/environments-and-variables) to learn how to handle dynamic data and secrets directly within the visual editor.
3. Use the [Run cloud tests from Maestro Studio](/maestro-studio/run-cloud-tests-from-maestro-studio) to run your tests on Maestro Cloud directly from Studio.

### What you can do in Studio

<table data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><strong>Write tests faster</strong></td><td>Type a hyphen in the YAML editor to see all available Maestro commands. Autocomplete suggests commands, arguments, and real selectors pulled from your connected device's screen, so you spend less time in the docs and fewer errors make it to run time.</td></tr><tr><td><strong>Visual test creation</strong></td><td>Right-click any element on your connected device to add YAML commands directly to your test. No need to manually look up element IDs or selectors. </td></tr><tr><td><strong>Run and debug locally</strong></td><td>Run your tests step by step and see exactly what happens on screen at each stage. Every run is recorded, so you can jump to any point and inspect what went wrong.</td></tr></tbody></table>

### Maestro Studio vs. Maestro CLI

Maestro is also available via [CLI](/maestro-cli). However, while the Maestro CLI is the core engine for executing tests and CI/CD integration, the Maestro Studio  is a specialized layer designed for development and debugging.

| **Feature**       | **Maestro Studio**                                                | **Maestro CLI**                               |
| ----------------- | ----------------------------------------------------------------- | --------------------------------------------- |
| Primary Interface | Visual GUI / Desktop App                                          | Terminal / Command Line                       |
| Core Purpose      | Writing, running, and debugging tests visually.                   | Executing test suites in CI/CD.               |
| Setup Required    | Requires Android SDK and/or Xcode to run against virtual devices. | Requires Java 17+, Android SDK, and Xcode.    |
| Inspection Tool   | Point-and-click interface to see what Maestro sees.               | Uses `maestro hierarchy` for terminal output. |


# Run tests with Maestro Studio

Build and run mobile tests visually with Maestro Studio. Install the app, create tests via screen inspection, and automate without writing code from scratch.

Maestro Studio is a visual desktop app that simplifies mobile test automation. This guide covers how to install the application and use its interactive tools to build a contact creation test without writing any code.

### Prerequisites&#x20;

Before building your test, ensure you have a running Android emulator. Access the [QuickStart](/get-started/quickstart) for further guidance on setting up your virtual environment.&#x20;

{% stepper %}
{% step %}

### Installation

Get started by downloading the installer for your specific operating system:

* **Windows**: Download [MaestroStudio.exe](https://studio.maestro.dev/MaestroStudio.exe) and follow the setup wizard.
* **macOS**: Download [MaestroStudio.dmg](https://studio.maestro.dev/MaestroStudio.dmg) and drag the icon into your Applications folder.
* **Linux**: Download [MaestroStudio.AppImage](https://studio.maestro.dev/MaestroStudio.AppImage), make it executable with `chmod +x`, and run it.
  {% endstep %}

{% step %}

### Initial setup

1. Launch Maestro Studio.
2. Click **New workspace** to select a folder on your machine where your test files will be saved.&#x20;

<figure><img src="/files/g5Uwc6PH0SdffJyZAOVk" alt=""><figcaption></figcaption></figure>

3. Click on **Select device** at the top left and select your active virtual device from the list.

<figure><img src="/files/bHD9s3EDSdyUJtMNijU9" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Create the test file

1. Click **New File.**
2. Enter a name for your test file, for example `my_test.yaml`.
3. Select your app from the dropdown menu.
4. Click **Create Test**.

{% hint style="info" %}
Tags are used to control which tests run. For more information, access the following pages:

* [Environments and variables](/maestro-studio/environments-and-variables)
* [Run cloud tests from Maestro Studio](/maestro-studio/run-cloud-tests-from-maestro-studio)
  {% endhint %}

<figure><img src="/files/zXalM9wIfUrBgYJuqjeQ" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Test the app initialization

Before you begin building the test, run the app initialization. Click **Run Test** and observe the app initializing with a clear state.

<figure><img src="/files/8KR3M3SbLyxWxUPXPeWU" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Build your test

The fastest way to build a test is to **right-click directly on any element** in your app. A menu appears with the available Maestro commands for that element. Click the one you want, and it gets added to your test automatically.

<figure><img src="/files/8jObHZdZpaKEiSJM4MYj" alt=""><figcaption></figcaption></figure>

You can also type a hyphen in the editor to see all available Maestro commands.

<figure><img src="/files/OdXX5Bk0sAu4H8oS1g6p" alt=""><figcaption></figcaption></figure>

Autocomplete suggests commands, arguments, and real selectors from your connected device's screen, so you spend less time looking up syntax.

<figure><img src="/files/MiZjSViQ2T6ih5xeXLHo" alt=""><figcaption></figcaption></figure>

{% endstep %}

{% step %}

### Review and run the test

Once your first test is ready, click **Run Test** to watch Maestro Studio execute these steps automatically on your device.

<figure><img src="/files/pMMU5gjW1tirxc86NHGi" alt=""><figcaption></figcaption></figure>

After a successful run, the recording file will be available in the `.maestro` directory.
{% endstep %}
{% endstepper %}

### Next steps

Now that you have created a test using the interactive features of Maestro Studio, you can explore more advanced capabilities:

* [Environments and variables](/maestro-studio/environments-and-variables): Learn how to pass dynamic data like names or phone numbers using variables.
* [Run cloud tests from Maestro Studio](/maestro-studio/run-cloud-tests-from-maestro-studio): Learn how to execute this test on using Maestro Cloud.

To learn more about test structure and advanced logic, visit the [Maestro Flows documentation](/maestro-flows).


# Environments and variables

Manage dynamic test configurations in Maestro Studio. Use Environment Variables and Tags to run tests across different platforms and environments.

Maestro Studio allows you to manage dynamic configurations through Environments. This is essential for running the same tests across different app variants, server environments, or localized settings without hardcoding values into your tests.

### Organize your environments

To keep your tests maintainable, it is recommended to separate your environment configurations. Common use cases for different environments include:

* **Platform Variants**: Running tests against the same app on multiple platforms with different properties, such as a unique `appId` for Android vs. iOS.
* **Host Environments**: Testing against different backends (e.g., Staging vs. Production) using different credentials or API endpoints.
* **Whitelabel Variants**: Running tests against variants of a whitelabelled application that require different test data or theme settings.

### Add environments

Environments in Maestro enable you to define two key types of configurations:

* **Environment Variables**: Key-value pairs used to inject dynamic data into your tests (e.g., `BASE_URL`, `USER_EMAIL`).
* **Configure Tags**: Specify the tags to be included and excluded when running the tests. This allows you to create specific labels used to filter which tests are executed during a run (e.g., `smoke`, `regression`).

When you start using Maestro Studio, you have the default environment, called `None`. But, to better organize your tests, it's recommended to have environments and environment variables separated.

To add a new environment, follow these steps:

1. Open the Maestro Studio.
2. In Maestro Studio, click the **Env** icon.
3. Click on **Manage Environments**, then click **Create**.

<figure><img src="/files/azvY5WUxhm0z5vqr15Mi" alt=""><figcaption></figcaption></figure>

4. Specify the **Included** and **Excluded** tags for this environment.
5. Add your variables as **Key-Value** pairs.

{% hint style="success" %}

#### Example

Imagine you have created a set of generic test to be used across all available operating systems. To ensure the correct tests run for a specific platform, you can create a dedicated Android Environment:

* **Filter with Tags**: Add `Android` to **includedTags**. This ensures that only tests applicable to Android are executed. Alternatively, add `iOS` and `Web` to **excludedTags.**
* **Define Variables**: Specify an `appId` variable containing the unique package name for your Android app (e.g., `com.example.android`).
  {% endhint %}

<figure><img src="/files/TY4oYTy8lL5zuPhgIW6h" alt=""><figcaption></figcaption></figure>

### Using variables in your Tests

Environment variables are available throughout your test wherever a JavaScript expression is supported. You are not limited to a specific `env` section; you can use them directly in UI commands or logic gates.

You can use the variables as direct iputs, using `${variableName}` to inject values into text fields:

```yaml
- inputText: ${email}
```

You can also use the variables to control test execution based on the environment:

```yaml
- runFlow:
    when:
      true: ${APP_ID == "com.example.android"}
    commands:
      - tapOn: "Start"
```

### Next steps

To take the next step with Maestro Studio, learn how to run your tests using the Maestro Cloud solution.

If you want to deepen your understanding of how Maestro works, explore the related documentation:

* [**Parameters and constants**](/maestro-flows/flow-control-and-logic/parameters-and-constants)**:** Pass dynamic values to tests using parameters and inline constants.
* [**Test discovery and tags**](/maestro-flows/workspace-management/test-discovery-and-tags)**:** Organize tests with tags and control which tests run using include/exclude filters.
* [**Conditions**](/maestro-flows/flow-control-and-logic/conditions): Execute commands conditionally based on visibility, platform, or custom expressions.


# Run cloud tests from Maestro Studio

Execute mobile tests on cloud devices directly from Maestro Studio. Select apps, pick device models, and monitor live results from the editor.

Maestro Studio provides a seamless way to execute your mobile tests on cloud infrastructure without leaving your development environment. This allows you to test on various device models and OS versions while keeping your local machine free for other tasks.

### Prerequisites&#x20;

You need an account to take advantage of Maestro Cloud solution. Access [Maestro Cloud Plan](https://signin.maestro.dev/sign-up) for more information.

{% stepper %}
{% step %}

### Trigger a cloud runs

After creating your tests, you can initiate a [cloud](/maestro-cloud) execution directly from the Studio interface in two ways:

1. **Run All Tests**: Click on the **Cloud tab**, and then click on the **Run All Tests** button to execute all root-level tests in your workspace.
2. **Run a Single Test**: Open a specific test file in the editor and click on the **Cloud** tab, then click on **Run Test**.

<figure><img src="/files/6tRKyOtUP3P0RFRzyJvq" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Configure the run

Clicking on **Cloud,** and then on **Run Test**, opens a configuration modal. You must complete the following required settings before launching the run:

* **Device**: Select the specific device model and API level for your test (e.g., Pixel 6 - API 34).
* **App Selection**: Select your application. If this is your first time testing the app, click the app selector and choose **Upload App File** to pick your `.apk` or `.app` bundle.

<figure><img src="/files/lQvstqP8Za75dabAAGwi" alt=""><figcaption></figcaption></figure>

For more advanced testing scenarios, you can expand **More Options** to select:

* Environment
* Project
* Device Locale

You can select an existing Environment, or create a new one. To create a new one, click **Manage Environments** to define tags (for filtering tests) and environment variables.

{% hint style="info" %}
&#x20;For further information about tags, access [Test discovery and tags](/maestro-flows/workspace-management/test-discovery-and-tags).

For more information about variables, access [Parameters and constants](/maestro-flows/flow-control-and-logic/parameters-and-constants).
{% endhint %}

If you manage multiple Maestro projects, select the specific project to receive these test results.&#x20;

Use the **Device Locale** option to manually set the locale for the cloud device (e.g., `en_US` or `de_DE`). For more information, access [Test in different locales](/maestro-flows/flow-control-and-logic/test-in-different-locales).
{% endstep %}

{% step %}

### Execute and monitor

After defining your settings, click **Run Test**. Maestro Studio will package your tests and app binary, then upload them to the cloud infrastructure.

You can monitor the execution directly within the Maestro Studio output terminal:

* **Real-time Updates**: The output terminal displays the upload status and the live progress of each test.
* **Success and Failure**: A summary shows how many tests passed or failed. If a test fails, the specific error or failed assertion is displayed directly in the output terminal.

To deep-dive into a specific run, click the **View on Maestro Cloud** button in the output terminal. This opens the Maestro Cloud Console, where you can:

* Inspect every individual step of the test.
* Review the screen recording of the execution.
* Access detailed logs and UI hierarchy data for debugging.

<figure><img src="/files/DWPwsZYyThAleToS55rh" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Maestro CLI overview

Open-source CLI for mobile and web UI testing. Run tests, manage devices, use Continuous Mode, and scale to Maestro Cloud from your terminal.

The Maestro CLI is an open-source, single-binary framework designed for end-to-end mobile and web UI testing. It allows developers and testers to define user journeys, referred to as [Flows](https://docs.maestro.dev/maestro-flows/), using a simple, declarative YAML syntax.

### Learn how to use Maestro CLI

To get up and running with the Maestro CLI, follow these foundational steps:

1. [Installation](/maestro-cli/how-to-install-maestro-cli): Step-by-step instructions for installing the Maestro CLI on macOS, Windows, WSL, and Linux.
2. [Run your first test](/maestro-cli/run-your-first-test-with-the-maestro-cli): A hands-on guide to writing and executing your first Flow on an Android emulator.
3. [Maestro CLI reference](/maestro-cli/maestro-cli-commands-and-options): A complete list of all global options and subcommands.
4. [Environment variables](/maestro-cli/environment-variables): A list of environment variables you can set to change the Maestro behaviour.

{% hint style="info" %}
If you are using WSL and encounter issues running tests, see the [Troubleshooting](broken://pages/5QVQrTAL6a5OzHx1rpyp) page for additional guidance.
{% endhint %}

### Maestro CLI features

The CLI serves as the central engine for multiple automation workflows. Whether you prefer using Maestro Studio or your own IDE, the CLI handles the execution against emulators, simulators or physical devices. With Maestro CLI you can:

<table data-view="cards"><thead><tr><th></th><th></th></tr></thead><tbody><tr><td><strong>Run Local Tests</strong></td><td>Execute tests against a running emulator/simulator or physical device using the command <code>maestro test flow.yaml</code>.</td></tr><tr><td><strong>Continuous Development</strong></td><td>The Continuous Mode (<code>maestro test -c</code>) monitors your YAML test files for changes and automatically restarts the test upon saving.</td></tr><tr><td><strong>Device Management</strong></td><td>Create and launch specific emulator or simulator configurations with <code>maestro start-device</code>.</td></tr><tr><td><strong>Scale to the Cloud</strong></td><td>Upload your Flows to Maestro Cloud to run tests at scale across a variety of managed device configurations.</td></tr><tr><td><strong>Debug Tools</strong></td><td>Identification of selectors is simplified with commands like <code>maestro hierarchy</code>, which prints the current app’s view hierarchy directly to the terminal.</td></tr></tbody></table>

### Core Features

Maestro is built on the philosophy of "embracing instability," providing a suite of features that move beyond traditional automation tools:

<table data-header-hidden><thead><tr><th width="229.5555419921875"></th><th></th></tr></thead><tbody><tr><td><strong>Feature</strong></td><td><strong>Description</strong></td></tr><tr><td>Built-in Tolerance</td><td>Automatically handles network delays and UI flakiness by waiting for the screen to "settle" before proceeding.</td></tr><tr><td>Extensive Commands</td><td>A library of commands for UI interactions (<code>tapOn</code>, <code>swipe</code>), navigation (<code>launchApp</code>, <code>openLink</code>), and device control.</td></tr><tr><td>JavaScript Integration</td><td>Run JavaScript expressions directly from YAML to manage complex data or perform HTTP requests</td></tr><tr><td>Modularity</td><td>Create composable subflows that can be reused across multiple tests to keep your automation suite DRY (Don't Repeat Yourself).</td></tr><tr><td>Flow Recording</td><td>The <code>maestro record</code> command stitches screen recordings and test output into a shareable MP4 video.</td></tr><tr><td>AI Analysis</td><td>[Beta] Generate LLM-based analysis reports for UI and internationalization issues.</td></tr></tbody></table>

### Next Step

Learn how to get started with Maestro accessing the [How to install Maestro CLI](/maestro-cli/how-to-install-maestro-cli) guide.


# How to install Maestro CLI

Step-by-step installation guide for Maestro CLI on macOS, Windows, and Linux.

Maestro also provides you with the option to use the CLI by terminal. This document will cover the installation of Maestro CLI on the three operational system supported: macOS, Windows and Linux.

### Prerequisites

To install the Maestro CLI, you need the following:

* Java version 17 or higher.

{% hint style="warning" %}

* Ensure that the `JAVA_HOME` environment variable points to your Java 17+ installation.
* You can install Java using [Oracle JDK](https://www.oracle.com/java/technologies/downloads/), [Temurin JDK](https://adoptium.net/en-GB/temurin/releases) or [SDKMAN](https://sdkman.io/).
* To verify the version of your Java installation, run `java -version`.
  {% endhint %}

### Installation

You can install the Maestro CLI on Windows, macOS and Linux.

{% tabs %}
{% tab title="macOS" %}
To install on macOS, you can either run:

```bash
curl -fsSL "https://get.maestro.mobile.dev" | bash
```

Or you can use `homebrew` by running the commands:

```bash
brew tap mobile-dev-inc/tap
brew trust --formula mobile-dev-inc/tap/maestro
brew install mobile-dev-inc/tap/maestro
```

Run `maestro --help` to verify that the Maestro CLI is working properly.

{% hint style="warning" %}
For macOS, ensure that your have also the last version os [XCode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) and [XCode Command Line Tools](https://developer.apple.com/documentation/xcode/installing-the-command-line-tools/) installed.
{% endhint %}
{% endtab %}

{% tab title="Windows" %}
To install on Windows:

Install using the [releases page on GitHub](https://github.com/mobile-dev-inc/Maestro/releases). To install using the release package, follow these steps:

1. Download the latest [maestro.zip](https://github.com/mobile-dev-inc/maestro/releases/latest/download/maestro.zip).
2. Extract the content to a stable location (e.g., `C:maestro`).
3. Update your `PATH` to add the Maestro CLI environment variable. Run the following in PowerShell to add the Maestro `bin` folder to your environment variables:<br>

   ```powershell
   setx PATH "%PATH%;C:\maestro\bin"
   ```
4. Restart your terminal to apply changes.

Run `maestro --help` to verify that the Maestro CLI is working properly.
{% endtab %}

{% tab title="Windows (WSL)" %}
Installing Maestro in WSL2 allows you to use a Linux environment while using Android emulators running on your Windows host.

{% hint style="warning" %}

#### Use the Windows (WSL) option only if it is strictly necessary.

Although it is possible to run the Maestro CLI on WSL, Maestro recommends using one of the other supported environments (macOS, Windows, or Linux). The WSL setup requires advanced port configuration, which can introduce issues when testing your app.
{% endhint %}

{% hint style="success" %}
If you encounter problems running your tests using Maestro CLI on WSL, check the troubleshooting guide.
{% endhint %}

#### 1. Install Java and Maestro

First, ensure you have Java 17+ installed.

```bash
sudo apt update
sudo apt install openjdk-17-jdk
```

Update your environment variables to ensure `JAVA_HOME` is set and Maestro is in your `PATH`. Add the following to your `~/.bashrc` (or `~/.zshrc`):

```bash
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
export PATH=$JAVA_HOME/bin:$PATH
export PATH=$PATH:$HOME/.maestro/bin
```

Reload your configuration:

```bash
source ~/.bashrc
```

Now, install Maestro:

```bash
curl -fsSL "https://get.maestro.mobile.dev" | bash
```

#### 2. Setup Android environment

Since you cannot run the Android Studio GUI directly in WSL easily, you need to set up the Android command-line tools manually. This enables you to access and control the Android emulator running on Windows.

1. Create the Android directory:

   ```bash
   mkdir -p $HOME/Android/cmdline-tools
   ```
2. Download the latest [command line tools for Linux](https://developer.android.com/studio#command-tools) (zip/targz) and unzip it. Make sure you update the URL with the latest version.

   ```bash
   cd $HOME/Android/cmdline-tools
   # Replace with the actual URL for the latest version
   wget https://dl.google.com/android/repository/commandlinetools-linux-14742923_latest.zip -O cmdline-tools.zip
   unzip cmdline-tools.zip
   mv cmdline-tools latest
   rm cmdline-tools.zip
   ```
3. Configure the environment by adding the following to your `~/.bashrc`:

   ```bash
   # --- Android PATH ---
   export ANDROID_HOME=$HOME/Android
   export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin
   export PATH=$PATH:$ANDROID_HOME/platform-tools
   ```
4. Reload the terminal running:

   ```bash
   source ~/.bashrc.
   ```
5. Install the platform tools:

   ```bash
   sdkmanager --install "platform-tools"
   ```

#### 3. Connect to Windows Emulator

Android Emulators run on the Windows host. You need to bridge ADB from WSL to Windows.

**On Windows (PowerShell):**

Start the ADB server to allow external connections.&#x20;

```powershell
adb -a -P 5037 nodaemon server
```

If successful, the command will not show any output and will just sit there. This is normal! Do not close this PowerShell window, as it keeps the connection alive.

After starting the server and proceeding to WSL, you must have an active Android device. To accomplish this:

1. Open Android Studio on Windows.
2. Launch a Virtual Device using the Emulator.

{% hint style="info" %}

#### `adb` is not recognized?

If you see an error saying `The term 'adb' is not recognized`, it means the Android SDK platform-tools are not in your Windows PATH.

**Add to PATH via PowerShell (Recommended)**

Run this command in PowerShell to permanently add the path for your user:

```powershell
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";$env:LOCALAPPDATA\Android\Sdk\platform-tools", "User")
```

Restart your PowerShell terminal after running this command.

**Manual method**

1. Search for **Edit environment variables for your account** in Windows Search.
2. Edit the `Path` variable.
3. Click **New** and paste: `%LOCALAPPDATA%\Android\Sdk\platform-tools`.
   {% endhint %}

{% hint style="info" %}

#### Error: could not install `smartsocket` listener?

If you see an error like `cannot bind to 0.0.0.0:5037`, it means another ADB (e.g., from Android Studio) is already running. To solve this problem, do the following:

1. Close Android Studio.
2. Kill the existing process running:

   ```powershell
   taskkill /F /IM adb.exe
   ```
3. Try to start the ADB server again.
   {% endhint %}

**On WSL:**

Configure ADB to connect to the Windows host IP. Replace `<WINDOWS_IP>` with your actual Windows IP address.

```bash
adb kill-server
export ADB_SERVER_SOCKET=tcp:<WINDOWS_IP>:5037
adb devices
```

{% hint style="success" %}
To identify the `WINDOWS_IP`, run the following command in yout WSL terminal:

```bash
ip route show | grep default | awk '{print $3}'
```

{% endhint %}

You should see your emulator listed, indicating your connection is working.&#x20;

#### 4. Running Maestro

When running Maestro commands, use the `--host` flag to point to your Windows machine:

```bash
maestro --host <WINDOWS_IP> test flow.yaml
```

{% endtab %}

{% tab title="Linux" %}
To install on Linux, you can use the `cURL` command on any distribution:

```bash
curl -fsSL "https://get.maestro.mobile.dev" | bash
```

Run `maestro --help` to verify that the Maestro CLI is working properly.
{% endtab %}
{% endtabs %}

### Next steps

Now that the Maestro CLI is installed on your system, it’s time to run your first app test. Follow the [Run your first test with the Maestro CLI](/maestro-cli/run-your-first-test-with-the-maestro-cli) guide to get started.

If you need to update the CLI or install a specific Maestro CLI version, check the [Update the Maestro CLI](/maestro-cli/how-to-install-maestro-cli/update-the-maestro-cli) guide.


# Update the Maestro CLI

Update Maestro CLI via curl, Homebrew, or manual download. Set MAESTRO\_VERSION to install a specific version for team or CI/CD compatibility.

Keeping your Maestro CLI up to date ensures you have access to the latest features, command improvements, and security patches. Depending on your original installation method, follow the appropriate steps below.

### Standard update

If you originally used the installation script to set up Maestro on macOS, Linux, or WSL2, you can upgrade to the latest version by running the script again:

```bash
curl -fsSL "https://get.maestro.mobile.dev" | bash
```

This command automatically fetches the most recent release and replaces your existing binary.

After running the update, verify that you are running the expected version by checking the CLI metadata:

```bash
maestro --version
```

### Alternative update methods

If you installed Maestro using a package manager or manual download, use these specific commands:

#### **Homebrew (macOS)**

If you used the Homebrew tap, update using the standard brew commands:

```bash
brew update
brew upgrade mobile-dev-inc/tap/maestro
```

#### Manual update (Windows)

For native Windows installations not using `curl`:

1. Download the latest [maestro.zip](https://github.com/mobile-dev-inc/maestro/releases/latest/download/maestro.zip) package.
2. Extract the contents into your existing Maestro folder (e.g., `C:\maestro`).
3. Replace all existing files to update the binary.

### Install a specific version

There are scenarios where you may need to downgrade or lock your suite to a specific version for compatibility across a team or CI/CD environment.

To install a specific version, set the `MAESTRO_VERSION` environment variable before running the update script:

```
export MAESTRO_VERSION={version}; curl -Ls "https://get.maestro.mobile.dev" | bash
```

{% hint style="info" %}
You can find a full list of valid versions on the [GitHub releases page](https://github.com/mobile-dev-inc/maestro/releases).
{% endhint %}

You have to pass the version of Maestro CLI you want to install. To do this, replace the `{version}` parameter with your desired Maestro version.

For example, to install version 1.39.0 of Maestro CLI:

```bash
export MAESTRO_VERSION=1.39.0; curl -Ls "https://get.maestro.mobile.dev" | bash
```


# Run your first test with the Maestro CLI

Write and execute your first Maestro Flow using the CLI. Automate the Contacts app on an Android emulator with YAML commands and recording.

In this tutorial, you will write and execute your first Maestro Flow using the CLI. You will create a test that automates the process of adding a new contact to an Android device using the native Contacts app.

### Prerequisites

Ensure you have the following ready before starting:

* **Maestro CLI**: Installed and configured on your local machine. If not yet installed, follow the [How to install Maestro CLI](/maestro-cli/how-to-install-maestro-cli) guide.
* **Android Studio**: Used to manage and launch virtual devices. See the [QuickStart](/get-started/quickstart) guide.

{% stepper %}
{% step %}

### Start the Android emulator

Maestro requires an active device or emulator to interact with the application UI. This example uses Android Emulator to run an emulated Android device:

1. Open **Android Studio**.
2. Navigate to the **Virtual Device Manager**.
3. Launch a virtual device (e.g., Pixel 8 or similar).
4. Wait for the device to appear in your home screen.

<figure><img src="/files/jzLBBSDznaeTvYSxKQDJ" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

### Create the Flow file

A Flow is a YAML file containing the commands Maestro executes. For this tutorial, we will use the system's default Contacts app (`com.google.android.contacts`), which is pre-installed on standard Android emulators:

1. Create a new directory for your test and navigate into it.
2. Create a file named `contacts.yaml`.
3. Copy and paste the following content:

```yaml
appId: com.google.android.contacts
---
- launchApp:
    clearState: true              # Resets the app to a fresh state before starting
- startRecording: recording       # Starts capturing a video of the execution
- tapOn: "Allow"                  # Handles system permission dialog if it appears
- tapOn: Create contact
- tapOn: First name
- inputText: John
- tapOn: Last name
- inputText: Doe
- tapOn: Company
- inputText: Maestro
- tapOn: "+1"
- inputText: 111-111-1111
- tapOn: Save
- back                            # Returns to the main contact list
- stopRecording                   # Saves the video file
```

{% hint style="info" %}
If you don't know to create and structure Flows, access the [specific documentation](/maestro-flows).
{% endhint %}

{% hint style="info" %}

#### Download and use Maestro samples

The Maestro CLI provides the `download-samples` command, which lets you download a curated collection of Flow files to help you learn Maestro. Use these samples to explore examples and understand how different Maestro features work in practice.

To use this command, run `maestro doenload-samples`.
{% endhint %}
{% endstep %}

{% step %}

### Run the Flow

With the emulator running and your YAML file ready, you can now execute the test:

1. Open your terminal.
2. Run the following command:

```bash
maestro test contacts.yaml
```

{% hint style="info" %}

### CLI options and commands

To see all the options and commands available when using the Maestro CLI, [access the Maestro CLI documentation](/maestro-cli/maestro-cli-commands-and-options).
{% endhint %}

{% hint style="success" %}

#### Troubleshooting: Connection timeouts

If your CI runner fails to start the Maestro driver within the default timeframe, you may see a timeout error.

The default timeout is 15 seconds (15000 ms) for Android and 120 seconds (120000 ms) for iOS.

You can extend this by setting a custom millisecond value in your pipeline environment. Here's an example to increase the timeout to 3 minutes (180000 ms):

```bash
export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000
```

{% endhint %}

Maestro will connect to the emulator and execute the steps sequentially. You will see a live progress report in your terminal.

<figure><img src="/files/VHQP2sO4vbmFe6aUtmgV" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}

#### What happens during execution:

1. Maestro begins capturing the screen.
2. The Contacts app opens and resets any existing state.
3. Maestro identifies fields by their text or accessibility labels and inputs names and phone numbers.
4. The contact is saved, and the app navigates back to the list view.
5. The recording stops, and a file named `recording.mp4` is saved to your directory.
   {% endhint %}
   {% endstep %}
   {% endstepper %}

### Final Outcome

Once the test completes, check your folder for the `recording.mp4` file. It should display the automated process exactly as seen in the example below:

<figure><img src="/files/g5psPJnspxsL9jR31jZi" alt=""><figcaption></figcaption></figure>

### Next steps

Now that you have executed your first Flow, you are ready to explore the deeper capabilities of Maestro:

* [Flows](/maestro-flows/flow-control-and-logic/flow-control-and-logic-overview): Learn how to build resilient, intelligent journeys by utilizing modular subflows, conditional execution, and repetitive loops to handle complex app states.
* [Selectors](/maestro-flows/flow-control-and-logic/how-to-use-selectors): Learn how Maestro identify UI elements when testing your app.
* [JavaScript](/maestro-flows/javascript/javascript-overview): Learn how to use JavaScript to extend your YAML logic.
* [Workspace management](/maestro-flows/workspace-management/workspace-management-overview): Learn how to organize your test suite for larger projects.


# Maestro CLI commands and options

Complete reference for Maestro CLI global options and subcommands including test, cloud, record, start-device, and their specific options.

This document lists all the options and subcommands you can pass to the Maestro CLI.

### Usage

To use the subcommands and/or options with the Maestro CLI, follow this pattern:

```bash
maestro [options] [subcommand] [subcommand options]
```

For example, to run a test with verbose logging and a specific tag:

```bash
 maestro --verbose test my-flow.yaml --include-tags=smoke
```

### Options

You can pass these global options with the Maestro CLI:

| Flag                          | Description                                                                         |
| ----------------------------- | ----------------------------------------------------------------------------------- |
| `--[no-]ansi`, `--[no-]color` | Enable or disable color and ANSI output.                                            |
| `--udid`, `--device`          | Pass the device ID to run the test on. Usage: `--device=00008030-001C195E0E88002E`. |
| `-h`, `--help`                | Display the help message for the flag or subcommand.                                |
| `-p`, `--platform`            | Select the platform to run the test on. Usage: `--platform=ios`.                    |
| `--verbose`                   | Enable verbose mode.                                                                |
| `-v`, `--version`             | Display the version of the Maestro CLI you have installed.                          |

### Subcommands

You can pass these subcommands with the Maestro CLI:

| Subcommand           | Description                                                                          |
| -------------------- | ------------------------------------------------------------------------------------ |
| `bugreport`          | Send a bug report.                                                                   |
| `cloud`              | Upload your Flows to Maestro Cloud.                                                  |
| `download-samples`   | Download sample Flows and sample apps for running with the Maestro CLI.              |
| `driver-setup`       | Setup Maestro drivers for your device.                                               |
| `list-cloud-devices` | List devices available on Maestro Cloud, grouped by platform.                        |
| `list-devices`       | List local devices available, grouped by platform.                                   |
| `login`              | Login into Maestro Cloud.                                                            |
| `logout`             | Logout from Maestro Cloud.                                                           |
| `mcp`                | Start the Maestro Model Context Protocol (MCP).                                      |
| `record`             | Record your Flows.                                                                   |
| `start-device`       | Start an iOS Simulator or an Android Emulator.                                       |
| `test`               | Test a Flow or a selected set of Flows on a local iOS Simulator or Android Emulator. |

{% hint style="info" %}
**`maestro chat` (MaestroGPT) has been discontinued.**

Use [Maestro MCP](https://docs.maestro.dev/get-started/maestro-mcp) instead — it connects your coding agent (Claude Code, Cursor, Codex, and others) directly to Maestro so it can write, run, and debug your flows, not just answer questions about them.
{% endhint %}

### Subcommand Options

Each subcommand of Maestro CLI supports specific options.

#### `test`

Run tests on your local device or emulator.

| Option                              | Description                                                                                                  |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `--analyze`                         | \[Beta] Enhance the test output analysis with AI Insights.                                                   |
| `--api-key=<apiKey>`                | \[Beta] API key.                                                                                             |
| `--api-url=<apiUrl>`                | \[Beta] API base URL.                                                                                        |
| `--[no-]ansi`, `--[no-]color`       | Enable / disable colors and ANSI output.                                                                     |
| `--config=<configFile>`             | Optional YAML configuration file for the workspace.                                                          |
| `-c`, `--continuous`                | Run tests in continuous mode.                                                                                |
| `--debug-output=<debugOutput>`      | Configures the debug output in this path, instead of default.                                                |
| `-e`, `--env=<String=String>`       | Set environment variables.                                                                                   |
| `--exclude-tags=<excludeTags>`      | List of tags that will remove the Flows containing the provided tags.                                        |
| `--flatten-debug-output`            | All file outputs from the test case are created in the folder without subfolders or timestamps for each run. |
| `--format=<format>`                 | Test report format (default=NOOP). Options: `JUNIT`, `HTML`, `NOOP`.                                         |
| `-h`, `--help`                      | Display help message.                                                                                        |
| `--headless`                        | (Web only) Run the tests in headless mode.                                                                   |
| `--include-tags=<includeTags>`      | List of tags. Only flows containing these tags will be run.                                                  |
| `--output=<output>`                 | Specify the output destination.                                                                              |
| `--screen-size=<width>x<height>`    | Specify the dimensions of the headless browser, e.g. 1920x1080. Web only.                                    |
| `-s`, `--shards=<count>`            | Number of parallel shards to distribute tests across.                                                        |
| `--shard-all=<shardAll>`            | Run all the tests across N connected devices.                                                                |
| `--shard-split=<shardSplit>`        | Run the tests across N connected devices, splitting the tests evenly across them.                            |
| `--test-output-dir=<testOutputDir>` | Configures the test output directory for screenshots and other test artifacts.                               |
| `--test-suite-name=<testSuiteName>` | Test suite name.                                                                                             |
| `<flowFiles>...`                    | One or more flow files or folders containing flow files.                                                     |

#### `cloud`

Upload and run your flows on Maestro Cloud.

| Option                                      | Description                                                                                                                                                            |
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--android-api-level=<level>`               | (Deprecated) Android API level. Use `--device-os` instead.                                                                                                             |
| `--apiKey`, `--api-key=<key>`               | API key for Maestro Cloud.                                                                                                                                             |
| `--apiUrl`, `--api-url=<url>`               | API base URL.                                                                                                                                                          |
| `--appBinaryId`, `--app-binary-id=<id>`     | The ID of an app binary previously uploaded to Maestro Cloud.                                                                                                          |
| `--app-file=<path>`                         | App binary to run your Flows against.                                                                                                                                  |
| `--async`                                   | Run the upload asynchronously and exit immediately.                                                                                                                    |
| `--branch=<name>`                           | The branch this upload originated from.                                                                                                                                |
| `--commitSha`, `--commit-sha=<sha>`         | The commit SHA of this upload.                                                                                                                                         |
| `--config=<file>`                           | Optional .yaml configuration file for Flows.                                                                                                                           |
| `--device-locale=<locale>`                  | Locale for the device (e.g., "de\_DE" for Germany).                                                                                                                    |
| `--device-model=<model>`                    | Device model to run against. iOS: `iPhone-11`, `iPhone-17-Pro`, etc. Android: `pixel_6`, `pixel_7`, etc. Run `maestro list-cloud-devices` to see all supported models. |
| `--device-os=<os>`                          | OS version to run against. iOS: `iOS-18-2`, `iOS-26-2` etc. Android: `android-33`, `android-34`, etc. Run `maestro list-cloud-devices` to see all supported versions.  |
| `-e`, `--env=<Key=Value>`                   | Environment variables to inject into your Flows.                                                                                                                       |
| `--exclude-tags=<tags>`                     | List of tags that will remove the Flows containing the provided tags.                                                                                                  |
| `--flows=<path>`                            | A Flow path or a folder path that contains Flows.                                                                                                                      |
| `--format=<format>`                         | Test report format (default=NOOP). Options: `JUNIT`, `HTML`, `NOOP`.                                                                                                   |
| `-h`, `--help`                              | Display help message.                                                                                                                                                  |
| `--include-tags=<tags>`                     | List of tags. Only flows containing these tags will be run.                                                                                                            |
| `--ios-version=<version>`                   | (Deprecated) iOS version. Use `--device-os` instead.                                                                                                                   |
| `--mapping=<path>`                          | dSYM file for iOS or Proguard mapping file for Android.                                                                                                                |
| `--name=<name>`                             | Name of the upload.                                                                                                                                                    |
| `--[no-]ansi`, `--[no-]color`               | Enable / disable colors and ANSI output.                                                                                                                               |
| `--output=<path>`                           | File to write report into (default is report.xml).                                                                                                                     |
| `--projectId`, `--project-id=<id>`          | Project ID.                                                                                                                                                            |
| `--pullRequestId`, `--pull-request-id=<id>` | The ID of the pull request this upload originated from.                                                                                                                |
| `--repoName`, `--repo-name=<name>`          | Repository name (e.g., GitHub repo slug).                                                                                                                              |
| `--repoOwner`, `--repo-owner=<owner>`       | Repository owner (e.g., GitHub organization or user slug).                                                                                                             |
| `--test-suite-name=<name>`                  | Test suite name.                                                                                                                                                       |

#### `record`

Record your flow execution.

| Option                                | Description                                                                         |
| ------------------------------------- | ----------------------------------------------------------------------------------- |
| `<flowFile>`                          | The Flow file to record.                                                            |
| `[<outputFile>]`                      | Output file for the rendered video. Only valid for local rendering using `--local`. |
| `--apple-team-id=<appleTeamId>`       | The unique 10-character Apple Team ID assigned to your team's account.              |
| `--[no-]ansi`, `--[no-]color`         | Enable or turn off colors and ANSI output.                                          |
| `--config=<configFile>`               | Optional .yaml configuration file. Defaults to `config.yaml` in the root directory. |
| `--debug-output=<debugOutput>`        | Configures a custom path for debug output.                                          |
| `-e`, `--env=<String=String>`         | Environment variables to inject into the Flow.                                      |
| `-h`, `--help`                        | Display the help message.                                                           |
| `--local`                             | Record using local rendering (Beta).                                                |
| `--output=<path>`                     | Write the report to this file. The default is report.xml.                           |
| `--repoName`, `--repo-name=<name>`    | Set the repository name.                                                            |
| `--repoOwner`, `--repo-owner=<owner>` | Set the repository owner.                                                           |

#### `start-device`

Launch an emulator or simulator.

| Option                     | Description                                                                                                                                                      |
| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--device-locale=<locale>` | Combination of lowercase ISO-639-1 code and uppercase ISO-3166-1 code (e.g., "de\_DE").                                                                          |
| `--device-model=<model>`   | Device model to run against. iOS: `iPhone-11`, `iPhone-17-Pro`, etc. Android: `pixel_6`, `pixel_7`, etc. Run `maestro list-devices` to see all supported models. |
| `--device-os=<os>`         | OS version to use. iOS: `iOS-18-2`, `iOS-26-2` etc. Android: `android-33`, `android-34`, etc. Run `maestro list-devices` to see all supported versions.          |
| `--force-create`           | Overrides the existing device if it already exists.                                                                                                              |
| `-h`, `--help`             | Display the help message.                                                                                                                                        |
| `--os-version=<version>`   | OS version. (Deprecated - use `device-os` instead.)                                                                                                              |
| `--platform=<platform>`    | Platforms: `android`, `ios`, or `web`                                                                                                                            |

#### `list-devices`

List local devices available, grouped by platform.

| Option                  | Description                                     |
| ----------------------- | ----------------------------------------------- |
| `-h`, `--help`          | Display the help message.                       |
| `--platform=<platform>` | Filter by platform: `android`, `ios`, or `web`. |

#### `list-cloud-devices`

List devices available on Maestro Cloud, grouped by platform.

| Option                  | Description                                     |
| ----------------------- | ----------------------------------------------- |
| `-h`, `--help`          | Display the help message.                       |
| `--platform=<platform>` | Filter by platform: `android`, `ios`, or `web`. |

### Named parameters

While Maestro supports positional parameters for quick commands, using named parameters is strongly recommended for clarity and reliability, especially in CI/CD pipelines.

Named parameters such as `--app-file` and `--flows` can be provided in any order, making scripts easier to read and less error-prone.

| Parameter    | Purpose                                                          |
| ------------ | ---------------------------------------------------------------- |
| `--app-file` | Specifies the local app file path you are uploading.             |
| `--flows`    | Specifies the local directory or specific file of flows to test. |

```bash
# Using a folder of flows
maestro cloud \
  --app-file app.apk \
  --flows myFlows/

# Using a single flow file
maestro cloud \
  --app-file app.apk \
  --flows flow.yaml
```

Because named parameters are explicit, their order does not matter:

```bash
# Order A
maestro cloud --app-file example.apk --flows ./myTests

# Order B
maestro cloud --flows ./myTests --app-file example.apk
```

If you rely on positional parameters, the order must be correct or the command will fail:

```bash
# This works
maestro cloud example.apk ./myTests

# This will FAIL
maestro cloud ./myTests example.apk
```

For CI environments and long-lived scripts, prefer named parameters to avoid subtle errors.


# Environment variables

Reference for all environment variables supported by the Maestro CLI, covering   logging, analytics, cloud authentication, and driver behavior.

You can configure the Maestro CLI behavior using the following environment variables. Set them in your shell or CI/CD environment before running any `maestro` command.

| Variable                                     | Type    | Default                                              | Description                                                                                                                                                                                                                           |
| -------------------------------------------- | ------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED` | Boolean | `false`                                              | Disables the notification displayed on each run about AI analysis.                                                                                                                                                                    |
| `MAESTRO_CLI_LOG_PATTERN_CONSOLE`            | String  | `%highlight([%5level]) %msg%n`                       | Controls the console logging format using [Logback layout](https://logback.qos.ch/manual/layouts.html) patterns.                                                                                                                      |
| `MAESTRO_CLI_LOG_PATTERN_FILE`               | String  | `%d{HH:mm:ss.SSS} [%5level] %logger.%method: %msg%n` | Controls the file logging format using [Logback layout](https://logback.qos.ch/manual/layouts.html) patterns. For more information, see [Test reports and artifacts](/maestro-flows/workspace-management/test-reports-and-artifacts). |
| `MAESTRO_CLI_NO_ANALYTICS`                   | Boolean | `false`                                              | Disables Maestro analytics collection.                                                                                                                                                                                                |
| `MAESTRO_CLOUD_API_KEY`                      | String  | Not set                                              | The API key used when communicating with Maestro Cloud. Required for cloud operations. For more information, see [Run tests on Maestro Cloud](/maestro-cloud/run-tests-on-maestro-cloud).                                             |
| `MAESTRO_DISABLE_UPDATE_CHECK`               | Boolean | `false`                                              | Disables the check for newer Maestro versions when running the CLI.                                                                                                                                                                   |
| `MAESTRO_DRIVER_STARTUP_TIMEOUT`             | Number  | `15000`                                              | The maximum time (in milliseconds) to wait for a driver to start. For more information, see [Run your first test with the Maestro CLI](/maestro-cli/run-your-first-test-with-the-maestro-cli).                                        |

#### Usage examples

{% tabs %}
{% tab title="Disable analytics" %}

```shellscript
export MAESTRO_CLI_NO_ANALYTICS=true
export MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED=true
maestro test my-flow.yaml
```

{% endtab %}

{% tab title="Cloud authentication" %}

```shellscript
export MAESTRO_CLOUD_API_KEY=your_api_key_here
maestro cloud --app-file app.apk --flows ./flows
```

{% endtab %}

{% tab title="Driver startup timeout" %}

```shellscript
export MAESTRO_DRIVER_STARTUP_TIMEOUT=30000
maestro test my-flow.yaml
```

{% endtab %}

{% tab title="Custom log format" %}

```shellscript
export MAESTRO_CLI_LOG_PATTERN_CONSOLE="%d{HH:mm:ss} [%level] %msg%n"
maestro test my-flow.yaml
```

{% endtab %}
{% endtabs %}


# Using a proxy with Maestro CLI

How to configure system environment variables that Maestro CLI can read to enable it to use a proxy server when it needs to connect to the internet.

Lots of corporate environments require use of proxy server for internet traffic for security purposes. Some technically-minded folks prefer a proxy in their own home. Configuring Maestro CLI to use a proxy server is done through environment variables.

### Environment Variables

You configure proxy options through either of 2 variables:

* **JAVA\_OPTS** - Used by all Java (or JVM-based) applications on the system. This is useful when you might have more than one application with this need, and you only want to configure it once.
* **MAESTRO\_OPTS** - Exactly the same format, but applies only to Maestro, not to other application. This is useful when you might have specific configurations already in JAVA\_OPTS, or you're worried about breaking another JVM-based application

### Variable Settings

#### Using a System Proxy

To use a system proxy, set the value of the environment variable like this:

<pre><code><strong>MAESTRO_OPTS="-Djava.net.useSystemProxies=true"
</strong></code></pre>

#### Using a Custom Proxy

To use a custom proxy, set the environment variable with the host and port of the server to connect to:

```
MAESTRO_OPTS="-Dhttps.proxyHost=myproxy.com -Dhttps.proxyPort=8080"
```

### Configuring Variables

This section covers how to set the variables on different operating systems, and for different lengths of time.

#### Single use configuration

{% tabs %}
{% tab title="macOS / Linux" %}
For a single command

```bash
MAESTRO_OPTS="-Djava.net.useSystemProxies=true" maestro login
```

Until you close this terminal window

```bash
export MAESTRO_OPTS="-Djava.net.useSystemProxies=true"
maestro login
```

{% endtab %}

{% tab title="Windows" %}
To set a variable for the duration of this open Command Prompt:

```bat
set MAESTRO_OPTS=-Djava.net.useSystemProxies=true                                                                                                                                                
maestro login
```

To set a variable for the duration of this Powershell Prompt:

```powershell
$env:MAESTRO_OPTS="-Djava.net.useSystemProxies=true"
maestro login
```

{% endtab %}
{% endtabs %}

#### Permanent configuration

{% tabs %}
{% tab title="macOS" %}
To permanently add the environment variable to your .zshrc for all future shell sessions:

```bash
echo 'export MAESTRO_OPTS="-Djava.net.useSystemProxies=true"' >> ~/.zshrc
```

{% endtab %}

{% tab title="Linux" %}
To permanently add the environment variable to your .bashrc for all future shell sessions:

```bash
echo 'export MAESTRO_OPTS="-Djava.net.useSystemProxies=true"' >> ~/.bashrc
```

{% endtab %}

{% tab title="Windows" %}
To permanently add the environment variable for all future console sessions:

```
setx MAESTRO_OPTS=-Djava.net.useSystemProxies=true
```

{% endtab %}
{% endtabs %}


# Maestro Cloud overview

Maestro Cloud provides enterprise-grade infrastructure for parallel mobile test execution.

Maestro Cloud is a hosted, enterprise-grade infrastructure designed to execute automated tests with high parallelism and reliable scaling. It allows teams to run their Flows in a stable, managed environment, eliminating the need to configure or maintain local emulators and simulators.

By offloading device management to the cloud, teams can reduce test execution time by up to 90% through asynchronous parallel runs, enabling faster shipping cycles with increased confidence.

{% hint style="info" %}
Maestro does not provide a separate "Cloud CLI." To take advantage of Maestro Cloud features, you use the `cloud` subcommand within the standard [Maestro CLI](https://docs.maestro.dev/maestro-cli/). This subcommand uploads your app and test flows to our cloud infrastructure and enables hosted test execution.
{% endhint %}

### Learn how to use Maestro Cloud

Follow these guides to set up your cloud testing environment:

1. [How to run your tests on Maestro Cloud](/maestro-cloud/run-tests-on-maestro-cloud): A step-by-step tutorial on prerequisites and your first cloud upload.
2. [CI Integration](/maestro-cloud/ci-cd-integration): Detailed guides for connecting to GitHub, Bitrise, Bitbucket, CircleCI, or any generic CI platform.
3. [Cloud Command Reference](/maestro-cloud/cloud-commands): A technical reference for all cloud-specific CLI arguments and parameters.

### Core capabilities

Maestro Cloud provides a purpose-built environment that ensures deterministic results by addressing the common causes of flakiness in mobile testing.

| Capability                 | Description                                                                                               |
| -------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Device Isolation**       | Every virtual device is wiped and recreated between tests to ensure complete isolation and repeatability. |
| **Flexible Configuration** | Customize exact environments, such as Android API levels or specific iOS models.                          |
| **Global Testing**         | Test localized app behavior using the `--device-locale` parameter and fixed regional timezones.           |
| **Cross-Platform**         | Native support for Android (Views/Compose), iOS (UIKit/SwiftUI), React Native, Flutter, and Web.          |

### CI/CD and workflow integration

You can integrate Maestro into your existing development lifecycle. Maestro Cloud offers native integrations with popular CI providers to automate your testing pipeline.

| Feature                      | Description                                                                                                           |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Native CI Support**        | Ready-made integrations for GitHub Actions, Bitrise, Bitbucket Pipelines, and CircleCI.                               |
| **Pull Request Integration** | Specifically for GitHub and GitHub Enterprise, Maestro can block merging if test failures are detected in a PR.       |
| **Performance Optimization** | Reuse cached app binaries via the `app-binary-id` to skip re-uploading the application for every individual test run. |

### Next step

Learn [How to run your tests on Maestro Cloud](/maestro-cloud/run-tests-on-maestro-cloud) using the tutorial to launch your first parallel test run.


# Run tests on Maestro Cloud

Upload and execute your Maestro tests on Cloud infrastructure using the CLI.

Running your tests on Maestro Cloud provides reliable scaling, guaranteed parallelism, and seamless CI integration for your mobile and web applications.

This guide explains how to execute your tests using Maestro Cloud via the [Maestro CLI](https://docs.maestro.dev/maestro-cli/).

### Prerequisites

Before running tests on Maestro Cloud, ensure you have the following:

* **Maestro account:** [Sign up for a Maestro account](https://signin.maestro.dev/sign-up).
* **Cloud plan:** Maestro Cloud requires a Cloud plan, which you can start as a trial from the [Maestro Dashboard](https://signin.maestro.dev/sign-up).
* [**Maestro CLI**](/maestro-cli/how-to-install-maestro-cli)**:** Install the Maestro CLI on your local machine or CI environment.

{% hint style="info" %}
**Test your app**

When testing your app, you also need the app binary for Android (ARM APK) or iOS (simulator .app bundle). If you don’t know how to build your app, check the [Build your app for the cloud](/maestro-cloud/build-your-app-for-the-cloud) guide.
{% endhint %}

### Command syntax

Use the `maestro cloud` command to upload your app and execute your flows. This command can be used both for local testing and within CI pipelines.

```bash
maestro cloud [options] --app-file <app-file> --flows <flow-file-or-directory>
```

### Run Flows on the Cloud

The Maestro CLI provides sample files to help you get started quickly. Use the `download-samples` command to download a sample app and Flow file:

```bash
maestro download-samples
```

{% hint style="info" %}
You can upload your own app and Flow files, but we recommend using the samples first to understand how it works.
{% endhint %}

After running the command, the Maestro CLI downloads a folder containing a set of Flows into your current directory. You can use the included app builds to test both Android and iOS apps:

{% tabs %}
{% tab title="Android" %}
To run an Android test using the sample app and Flow, run the following command:

```bash
cd samples
maestro cloud --app-file sample.apk --flows android-flow.yaml
```

{% endtab %}

{% tab title="iOS" %}
To run an iOS test using the sample app and Flow, run the following command:

```bash
cd samples
maestro cloud --app-file sample.app --flows ios-flow.yaml
```

{% endtab %}
{% endtabs %}

After a successful upload, the CLI prints a link to the Maestro Console.

1. Click the provided link to open the console.
2. Test processing may take a few minutes depending on your other uploads and how many runners are configured on your account.
3. Review the test results, including videos, logs, and hierarchy data.

<figure><img src="/files/71INpIVpt5Qp5D7ISZmn" alt=""><figcaption></figcaption></figure>

### Authentication and project selection

If you belong to multiple organizations or projects, use the following flags to avoid interactive prompts:

* `--api-key`: Your Maestro Cloud API key.
* `--project-id`: The specific project ID for the upload.

The following example demonstrates how to use these flags:

```bash
maestro cloud --api-key <YOUR_API_KEY> --project-id <YOUR_PROJECT_ID> --app-file sample.apk --flows flow.yaml
```

For a complete list of cloud command options, see the [Maestro CLI reference](/maestro-cli/maestro-cli-commands-and-options).

### Next steps

After verifying your cloud execution, [learn how to build your app](/maestro-cloud/build-your-app-for-the-cloud) or integrate Maestro Cloud into your development workflow:

* [CI/CD Integration](/maestro-cloud/ci-cd-integration): Automate your tests using GitHub Actions, Bitrise, CircleCI, and more.
* [Advanced features](/maestro-cloud/advanced-features): Manage secrets, configure locales, and use IP allowlists.


# Build your app for the cloud

Build Android APKs and iOS .app bundles for Simulator to run on Maestro Cloud.

Before you can execute tests on Maestro Cloud, you must provide a mobile application binary (APK or `.app` directory). This page provides the specific technical requirements and build instructions for the most common development environments to ensure your app is compatible with Maestro's cloud infrastructure.

{% hint style="info" %}
**Project-specific build requirements**

The instructions on this page cover typical ways to build mobile applications. However, your project may have unique pipelines, specific command-line switches, or environment configurations (e.g., specialized Expo or React Native build scripts).

We recommend checking your project's README, internal developer documentation, or the official documentation for your chosen framework for exact build steps.
{% endhint %}

{% hint style="success" %}
This feature requires a Cloud plan. Start for free at [**maestro.dev**](https://signin.maestro.dev/sign-up).
{% endhint %}

### Android build instructions

Maestro Cloud now uses ARM architectures for Android. Ensure your app binary meets the following requirements:

* **Format:** APK only. Android App Bundles (.aab) are not currently supported.
* **Architecture:** Must be compatible with ARM or be a multi-architecture build. x86-only APKs will fail to launch in the cloud environment.
* **Build Type:** Both Release and Debug builds are supported.

To build your app, use one of the following approaches:

{% tabs %}
{% tab title="Build with Gradle" %}
Run the following commands from your project root to generate the APK.

```bash
# To generate a Debug build
./gradlew assembleDebug

# To generate a Release build
./gradlew assembleRelease
```

Once finished, find the file in the `app/build/outputs/apk/` directory.
{% endtab %}

{% tab title="Build with Flutter" %}
Use the Flutter CLI to create a compatible APK. You can use anyone of the following commands:

```bash
# Debug build (Recommended for initial testing)
flutter build apk --debug

# Release build
flutter build apk
```

The result will be located in the `build/app/outputs/flutter-apk/` folder.
{% endtab %}
{% endtabs %}

### iOS build instructions

Maestro Cloud runs iOS tests on Simulators. Do not upload binaries built for physical iOS devices.

* **Format:** `.app` bundle.
* **Target:** Must be built for the iOS Simulator.

To build your app, use one of the following options:

{% tabs %}
{% tab title="Build with Xcode CLI" %}
Use the `xcodebuild` command to create a simulator build. The following example builds a project named `MyApp` and saves the output to a `build/` folder:

```bash
xcodebuild -project MyApp.xcodeproj \
  -scheme MyApp \
  -configuration Debug \
  -destination 'generic/platform=iOS Simulator' \
  CONFIGURATION_BUILD_DIR=$PWD/build
```

The `.app` bundle will be available in the `build/` directory.
{% endtab %}

{% tab title="Build with Fastlane" %}
If you use [Fastlane](https://fastlane.tools/) for automation, the script should look the following one to generate the app file:

```ruby
xcodebuild(
    configuration: build_config[:configuration],
    scheme: build_config[:scheme],
    workspace: build_config[:xcode_workspace],
    xcargs: "-quiet -sdk 'iphonesimulator' -destination 'generic/platform=iOS Simulator'",
    derivedDataPath: IOS_DERIVED_DATA_PATH # this will contain the .app which we need later on
)
```

{% endtab %}

{% tab title="Building with Flutter" %}
Use the following command to create a simulator-compatible debug build:

```bash
flutter build ios --debug --simulator
```

The app bundle is located in the `build/ios/iphonesimulator/` directory.
{% endtab %}
{% endtabs %}

### Run the build

Once your binary is ready, you can upload it to Maestro Cloud using the Maestro CLI or Maestro Studio:

{% tabs %}
{% tab title="Maestro CLI" %}
Use the command:

```bash
maestro cloud --app-binary [your-app-binary] --flows [your-flow-directory]
```

{% endtab %}

{% tab title="Maestro Studio" %}
You can choose to run all or a single Flow on Maestro Cloud:

* **Run all Flows:** Click on the **Run on Cloud** button in the sidebar and then select your app binary.
* **Run a single Flow**: Open the Flow file, click on the **Run on Cloud** button at the top of the file, and then select your app binary.
  {% endtab %}
  {% endtabs %}

#### Next steps

Now that you know how to build your app, you can explore one of the following guide categories to improve your use of Maestro Cloud:

* [CI/CD integration](/maestro-cloud/ci-cd-integration): Move your tests out of your local terminal and into your deployment pipeline.
* [Environment configuration](/maestro-cloud/environment-configuration): Customize the cloud hardware to match your users' real-world conditions.
* [Notifications](/maestro-cloud/notifications): Set up Slack, Email, or Webhooks to keep your team informed of every pass or failure.
* [Advanced features](/maestro-cloud/advanced-features): Use binary reuse to speed up iterations and manage secrets via environment variables.

If you haven’t tested the Maestro CLI yet, check the [Run tests on Maestro Cloud](/maestro-cloud/run-tests-on-maestro-cloud) guide.


# CI/CD integration


# GitHub Actions

Official GitHub Action for Maestro Cloud. Run mobile tests on push or PR, pass env variables, and access outputs like console URL.

Maestro Cloud has a integration with GitHub Actions that allows you to automate your mobile and web testing pipelines. By using the official [Maestro Cloud GitHub Action](https://github.com/marketplace/actions/maestro-cloud-upload-action), you can trigger tests on every push or pull request and view results directly in the Maestro Console.

{% hint style="info" %}
**Maestro Cloud Plan required.**

GitHub Actions integration is available on the [Maestro Cloud Plan](https://signin.maestro.dev/sign-up).
{% endhint %}

### Configuration and usage

The following steps describe how to configure and use the GitHub Action to run Maestro tests.

{% stepper %}
{% step %}
**Add your API key secret**

The GitHub Action requires an API key to authenticate with Maestro Cloud. You must expose your API key as a [GitHub Repository Secret](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets):

1. Navigate to your GitHub repository and click **Settings**.
2. In the sidebar, click **Secrets and variables** > **Actions**.
3. Click **New repository secret**.
4. Name the secret `MAESTRO_API_KEY` and paste your API key from the [Maestro Dashboard](https://app.maestro.dev/) into the **Secret** field.
5. Click **Add secret**.
   {% endstep %}

{% step %}
**Add your Project ID**

You can find your Project ID in the **Settings** section of the [Maestro Dashboard](https://app.maestro.dev/). Open the **Settings** menu and select the desired project to have access to the ID. While not a secret, you can also store it as a Repository Secret (e.g., `MAESTRO_PROJECT_ID`) for convenience.

<figure><img src="/files/HHnuXLDCqiiMZqtefbBn" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
**Update your action**

Add the following step to your workflow `.yaml` file. This basic configuration uploads your app and runs all Flows found in the `.maestro` directory.

```yaml
- name: Run Maestro Cloud
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_API_KEY }}
    project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
    app-file: app/build/outputs/apk/debug/app-debug.apk
```

To help you, the following code snippet shows an example of a complete GItHub Action used to build and run Maestro tests:

```yaml
name: Build and run Maestro tests (Native Android)

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  maestro-cloud:
    runs-on: ubuntu-latest
    outputs:
      app: app/build/outputs/apk/debug
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          java-version: 11
          distribution: 'temurin'
      - run: ./gradlew assembleDebug
      - uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
        with:
          api-key: ${{ secrets.MAESTRO_API_KEY }}
          project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
          app-file: app/build/outputs/apk/debug/app-debug.apk
```

{% endstep %}
{% endstepper %}

### Inputs reference

Below are all available inputs for the `mobile-dev-inc/action-maestro-cloud` action.

#### Required inputs

| Input        | Description                                                                                                                                     |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `api-key`    | Your Maestro Cloud API Key.                                                                                                                     |
| `project-id` | The Project ID to run tests against.                                                                                                            |
| `app-file`   | <p>Path to the app binary (APK, AAB, or ZIP of .app) to upload.<br><br><strong>Required</strong> unless <code>app-binary-id</code> is used.</p> |

#### Optional configuration

| Input           | Description                                         | Default    |
| --------------- | --------------------------------------------------- | ---------- |
| `app-binary-id` | ID of a previously uploaded binary to reuse.        | `null`     |
| `async`         | If `true`, starts the upload and exits immediately. | `false`    |
| `env`           | Environment variables to pass to the Flow run.      | `null`     |
| `exclude-tags`  | Comma-separated list of tags to **exclude**.        | `null`     |
| `include-tags`  | Comma-separated list of tags to **include**.        | `null`     |
| `name`          | Friendly name for the upload.                       | Commit Msg |
| `timeout`       | Max time (in minutes) to wait for completion.       | `30`       |
| `workspace`     | Path to the directory containing Flows.             | `.maestro` |

#### Device configuration

| Input           | Description                                                                                | Example                    |
| --------------- | ------------------------------------------------------------------------------------------ | -------------------------- |
| `device-model`  | Device model to run against. Run `maestro list-cloud-devices` to see all supported values. | `iPhone-17-Pro`, `pixel_6` |
| `device-os`     | OS version to run against. Run `maestro list-cloud-devices` to see all supported values.   | `iOS-26-2`, `android-34`   |
| `device-locale` | Device locale (ISO-639-1 + ISO-3166-1).                                                    | `de_DE`                    |
| `mapping-file`  | Path to ProGuard map (Android) or dSYM (iOS).                                              | `./MyApp.dSYM`             |

{% hint style="warning" %}
The `android-api-level` and `ios-version` inputs are **deprecated** in favor of `device-os`. Existing workflows that set them continue to work but emit a deprecation warning. Migrate to `device-os` (e.g. `device-os: android-34` or `device-os: iOS-26-2`) when convenient.
{% endhint %}

{% hint style="info" %}
Access the [Configure the OS](/maestro-cloud/environment-configuration/configure-the-os) page for more information.
{% endhint %}

### Next steps

Explore the complementary content to improve your GitHub Action to run Maestro tests exploring the following pages:

* [Maestro Cloud Action](https://github.com/marketplace/actions/maestro-cloud-upload-action): Official GitHub Action for you to upload your app to Maestro Cloud to run your Flows in CI.
* [Platform guides](/maestro-cloud/ci-cd-integration/github-actions/platform-guides): Explore the guides to use the official GitHub Action for Android, iOS, and Flutter.
* [Advanced configurations](/maestro-cloud/ci-cd-integration/github-actions/advanced-configuration): Learn how to configure async mode, environment variables, and custom workspaces.
* [Outputs and triggers](/maestro-cloud/ci-cd-integration/github-actions/outputs-and-triggers): Learn how to use action outputs and configure CI triggers.
* Explore all the [subcommand options for cloud.](/maestro-cli/maestro-cli-commands-and-options#cloud)


# Platform guides

Set up Maestro Cloud GitHub Actions for Android, iOS, and Flutter.

This guide covers platform-specific configurations for setting up Maestro Cloud GitHub Actions.

{% tabs %}
{% tab title="Android" %}
To run tests on Android, you generally need to build an ARM compatible APK (typically a debug build) and upload it to Maestro Cloud.

**Build command**

Use Gradle to assemble your debug APK before the Maestro step.

```yaml
- uses: actions/setup-java@v3
  with:
    java-version: 11
    distribution: 'temurin'

- run: ./gradlew assembleDebug
```

**Action configuration**

Point the `app-file` to your generated APK.

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_API_KEY }}
    project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
    app-file: app/build/outputs/apk/debug/app-debug.apk
```

{% hint style="info" %}
The `app-file` path supports glob patterns. If multiple files match, the first one is used.
{% endhint %}

**ProGuard deobfuscation**

If your app uses ProGuard/R8, you should upload the mapping file to deobfuscate performance traces and error logs.

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_API_KEY }}
    project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
    app-file: app/build/outputs/apk/release/app-release.apk
    mapping-file: app/build/outputs/mapping/release/mapping.txt
```

**Specifying the Android OS version and device**

The default API level on Maestro Cloud is 33 (Android 13). You can override the OS version and the device model using `device-os` and `device-model`:

```yaml
with:
  # ... other inputs
  device-model: pixel_6
  device-os: android-34
```

{% hint style="info" %}
The `android-api-level` input is deprecated in favor of `device-os`. Run `maestro list-cloud-devices` to see all supported Android device models and OS versions.
{% endhint %}

**Complete example for Android**

The following code snippet shows a complete GitHub Action to build your Android app and test is using Maestro Cloud.

```yaml
name: Build and run Maestro tests (Native Android)

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  maestro-cloud:
    runs-on: ubuntu-latest
    outputs:
      app: app/build/outputs/apk/debug
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          java-version: 11
          distribution: 'temurin'
      - run: ./gradlew assembleDebug
      - uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
        with:
          api-key: ${{ secrets.MAESTRO_API_KEY }}
          # note that you can supply the project id any way you like, it is not secret
          project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
          app-file: app/build/outputs/apk/debug/app-debug.apk
```

{% endtab %}

{% tab title="iOS" %}
To run tests on iOS, you must build your app for the iOS Simulator. Real device builds (ARM64 only) are not supported on the Cloud emulators.

**Build command**

You must specify `-destination 'generic/platform=iOS Simulator'` to ensure the build is compatible with Maestro Cloud simulators.

```yaml
- name: Build for Simulator
  run: |
    xcodebuild build \
      -scheme 'MyApp' \
      -configuration Debug \
      -project 'MyApp.xcodeproj' \
      -destination 'generic/platform=iOS Simulator' \
      CONFIGURATION_BUILD_DIR=$PWD/build
```

**Action configuration**

Point the `app-file` to the `.app` bundle generated in your build directory.

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_API_KEY }}
    project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
    app-file: build/MyApp.app
```

**Uploading symbol files (.dSYM)**

To get symbolicated stack traces, include the generated `.dSYM` file.

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_API_KEY }}
    project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
    app-file: build/MyApp.app
    mapping-file: build/MyApp.app.dSYM
```

**Specify the iOS Version and device**

The default iOS version is 16. You can specify a different OS version or device model if needed, using `device-os` and `device-model` respectively:

```yaml
with:
  # ... other inputs
  device-model: iPhone-17-Pro
  device-os: iOS-26-2
```

{% hint style="info" %}
The `ios-version` input is deprecated in favor of `device-os`. Run `maestro list-cloud-devices` to see all supported iOS device models and OS versions.
{% endhint %}

**Complete example for iOS**

The following code snippet shows a complete GitHub Action to build your iOS app and test is using Maestro Cloud.

```yaml
name: Build and run Maestro tests (Native iOS)

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: macOS-latest
    steps:
      - uses: actions/checkout@v2
      - run: xcodebuild build -scheme 'MyApp' -configuration Debug -project 'MyApp.xcodeproj' -destination 'generic/platform=iOS Simulator' CONFIGURATION_BUILD_DIR=$PWD/build
      - uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
        with:
          api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
          # note that you can supply the project id any way you like, it is not secret
          project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
          app-file: build/MyApp.app
```

{% endtab %}

{% tab title="Flutter" %}
Flutter requires specific build flags to generate binaries compatible with Maestro Cloud.

**Flutter Android**

Build the APK in debug mode.

```yaml
name: Build and run Maestro tests (Flutter Android)

on:
  workflow_dispatch:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  upload:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
        with:
          channel: "stable"
      - run: flutter build apk --debug
      - uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
        with:
          api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
          # note that you can supply the project id any way you like, it is not secret
          project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
          app-file: build/app/outputs/flutter-apk/app-debug.apk
```

**Flutter iOS**

Build the iOS app for the simulator.

```yaml
name: Build and run Maestro tests (Flutter iOS)

on:
  workflow_dispatch:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  ios:
    runs-on: macos-13
    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
        with:
          channel: "stable"
      - run: flutter build ios --debug --simulator
      - uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
        with:
          api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
          # note that you can supply the project id any way you like, it is not secret
          project-id: ${{ secrets.MAESTRO_PROJECT_ID }}
          app-file: build/ios/iphonesimulator/Runner.app # replace `Runner` with your app name
```

{% endtab %}
{% endtabs %}

#### Next Steps

* [Advanced configuration](/maestro-cloud/ci-cd-integration/github-actions/advanced-configuration): Learn how to configure async mode, environment variables, and custom workspaces.
* [Outputs and triggers](/maestro-cloud/ci-cd-integration/github-actions/outputs-and-triggers): Learn how to use action outputs and configure CI triggers.


# Advanced configuration

Customize Maestro Cloud runs with advanced settings for custom workspaces, upload naming, async mode, environment variables, and tag filtering.

Once you have the basics running, you can customize how Maestro Cloud executes your Flows using the options below.

### Custom workspace location

By default, the action looks for a `.maestro` folder in the root of your repository. If your Flows are located elsewhere, use the `workspace` argument.

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: app.zip
    workspace: myFlows/
```

### Custom upload name

Maestro Cloud automatically names your upload based on the context:

1. **Pull Request**: Uses the PR title.
2. **Push**: Uses the commit message.
3. **Fallback**: Uses the commit SHA.

To override this manually, use the `name` argument:

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: app.zip
    name: My Upload
```

### Async mode and timeouts

If you don't want the GitHub Action to wait for the tests to finish (e.g., "fire and forget"), set `async` to `true`.

{% hint style="info" %}
When running in async mode, the action will not fail if tests fail, and output variables like `MAESTRO_CLOUD_FLOW_RESULTS` will not be available.
{% endhint %}

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: app.zip
    async: true
```

If you want to wait for results but need more time than the default 30 minutes, use the `timeout` argument (in minutes).

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: app.zip
    timeout: 90 # Wait for 90 minutes
```

### Environment variables

You can pass environment variables to your Maestro Flows (accessible via `${env.VARIABLE_NAME}` in your YAML flows) using the `env` argument. Use a multiline string for multiple variables.

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: app.zip
    env: |
      USERNAME=<username>
      PASSWORD=<password>
```

### Filtering with Tags

You can use [Tags](/maestro-flows/workspace-management/test-discovery-and-tags) to include or exclude specific Flows from the run.

* `include-tags`: Only run Flows with these tags.
* `exclude-tags`: Skip Flows with these tags.

Values can be single tags or comma-separated lists.

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: app.zip
    include-tags: dev, pull-request
    exclude-tags: excludeTag
```

### Reusing uploaded binaries

If you have a workflow where you want to run multiple distinct test suites against the same app binary without re-uploading it, you can capture the `app-binary-id` output from a previous step.

```yaml
- id: upload
  uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: app.zip

- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-binary-id: ${{ steps.upload.outputs.MAESTRO_CLOUD_APP_BINARY_ID }}
```

Access the [Reuse app binary](/maestro-cloud/advanced-features/reuse-app-binary) guide for more information.

### Device Locale

To run your tests on a device with a specific locale (default is `en_US`), use the `device-locale` argument. The value is a combination of:

```
 lowercase ISO-639-1 code + _ + uppercase ISO-3166-1 code
```

The following code snippet displays an example:

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: app.zip
    device-locale: de_DE
```

Access the [App locales and device timezones](/maestro-cloud/environment-configuration/app-locales-and-device-timezones) guide for more information.

#### Next steps

* [Outputs and triggers](/maestro-cloud/ci-cd-integration/github-actions/outputs-and-triggers): Learn how to access build results and configure CI triggers.


# Outputs and triggers

Configure GitHub Action triggers and use output variables to integrate Maestro Cloud into your CI/CD pipeline.

To integrate Maestro Cloud seamlessly into your CI/CD pipeline, you need to configure when the action runs and how to use its results (e.g., posting a comment on a PR or failing a build).

### Triggers

You can trigger the Maestro Cloud action on various GitHub events. The most common are `push` and `pull_request`.

The following code snippet shows an example to trigger the action on pushes to your main branch and when pull requests are opened against it.

```yaml
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
```

#### Supporting Forks (`pull_request_target`)

If your repository receives pull requests from forks (which do not have access to your secrets by default), you may need to use the `pull_request_target` trigger.

{% hint style="info" %}
When using `pull_request_target`, you should explicitly checkout the PR's HEAD commit to ensure you are testing the new code, not the base branch.
{% endhint %}

```yaml
on:
  push:
    branches: [master]
  pull_request_target:
    branches: [master]
jobs:
  upload-to-mobile-dev:
    name: Run Flows on Maestro Cloud
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }} # Checkout PR HEAD
```

For more details on security and triggers, refer to [GitHub's documentation](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows).

### Action outputs

The Maestro Cloud action sets several output variables that you can use in subsequent steps of your workflow. To access them, you must give your step an `id`.

The following output variables are set by the action:

| **Output variable**           | **Description**                                                                          |
| ----------------------------- | ---------------------------------------------------------------------------------------- |
| `MAESTRO_CLOUD_CONSOLE_URL`   | The URL to view the test results in the Maestro Cloud Console.                           |
| `MAESTRO_CLOUD_APP_BINARY_ID` | The ID of the uploaded binary (useful for reusing the binary in later steps).            |
| `MAESTRO_CLOUD_UPLOAD_STATUS` | The final status of the upload. Not available in async mode.                             |
| `MAESTRO_CLOUD_FLOW_RESULTS`  | A JSON string containing the results of individual Flows. *Not available in async mode.* |

In order to access these variables you can use the following approach:

```yaml
- id: upload
  uses: mobile-dev-inc/action-maestro-cloud@v2.0.2
  with:
    api-key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}
    project-id: 'proj_01example0example1example2'
    app-file: <your_app_file>
    # ... any other parameters

- name: Access Outputs
  if: always()
  run: |
    echo "Console URL: ${{ steps.upload.outputs.MAESTRO_CLOUD_CONSOLE_URL }}"
    echo "Flow Results: ${{ steps.upload.outputs.MAESTRO_CLOUD_FLOW_RESULTS }}"
    echo "Upload Status: ${{ steps.upload.outputs.MAESTRO_CLOUD_UPLOAD_STATUS }}"
    echo "App Binary ID: ${{ steps.upload.outputs.MAESTRO_CLOUD_APP_BINARY_ID }}"
```

The `MAESTRO_CLOUD_UPLOAD_STATUS` output will contain one of the following strings:

* `PENDING`
* `PREPARING`
* `INSTALLING`
* `RUNNING`
* `SUCCESS`
* `ERROR`
* `CANCELED`
* `WARNING`
* `STOPPED`

Meanwhile, the `MAESTRO_CLOUD_FLOW_RESULTS` output is a JSON array containing objects with the following structure:

```json
[
  {
    "name": "my-first-flow",
    "status": "SUCCESS",
    "errors": []
  },
  {
    "name": "my-second-flow",
    "status": "SUCCESS",
    "errors": []
  },
  {
    "name": "my-cancelled-flow",
    "status": "CANCELED",
    "errors": [],
    "cancellationReason": "INFRA_ERROR"
  }
]
```

You can use a tool like `jq` in a subsequent step to parse this JSON and perform custom logic (e.g., sending a Slack notification with specific failure details).

### Related content

Learn how to send notifications after test finish using [Slack](/maestro-cloud/notifications/set-slack-notification), [email](/maestro-cloud/notifications/set-email-notification), or [webhooks](/maestro-cloud/notifications/configure-webhooks).


# Bitrise

Native Bitrise integration for Maestro Cloud. Set API Key, Project ID, and app binary path to trigger tests automatically.

Maestro Cloud provides a native integration step for Bitrise, allowing you to trigger cloud tests with minimal configuration.

{% hint style="info" %}
**Maestro Cloud Plan required.**

Bitrise integration is available on the [Maestro Cloud Plan](https://signin.maestro.dev/sign-up).
{% endhint %}

### Setting up Bitrise

Setting up Maestro Cloud in your Bitrise workflow is straightforward. Here’s how you can get it up and running:

1. First, head over to the [Maestro Dashboard](https://app.maestro.dev/) to get your **API Key** and **Project ID**.
2. In Bitrise, save your API Key as a secret variable (for example, `CLOUD_API_KEY`).

{% hint style="info" %}
Avoid using the `MAESTRO_` prefix for your secret names unless you specifically want them passed into the test run as environment variables.

Any variable prefixed with `MAESTRO_` will be [added as environment variable](/maestro-flows/flow-control-and-logic/parameters-and-constants) for your run.
{% endhint %}

3. Open the **Workflow Editor** and search for "Maestro" in the Bitrise step library. Add the step after your application binary build step.

<figure><img src="/files/CweEZLJMnsw5Iu4djt4W" alt=""><figcaption></figcaption></figure>

4. **Configure the Step**:
   * **API Key**: Enter your secret variable (e.g., `CLOUD_API_KEY`).
   * **Project ID**: Enter your Maestro Project ID.
   * **Build Path**: Provide the path to your built `.apk` or `.app` binary.

{% hint style="info" %}
Visit the [Maestro Step in the Bitrise catalog](https://bitrise.io/integrations/steps/maestro-cloud-upload) to see the full list of available step options.
{% endhint %}

5. **Workspace Validation**: Ensure that the **Flow Workspace** path matches your repository’s directory structure.

Once configured, Bitrise automatically triggers your Maestro tests in the cloud as part of your pipeline.

#### Device configuration

To target a specific OS version or device model, use the `device_os` and `device_model` inputs. Both inputs accept iOS and Android values.

```yaml
- maestro-cloud-upload@1:
    inputs:
    - api_key: $CLOUD_API_KEY
    - project_id: $MAESTRO_PROJECT_ID
    - app_file: $BITRISE_APK_PATH
    - device_os: android-34
    - device_model: pixel_6
```

| Input          | Description                                                                                | Example                    |
| -------------- | ------------------------------------------------------------------------------------------ | -------------------------- |
| `device_os`    | OS version to run against. Run `maestro list-cloud-devices` to see all supported values.   | `iOS-26-2`, `android-34`   |
| `device_model` | Device model to run against. Run `maestro list-cloud-devices` to see all supported values. | `iPhone-17-Pro`, `pixel_6` |

{% hint style="warning" %}
The `android_api_level` input is **deprecated** in favor of `device_os`. Existing workflows that set `android_api_level` continue to work but emit a deprecation warning. Migrate to `device_os` (e.g. `device_os: android-34`) when convenient.
{% endhint %}

#### Next steps

Now that your CI pipeline is connected, consider optimizing your cloud runs:

* Set up notifications via [Slack](/maestro-cloud/notifications/set-slack-notification), [email](/maestro-cloud/notifications/set-email-notification), or [webhooks](/maestro-cloud/notifications/configure-webhooks) to stay informed about build and test results.
* [Configure the operating system](/maestro-cloud/environment-configuration/configure-the-os) for your runs to match your application and dependency requirements.
* Define [locales and time zones](/maestro-cloud/environment-configuration/app-locales-and-device-timezones) to ensure consistent behavior across environments and regions.
* Explore all the [subcommand options for `claud`.](/maestro-cli/maestro-cli-commands-and-options#cloud)


# Bitbucket Pipelines

Native Bitbucket Pipe for Maestro Cloud. Set API key and Project ID to upload app binary and run Flows on cloud infrastructure.

Integrate Maestro Cloud into your Bitbucket CI/CD workflow using the [Maestro Cloud Upload Pipe](https://bitbucket.org/product/features/pipelines/integrations?search=maestro\&p=mobiledevinc/maestro-cloud-upload). This native integration allows you to automatically upload and execute your Flows on enterprise-grade infrastructure directly from your CI/CD pipeline.

{% hint style="info" %}
**Maestro Cloud Plan required.**

Bitbucket integration is available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

{% stepper %}
{% step %}

#### Setup environment variables

To authenticate with Maestro Cloud, you must add your credentials as secret variables in Bitbucket.

1. On Bitbucket, navigate to your **Repository → Repository settings → Repository Variables**.
2. Add the following secured variables:
   * `MDEV_API_KEY`: Your Maestro Cloud API Key.
   * `MDEV_PROJECT_ID`: Your specific Maestro Project ID.

{% hint style="info" %}
Access the [Maestro Dashboard](https://app.maestro.dev/) to get your API Key and Project ID.
{% endhint %}
{% endstep %}

{% step %}

#### Organize your Flows

Maestro expects your Flow files to be located in a specific directory (`.maestro/`) at the root of your repository.

1. Create a `.maestro/` directory at your repository root.
2. Place all Flow files directly inside `.maestro/` to be executed as a standalone test.
3. Place all "utility" Flows (those used by `runFlow`) in a `subflows/` subdirectory. This way you prevent these Flows from executing as top-level tests.

```
<root>
├── .maestro/
│   ├── subflows/
│   │   └── LoginSubFlow.yaml    # Only runs when called via runFlow
│   ├── Login.yaml               # Executed as a top-level Flow
│   ├── Add_to_Cart.yaml         # Executed as a top-level Flow
│   └── Search.yaml              # Executed as a top-level Flow
```

{% endstep %}

{% step %}

#### Add the Maestro Cloud Pipe

Update your `bitbucket-pipelines.yml` file to include the [Maestro Cloud Upload Pipe](https://bitbucket.org/mobiledevinc/maestro-cloud-upload/src/master/README.md). This step should occur immediately after your app has successfully built.

{% hint style="warning" %}
All file paths provided below are relative to the repository source root.
{% endhint %}

{% tabs %}
{% tab title="Android" %}

```yaml
script:
  - pipe: mobiledevinc/maestro-cloud-upload:1.5.0
    variables:
      MDEV_API_KEY: $MDEV_API_KEY
      MDEV_PROJECT_ID: $MDEV_PROJECT_ID
      MDEV_APP_FILE: app/build/outputs/apk/debug/app-debug.apk
```

{% endtab %}

{% tab title="iOS" %}

```yaml
script:
  - pipe: mobiledevinc/maestro-cloud-upload:1.5.0
    variables:
      MDEV_API_KEY: $MDEV_API_KEY
      MDEV_PROJECT_ID: $MDEV_PROJECT_ID
      MDEV_APP_FILE: <app_name>.app
```

{% endtab %}
{% endtabs %}

Once triggered, the Maestro Cloud Pipe automates the following lifecycle:

* Uploads your app binary and Flow workspace to Maestro Cloud.
* Runs your tests on dedicated cloud infrastructure.
* By default, the pipe waits for all Flows to complete. You can configure this option.
* Returns an exit code of `0` on success or `1` if failures are detected, allowing you to block builds based on test results.
  {% endstep %}
  {% endstepper %}

#### Next steps

Now that your CI pipeline is connected, consider optimizing your cloud runs:

* Set up notifications via [Slack](/maestro-cloud/notifications/set-slack-notification), [email](/maestro-cloud/notifications/set-email-notification), or [webhooks](/maestro-cloud/notifications/configure-webhooks) to stay informed about build and test results.
* [Configure the operating system](/maestro-cloud/environment-configuration/configure-the-os) for your runs to match your application and dependency requirements.
* Define [locales and time zones](/maestro-cloud/environment-configuration/app-locales-and-device-timezones) to ensure consistent behavior across environments and regions.
* Explore all the [subcommand options for `claud`.](/maestro-cli/maestro-cli-commands-and-options#cloud)


# CircleCI

Integrate Maestro Cloud into CircleCI pipelines to automate mobile testing. Set up API keys, organize flows, and configure your .circleci/config.yml.

Integrate Maestro Cloud into your CircleCI pipelines to automate your mobile testing. This guide walks you through setting up environment variables, organizing your flows, and configuring your `.circleci/config.yml`.

{% hint style="info" %}
**Maestro Cloud Plan required.**

CircleCI integration is available on the [Maestro Cloud Plan](https://signin.maestro.dev/sign-up).
{% endhint %}

{% stepper %}
{% step %}

#### Save API key and Project ID

First, add your credentials as secret environment variables in your CircleCI Project Settings to keep them secure and accessible to your runners.

{% hint style="info" %}
**API key and Project ID**

You can find your API key and Project ID by accessing the [Maestro Dashboard](https://app.maestro.dev/).

You can find your Project ID in the Dashboard **Settings**. Open the **Settings** menu and select the desired project to have access to the ID.
{% endhint %}

1. Navigate to your **Project -> Project Settings -> Environment Variables**.
2. Save your API Key (e.g., `MDEV_API_KEY`) and Project ID (e.g., `MDEV_PROJECT_ID`).

<figure><img src="/files/7lUOSCDZaanl5pLp2bTA" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}

#### Organize your Flows

Organize your test files in a dedicated directory within your repository. While you can use any folder name, it is important to point Maestro to this location during the upload step.

```
├── e2e-tests/
│   ├── subflows/
│   │   └── LoginSubflow.yaml
│   ├── Login.yaml
│   ├── Add to Cart.yaml
│   └── Search.yaml
```

{% hint style="success" %}
**Subflows**

Files in subdirectories (like `subflows/`) will not be executed as top-level tests, but can be called by other Flows using the `runFlow` command.
{% endhint %}
{% endstep %}

{% step %}

#### Add the Maestro upload job

Integrate Maestro by adding a specific job to your `.circleci/config.yml`. This job installs the CLI and uploads your binary and Flows using the `--app-file` and `--flows` parameters for better reliability.

```yaml
maestro-upload:
    docker:
      - image: cimg/openjdk:19.0.1
    steps:
      - attach_workspace:
          at: .
      - run:
          name: Download maestro and run in the cloud
          command: |
            curl -Ls "https://get.maestro.mobile.dev" | bash
            export PATH="$PATH":"$HOME/.maestro/bin"
            
            # Using named parameters for better reliability
            maestro cloud \
              --apiKey $MDEV_API_KEY \
              --projectId $MDEV_PROJECT_ID \
              --app-file path_to_my_app.apk \
              --flows e2e-tests
```

{% endstep %}
{% endstepper %}

### Configuration examples

{% tabs %}
{% tab title="Android pipeline" %}
This example builds an Android APK and uploads it to Maestro Cloud.

```yaml
version: 2.1
orbs:
  android: circleci/android@2.1.2
jobs:
  build-android:
    executor:
      name: android/android-docker
      tag: 2022.08.1
    steps:
      - checkout
      - android/restore-gradle-cache
      - run:
          name: Assemble debug build
          command: |
            ./gradlew :app:assembleDebug
      - persist_to_workspace:
          root: .
          paths:
            - .
  maestro-upload:
    docker:
      - image: cimg/openjdk:19.0.1
    steps:
      - attach_workspace:
          at: .
      - run:
          name: Upload to Maestro Cloud
          command: |
            curl -Ls "https://get.maestro.mobile.dev" | bash
            export PATH="$PATH":"$HOME/.maestro/bin"
            maestro cloud \
            --apiKey $MDEV_API_KEY \
            --projectId $MDEV_PROJECT_ID \
            --app-file app/build/outputs/apk/debug/app-debug.apk \
            --flows e2e-tests
workflows:
  build-and-upload:
    jobs:
      - build-android
      - maestro-upload:
          requires:
            - build-android
```

{% endtab %}

{% tab title="iOS Pipeline" %}
This example builds an iOS `.app` simulator bundle and uploads it.

```yaml
version: 2.1
jobs:
  build-ios:
    macos:
      xcode: 13.3.1
    environment:
      XCODE_VERSION: "Xcode-13.3.1"
    steps:
      - checkout
      - run:
          name: Switch xcode
          command: sudo xcode-select --switch /Applications/$XCODE_VERSION.app
      - run:
          name: Build iOS app
          command: |
            XCODE_PATH=$(xcode-select -p)
            SIMULATOR_SDKS_AVAILABLE=$(find "$XCODE_PATH/Platforms/iPhoneSimulator.platform/Developer/SDKs/" -type l -maxdepth 1)
            SIMULATOR_SDK_PATH=$(echo "$SIMULATOR_SDKS_AVAILABLE" | head -n1)
            SIMULATOR_SDK=$(basename -s .sdk -a "$SIMULATOR_SDK_PATH" | awk '{print tolower($0)}')
            
            mkdir build
            xcodebuild build \
            -sdk "$SIMULATOR_SDK" \
            -destination 'platform=iOS Simulator' \
            CONFIGURATION_BUILD_DIR=build
      - persist_to_workspace:
          root: .
          paths:
            - .
  maestro-upload:
    docker:
      - image: cimg/openjdk:19.0.1
    steps:
      - attach_workspace:
          at: .
      - run:
          name: Download and run maestro
          command: |
            curl -Ls "https://get.maestro.mobile.dev" | bash
            export PATH="$PATH":"$HOME/.maestro/bin"
            maestro cloud \
            --apiKey $MDEV_API_KEY \
            --projectId $MDEV_PROJECT_ID \
            --app-file build/MyApp.app \
            --flows e2e-tests
workflows:
  build-and-upload:
    jobs:
      - build-ios
      - maestro-upload:
          requires:
            - build-ios
```

{% endtab %}
{% endtabs %}

#### Advanced options

You can customize the upload behavior using additional CLI flags:

* `--name`: Assign a custom name to the upload (e.g., "Pull Request #42").
* `--async`: Exit the CLI immediately after the upload is complete, without waiting for test results.
* `-e`: Pass environment variables (e.g., `-e STAGE=prod`).

For a complete list of options, see the [`cloud` subcommand options](/maestro-cli/maestro-cli-commands-and-options#cloud) in the Maestro CLI documentation.

#### Next steps

Now that your CI pipeline is connected, consider optimizing your cloud runs:

* Set up notifications via [Slack](/maestro-cloud/notifications/set-slack-notification), [email](/maestro-cloud/notifications/set-email-notification), or [webhooks](/maestro-cloud/notifications/configure-webhooks) to stay informed about build and test results.
* [Configure the operating system](/maestro-cloud/environment-configuration/configure-the-os) for your runs to match your application and dependency requirements.
* Define [locales and time zones](/maestro-cloud/environment-configuration/app-locales-and-device-timezones) to ensure consistent behavior across environments and regions.
* Explore all the [subcommand options for `cloud`.](/maestro-cli/maestro-cli-commands-and-options#cloud)


# Generic CI platform

Integrate Maestro Cloud with any CI/CD provider using the CLI. Works with Jenkins, GitLab CI, Azure DevOps, and more.

You can run Maestro Flows in the cloud from any CI platform by using the Maestro CLI. This flexible approach works with Jenkins, GitLab CI, Azure DevOps, and more.

{% hint style="info" %}
**Maestro Cloud Plan required.**

Cloud execution is available on the [Maestro Cloud Plan](https://signin.maestro.dev/sign-up).
{% endhint %}

{% hint style="info" %}
Refer to the appropriate guide if you are using one of the following CI/CD integration options:

* [GitHub Actions](/maestro-cloud/ci-cd-integration/github-actions)
* [Bitrise](/maestro-cloud/ci-cd-integration/bitrise)
* [Bitbucket Pipelines](/maestro-cloud/ci-cd-integration/bitbucket-pipelines)
* [CircleCI](/maestro-cloud/ci-cd-integration/circleci)
  {% endhint %}

### Prerequisites

* **API Key**: Get your API Key in the [Maestro Dashboard](https://app.maestro.dev/).
* **Project ID**: Obtain your Project ID from your project settings in the dashboard.
* **Build Compatibility**:
  * **Android**: APKs must be ARMv8 compatible.
  * **iOS**: Simulator builds must be provided as a `*.app` directory or a zipped `*.app`.

### Integration steps

{% stepper %}
{% step %}
**Organize your Flows**

Add your Flow files to a single directory in your repository.

```
<root>
├── e2e/
│   ├── subflows/
│   │   └── LoginSubflow.yaml
│   ├── Login.yaml
│   └── Search.yaml
```

In this configuration, files in the root of `e2e` run as top-level Flows. Files in subdirectories can be used as subflows that are not executed, but can be used by other flows at the top level.
{% endstep %}

{% step %}

#### Install the Maestro CLI

Ensure the [Maestro CLI](/maestro-cli/how-to-install-maestro-cli) is installed on your CI runner:

```bash
curl -Ls "https://get.maestro.mobile.dev" | bash
```

{% endstep %}

{% step %}

#### Run the cloud command

Execute the `maestro cloud` command as part of your pipeline.

```bash
maestro cloud \
  --api-key "<YOUR_API_KEY>" \
  --project-id "<YOUR_PROJECT_ID>" \
  --name "<uploadName>" \
  --app-file "<APP_FILE>" \
  --flows "./e2e"
```

The following table describes all the parameter you must pass:

| Parameter      | Description                                                                                                                                                                     |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--api-key`    | Your Maestro Cloud API Key.                                                                                                                                                     |
| `--project-id` | Your Maestro Project ID.                                                                                                                                                        |
| `--name`       | **(Optional)** A custom name for this specific upload.                                                                                                                          |
| `--app-file`   | Path to your `.apk` or `.app` binary. Check the [Build your app for the cloud](/maestro-cloud/build-your-app-for-the-cloud) guide for more information on how to build the app. |
| `--flows`      | The directory containing your Flows.                                                                                                                                            |

{% hint style="info" %}
For a complete list of advanced flags, refer to the [Maestro CLI commands and options](/maestro-cli/maestro-cli-commands-and-options#cloud) reference.
{% endhint %}

{% hint style="info" %}
**Troubleshooting: Connection timeouts**

If your CI runner fails to start the Maestro driver within the default timeframe, you may see a timeout error.

The default timeout is 15 seconds (15000 ms) for Android and 120 seconds (120000 ms) for iOS.

You can extend this by setting a custom millisecond value in your pipeline environment. Here's an example to increase the timeout to 3 minutes (180000 ms):

```bash
export MAESTRO_DRIVER_STARTUP_TIMEOUT=180000
```

{% endhint %}
{% endstep %}

{% step %}

#### Handling results

Once your tests finish running, Maestro follows standard CI practices to let you know how they went:

* **Exit Codes**: The `maestro cloud` command returns `0` on success and `1` if any Flow fails. CI platforms typically use these exit codes to mark a build as failed.
* **Reports**: A link to the upload details in the Maestro Console is printed in the terminal logs for every run.
  {% endstep %}
  {% endstepper %}

#### Next steps

Now that your CI pipeline is connected, consider optimizing your cloud runs:

* Set up notifications via [Slack](/maestro-cloud/notifications/set-slack-notification), [email](/maestro-cloud/notifications/set-email-notification), or [webhooks](/maestro-cloud/notifications/configure-webhooks) to stay informed about build and test results.
* [Configure the operating system](/maestro-cloud/environment-configuration/configure-the-os) for your runs to match your application and dependency requirements.
* Define [locales and time zones](/maestro-cloud/environment-configuration/app-locales-and-device-timezones) to ensure consistent behavior across environments and regions.
* Explore all the [subcommand options for `claud`.](/maestro-cli/maestro-cli-commands-and-options#cloud)


# Pull request integration

Native pull request integration runs Maestro tests asynchronously and blocks merges on failures. Supports GitHub Enterprise.

Native pull request integration allows you to run Maestro Flows asynchronously on every code change. You can configure it to block pull requests from being merged if test failures are detected.

{% hint style="info" %}
**Maestro Cloud Plan required.** Pull request integration is available on the [Maestro Cloud Plan](https://signin.maestro.dev/sign-up).
{% endhint %}

{% hint style="info" %}
Maestro Cloud currently supports native pull request integration for:

* **GitHub**
* **GitHub Enterprise** only.
  {% endhint %}

{% stepper %}
{% step %}

#### Trigger uploads on every pull request

You must configure your CI environment to trigger a Maestro Cloud upload for every pull request. You can accomplish this by using one of the following options:

{% tabs %}
{% tab title="GitHub Actions" %}
Trigger your workflow on `pull_request` events against your baseline branch.

{% code title="" %}

```yaml
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
```

{% endcode %}
{% endtab %}

{% tab title="Maestro CLI" %}
Add the `--async` flag and PR-specific metadata to your `maestro cloud` command.

```bash
maestro cloud \
  --async \
  --api-key <YOUR_API_KEY> \
  --branch <BRANCH_NAME> \
  --repo-owner <REPO_OWNER> \
  --repo-name <REPO_NAME> \
  --pull-request-id <PR_ID> \
  --commit-sha <COMMIT_SHA> \
  --app-file <APP_FILE> --flows .maestro/
```

The following table describes each one of the required flags you must inform.

| Argument            | Description                                                                                                                    |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `--branch`          | The name of the git branch being tested (e.g., `feature-branch`). Maestro uses this to group results by branch in the console. |
| `--repo-owner`      | The owner (individual or organization) of the repository on GitHub. Required for posting status checks.                        |
| `--repo-name`       | The name of the repository. Combined with `--repo-owner`, this identifies the target project for integration.                  |
| `--pull-request-id` | The number of the pull request. Essential for linking the test run to a specific PR and enabling automated comments.           |
| `--commit-sha`      | The full 40-character SHA of the commit. This ensures test results are associated with the exact state of the code.            |
| {% endtab %}        |                                                                                                                                |

{% tab title="API" %}
If using the upload API directly, include the following fields in the JSON payload:

* `branch`
* `repoOwner`
* `repoName`
* `pullRequestId`
* `commitSha`

The following table describes each one of the required fields you must inform.

| Field           | Description                                                                                                                    |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `branch`        | The name of the git branch being tested (e.g., `feature-branch`). Maestro uses this to group results by branch in the console. |
| `repoOwner`     | The owner (individual or organization) of the repository on GitHub. Required for posting status checks.                        |
| `repoName`      | The name of the repository. Combined with `--repo-owner`, this identifies the target project for integration.                  |
| `pullRequestId` | The number of the pull request. Essential for linking the test run to a specific PR and enabling automated comments.           |
| `commitSha`     | The full 40-character SHA of the commit. This ensures test results are associated with the exact state of the code.            |
| {% endtab %}    |                                                                                                                                |
| {% endtabs %}   |                                                                                                                                |
| {% endstep %}   |                                                                                                                                |

{% step %}

#### Grant access to pull requests

Maestro requires permission to update pull request statuses. To accomplish this, you must install the [Maestro Cloud app](https://github.com/apps/maestro-cloud-app) and grant access to your repositories to the desired repositories.
{% endstep %}

{% step %}

#### Test the integration

Once configured, open a pull request. The Maestro Cloud status check appears in the checks section. A passing check indicates all Flows ran successfully, while a failing check indicates that at least one Flow failed.

<figure><img src="/files/0UsMoz6OCCqTQWLjC7sI" alt=""><figcaption></figcaption></figure>
{% endstep %}
{% endstepper %}


# Environment configuration


# Configure the OS

Set OS versions and device models for Maestro Cloud tests using the \`--device-os\` and \`--device-model\` flags.

Maestro Cloud allows you to test your application across multiple Android and iOS versions. This ensures your app remains compatible and performs reliably across different mobile environments.

{% hint style="info" %}
**Maestro Cloud Plan required.** OS configuration options are available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

### Full list of supported Cloud devices

You can access the full list of supported Cloud devices and operating systems by using the following command:

```bash
maestro list-cloud-devices
```

### Android

You can specify the Android OS version using the `--device-os` flag when using Maestro Cloud:

```bash
maestro cloud --device-os "<DEVICE_OS>" --app-file "<APP_FILE>" --flows "<FLOWS>"
```

For example, to run your flows on Android 14:

```bash
maestro cloud --device-os "android-34" --app-file myapp.apk --flows myflows/
```

Run `maestro list-cloud-devices` to see the full list of supported Android OS versions.

{% hint style="info" %}
**Reproducing Cloud runs locally**

To closely reproduce Maestro Cloud runs for Android on your local machine, create an emulator of the same API version and use the Google APIs variant (not Google Play).
{% endhint %}

### iOS

For iOS, you can configure both the runtime version and the device model.

Maestro recommends specifying both the minor OS version and the device model. Use the `--device-os` and `--device-model` flags to select a specific combination:

```bash
maestro cloud --device-os "<DEVICE_OS>" --device-model "<DEVICE_MODEL>" --app-file "<APP_FILE>" --flows "<FLOWS>"
```

For example, to run your flows on iOS 26.2 using an iPhone 17 Pro, use the following command:

```bash
maestro cloud --device-os "iOS-26-2" --device-model "iPhone-17-Pro" --app-file myapp.app --flows myflows/
```

Run `maestro list-cloud-devices` to see the full list of supported iOS device models and OS versions.

{% hint style="info" %}
**Deprecated: `--ios-version` & `--android-api-level`**

The `--ios-version` and `--android-api-level` options are deprecated and will be removed in a future release. Use the `--device-os` and `--device-model` flags instead.

Previously, the `--ios-version` flag allowed you to specify only a major version (for example, `16`, `17`, or `18`). When using this flag, Maestro Cloud automatically runs your flows on an **iPhone 11** simulator.
{% endhint %}

### Related content

Now that you understand how configure the OS, explore other ways to customize your test environment:

* [App locales and device timezones](/maestro-cloud/environment-configuration/app-locales-and-device-timezones): Select the device locale for Maestro Cloud tests.
* Set up notifications via [Slack](/maestro-cloud/notifications/set-slack-notification), [email](/maestro-cloud/notifications/set-email-notification), or [webhooks](/maestro-cloud/notifications/configure-webhooks) to stay informed about build and test results.


# App locales and device timezones

Configure app locales and understand default device timezones in Maestro Cloud to test internationalization and time-dependent features.

Maestro Cloud allows you to configure specific app locales and understand the default timezone settings for its cloud environments. This is essential for testing internationalization (i18n) and features that depend on localized data or time.

{% hint style="info" %}
**Maestro Cloud Plan required.**

Locale configuration features are available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

### Configure app locale

Use the `--device-locale` parameter with the `maestro cloud` command to automatically change the device locale for an upload.

The parameter value must follow the format `[`[`ISO-639-1`](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) `language code]_[`[`ISO-3166-1`](https://en.wikipedia.org/wiki/ISO_3166-1) `country code]`. Below you find two examples:

```bash
# Set device locale to German (Germany)
maestro cloud --device-locale de_DE --app-file <APP_FILE> --flows <WORKSPACE>

# Set device locale to Japanese (Japan)
maestro cloud --device-locale ja_JP --app-file <APP_FILE> --flows <WORKSPACE>
```

If the parameter is omitted, the default locale is `en_US`.

{% hint style="info" %}
**Locales supported by Maestro**

Check the [documentation](/maestro-flows/flow-control-and-logic/test-in-different-locales/locales-supported-by-maestro) to find the complete list of supported locales.
{% endhint %}

### Default device timezones

All Maestro Cloud instances are physically located in Las Vegas, USA. While the physical location is the same for all devices, the default timezones differ between platforms:

| Platform    | Default Timezone | Offset |
| ----------- | ---------------- | ------ |
| **Android** | UTC              | +00:00 |
| **iOS**     | Pacific Time     | GMT -7 |

It is important to note that these timezones are fixed and cannot currently be configured via CLI flags or configuration files. If your tests depend on specific time-of-day logic, ensure your assertions account for these offsets.

* **Android:** Regardless of the physical host region, the Android emulator defaults to UTC.
* **iOS:** iOS simulators inherit the system time of the host macOS instance, which is set to GMT -7.

### Related content

Now that you understand how locales and timezones work in the Maestro Cloud, explore other ways to customize your test environment:

* [Configure the OS](/maestro-cloud/environment-configuration/configure-the-os): Run your tests on specific Android API levels or iOS versions.
* Set up notifications via [Slack](/maestro-cloud/notifications/set-slack-notification), [email](/maestro-cloud/notifications/set-email-notification), or [webhooks](/maestro-cloud/notifications/configure-webhooks) to stay informed about build and test results.


# Notifications


# Set Slack notification

Connect Slack to Maestro Cloud for test result notifications after each upload. Option to notify on failed flows only.

Configure Maestro Cloud to notify you and your team in Slack about test results for a specific project.

{% hint style="info" %}
**Maestro Cloud Plan required.** Slack notifications are available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

### Connect Slack to Maestro Cloud

Follow these steps to integrate Slack with your Maestro project:

1. Log in to the [Maestro Dashboard](https://app.maestro.dev/).
2. Click **Settings** in the sidebar.
3. Select the project for which you want to receive Slack notifications.
4. Click **Connect Slack**.

<figure><img src="/files/86aDfyMsO8ig8SrJ0PM5" alt=""><figcaption></figcaption></figure>

5. You are redirected to Slack. Select the workspace and channel where you want to receive notifications, then click **Allow**.

<figure><img src="/files/gY143GXsSWGvFmrpte6X" alt=""><figcaption></figcaption></figure>

6. After authorization, you are redirected back to the Maestro Console. The integration is now enabled, and you will start receiving Slack notifications.

<figure><img src="/files/K5lQ7QeehtjmS8BZNNHF" alt=""><figcaption></figcaption></figure>

### Receive notifications

Once the integration is active, Maestro posts a message to your selected Slack channel after each upload finishes, regardless of whether the test succeeds or fails.

<figure><img src="/files/qppk9gJ2193aDyIGlMUG" alt=""><figcaption></figcaption></figure>

You have the option to configure Maestro to share updates only from failed tests. To enable this configuration:

1. Log in to the [Maestro Dashboard](https://app.maestro.dev/).
2. Click **Settings** in the sidebar.
3. Select the project you want to configure.
4. Enable **Send notifications for failed flows only**.

<figure><img src="/files/LyGsIWeoFi9TeIAZCJRz" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
**Multi-project routing**

You can connect different projects to different Slack channels. This allows you to route notifications to the specific teams responsible for each project (e.g., `#android-alerts` for your Android project and `#ios-alerts` for your iOS project).
{% endhint %}

### Manage the integration

You can disable the integration or change the notification channel at any time from the **Settings** page in the Maestro Console.

### Related content

Check the other notification options available when testing your app with Maestro Cloud:

* [Set email notification](/maestro-cloud/notifications/set-email-notification)
* [Configure webhooks](/maestro-cloud/notifications/configure-webhooks)


# Set email notification

Configure email notifications in config.yaml for Maestro Cloud. Default sends on failure only; add onSuccess for successful runs.

Configure Maestro Cloud to send email summaries for your Flow results.

{% hint style="info" %}
**Maestro Cloud Plan required** Email notifications are available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

### Configure email recipients

To receive email notifications, add the `notifications` mapping to your `config.yaml` file. This file should be located in the root of your workspace (usually the `.maestro` folder).

#### Notify on failure (Default)

By default, Maestro Cloud sends emails only when a Flow fails. Add the following to your `config.yaml`:

```yaml
# .maestro/config.yaml
notifications:
  email:
    enabled: true
    recipients:
      - dev-team@example.com
      - qa-lead@example.com
```

#### Notify on success and failure

If you want to receive notifications for successful runs as well, add `onSuccess: true`:

```yaml
# .maestro/config.yaml
notifications:
  email:
    enabled: true
    onSuccess: true # Enable on sucess notification
    recipients:
      - dev-team@example.com
```

### Example email notification

When a Flow fails, recipients receive an email containing a summary of the test run and a link to the detailed report in the Maestro Dashboard.

<figure><img src="/files/McLI3irOQbYfaLF0CFEn" alt=""><figcaption></figcaption></figure>

### Related content

Check the other notification options available when testing your app with Maestro Cloud:

* [Set Slack notification](/maestro-cloud/notifications/set-slack-notification)
* [Configure webhooks](/maestro-cloud/notifications/configure-webhooks)


# Configure webhooks

Enable webhooks for real-time POST notifications of Maestro Cloud upload results to external services. Supports multiple webhooks and token auth.

Enable webhooks to send real-time notifications about upload results from a specific Maestro project to your custom workflows, monitoring systems, or external services.

{% hint style="info" %}
**Maestro Cloud Plan required.** Webhook notifications are available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

### Set up a webhook

Follow these steps to configure a webhook in the Maestro:

1. Log in to the [Maestro Dashboard](https://app.maestro.dev/).
2. Click **Settings** in the sidebar.
3. Select the project for which you want to configure webhooks.
4. Under **Webhook Management**, enter your webhook URL. You must provide the full URL where Maestro should send POST requests.

<figure><img src="/files/EDu8TM5wZe7k7txhef3B" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
Ensure that your webhook endpoint can handle POST requests and is publicly accessible.
{% endhint %}

If you need to authenticate webhook requests from Maestro Cloud, you can use the **Webhook Token** generated after you add the webhook URL. Use this token in your webhook endpoint as a **Bearer token** to authenticate requests from Maestro.

<figure><img src="/files/D3JMxLnhVBDSktUae5ad" alt=""><figcaption></figcaption></figure>

You can update URLs or tokens, or disable an integration at any time from the settings page.

{% hint style="success" %}

#### **Multiple Webhooks**

You can configure multiple webhooks for the same project to trigger different services.
{% endhint %}

You can update URLs or tokens, or disable an integration at any time from the settings page.

### Webhook payload example

When an upload event occurs, Maestro sends a POST request with a JSON payload. The payload also includes [tags](/maestro-flows/workspace-management/test-discovery-and-tags) and [custom properties](/maestro-flows/workspace-management/test-reports-and-artifacts) defined in your flows, allowing you to filter and route events if necessary.

Below is an example of the data sent:

```json
{
  "id": "mupload_01kghgqmsqfa38z5gjpxpz3wv5",
  "name": "Upload 7",
  "url": "https://app.maestro.dev/project/proj_01kdze2nbdfactc52yg9jqdb0n/maestro-test/app/app_01kgfx3k6gfx4t84qyr8f3q9nj/upload/mupload_01kghgqmsqfa38z5gjpxpz3wv5",
  "githubBranch": null,
  "envVariables": {
    "MAESTRO_FILENAME": "android-advanced-flow"
  },
  "platform": "ANDROID",
  "deviceModel": "iPhone-17-Pro-Max",
  "osVersion": "iOS-26-2",
  "deviceLocale": "en_US",
  "appId": "app_01kgfx3k6gfx4t84qyr8f3q9nj",
  "startTime": 1770114514724,
  "endTime": 1770114503499,
  "flows": [
    {
      "id": "run_01kghgqmtbeb6afhnchgxcck48",
      "name": "android-advanced-flow",
      "url": "https://app.maestro.dev/project/proj_01kdze2nbdfactc52yg9jqdb0n/maestro-test/flow/run_01kghgqmtbeb6afhnchgxcck48",
      "status": "SUCCESS",
      "failureReason": null,
      "startTime": 1770114514724,
      "endTime": 1770114569337,
      "tags": [
        "release",
        "critical-path"
      ],
      "properties": {
        "jira_ticket": "ENG-402",
        "deployment_env": "staging"
      },
      "videoUrl": "https://storage.googleapis.com/.../screen-recording.mp4?X-Goog-Signature=..."
    }
  ]
}
```

Each flow includes a `videoUrl` field: a signed link to the flow's screen recording (`screen-recording.mp4`), valid for 7 days. It is available for all platforms (Android, iOS, and web). The value is `null` when the flow has no recording — for example, if the recording failed.

### Related content

Check the other notification options available when testing your app with Maestro Cloud:

* [Set Slack notification](/maestro-cloud/notifications/set-slack-notification)
* [Set email notification](/maestro-cloud/notifications/set-email-notification)


# Advanced features


# Manage secrets

Pass sensitive parameters like usernames and passwords to Maestro Cloud tests via environment variables using the -e CLI option.

Avoid storing sensitive values directly in your Flow files. Maestro allows you to pass parameters as environment variables during execution.

{% hint style="info" %}
**Maestro Cloud Plan required** Secret management via environment variables is available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

### Define environment variables

You can provide environment variables using the Maestro CLI or through CI integrations like GitHub Actions.

{% tabs %}
{% tab title="Maestro CLI" %}
Use the `-e` option to pass parameters as key-value pairs:

```bash
maestro cloud \
  --api-key "<YOUR_API_KEY>" \
  --project-id "<YOUR_PROJECT_ID>" \
  -e USERNAME=$TEST_USERNAME \
  -e PASSWORD=$TEST_PASSWORD \
  --app-file "<APP_FILE>" \
  --flows "<FLOW_OR_FOLDER>"
```

{% endtab %}

{% tab title="GitHub Action" %}
You can provide parameters in the multiline `env` field of the GitHub Action:

```yaml
- uses: mobile-dev-inc/action-maestro-cloud@v1
  with:
    api-key: ${{ secrets.MOBILE_DEV_API_KEY }}
    app-file: <path to APK or iOS Simulator build>
    env: |
        USERNAME=${{ secrets.TEST_USERNAME }}
        PASSWORD=${{ secrets.TEST_PASSWORD }}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
For more information about how to use parameters and constants in your Flow, access the [documentation](/maestro-flows/flow-control-and-logic/parameters-and-constants).
{% endhint %}

### Use variables in your Flows

Once defined, reference these variables in your Flow files using the `${VARIABLE_NAME}` syntax:

```yaml
appId: com.example.app
---
- launchApp
- inputText: ${USERNAME}
- tapOn: Next
- inputText: ${PASSWORD}
- tapOn: Login
```


# Reuse app binary

Use --app-binary-id to skip re-uploading your app and save time. Find the binary ID in CLI output or Maestro dashboard.

To run multiple test scenarios on the same build, you can reuse a previously uploaded binary instead of re-uploading the same file. This optimization saves time and bandwidth.

{% hint style="info" %}
**Maestro Cloud Plan required.**

App binary reuse is available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

### Find the app binary ID

When you upload an app to Maestro Cloud, a unique app binary ID is generated. You can find this ID in two locations:

* **CLI output**: The ID is returned in the terminal response immediately after a successful upload.

<figure><img src="/files/Syi8pecMYAw67jRHXvMs" alt=""><figcaption></figcaption></figure>

* **Maestro dashboard**: The ID is visible at the top of the run details page.

<figure><img src="/files/b3JRww0X7uVYjbMksq5A" alt=""><figcaption></figcaption></figure>

### Use the app binary ID

Use the `--app-binary-id` flag to reference a cached binary for subsequent test runs. You must copy the ID form the CLI or from the dashboard and pass it when running the test:

```bash
maestro cloud \
  --api-key <YOUR_API_KEY> \
  --project-id <YOUR_PROJECT_ID> \
  --app-binary-id <BINARY_ID> \
  .maestro/
```

This tells Maestro to skip the binary re-upload and go straight to execution.

### Security

Reusing an app binary is a performance optimization and does not compromise security.

* **Access**: Only you or authorized members of your project can access the binary from your previous uploads.
* **Privacy**: All cloud devices are wiped after every test execution. Even after the test completes, no one, including the Maestro team, has access to the running application data.

### Related content

* [Maestro CLI reference](/maestro-cli/maestro-cli-commands-and-options): Full list of all available parameters.


# IP allowlist

Static IP addresses used by Maestro Cloud to connect to your services. Add to your firewall or ACLs for access.

{% hint style="info" %}
**Maestro Cloud Plan required.** The IP allowlist feature is available on the [Maestro Cloud Plan](https://maestro.dev/cloud).
{% endhint %}

If your network requires external services to be on an allowlist to permit access, update your Access Control Lists (ACLs) with the following IP addresses.

These IP addresses are used by Maestro Cloud to connect to your services:

* 207.254.42.234
* 34.127.79.8
* 35.247.62.137
* 34.83.16.33

{% hint style="info" %}
These IP addresses are static, but Maestro reserves the right to add more in the future. To be notified of future changes, contact Maestro support to join the mailing list.
{% endhint %}


# Automatic Retries

There are situations where Maestro Cloud will automatically retry a test execution

### Smart Retries

We automatically retry tests that we consider might be a flaky test - perhaps there was a network hiccup, a timing issue in your test, or something happened on your test environment's backend.

We automatically retry a failed run when we detect that:

1. the previous run of the same flow (as determined by the `name`, or the filename if there isn't one) succeeded (irregardless of branch source
2. it used the exact same target device (os/model)

### Infrastructure-triggered retries

Operating systems are complex, and our device hosts have a virtual device operating system running on top. We're regularly improving device behaviours, but sometimes hiccups occur on a machine or on a network. When this happens and we detect that it was probably infrastructural, we automatically retry your test on a different machine.


# Limits

Maestro Cloud applies a 20-minute soft limit per test execution. Break longer suites into smaller, parallelizable Flows.

Maestro Cloud applies a **soft limit of 20 minutes** per test execution.

{% hint style="warning" %}
After 20 minutes of execution, your test may be stopped at any time.
{% endhint %}

Ensure your Flows are optimized for speed and efficiency to stay within this limit. For longer test suites, consider breaking them into smaller, parallelizable Flows.


# Cloud commands

Upload and execute your mobile tests on hosted cloud infrastructure using the Maestro CLI cloud subcommand.

Maestro does not provide a separate "Cloud CLI." To take advantage of Maestro Cloud features, you use the `cloud` subcommand within the standard [Maestro CLI](https://docs.maestro.dev/maestro-cli/). This subcommand uploads your app and tests to our cloud infrastructure and enables hosted test execution

#### Maestro CLI Reference

Because the `cloud` command is part of the core Maestro CLI, all available flags, options, and global settings are documented in a centralized reference.

To explore the full list of parameters you can use to customize your cloud runs, visit the [Maestro CLI commands and options](/maestro-cli/maestro-cli-commands-and-options) reference page.


# Maestro Flows overview

Maestro Flows are YAML-based test scripts that define user journeys for UI automation.

Maestro Flows are the fundamental building blocks of UI automation, representing specific segments of a user journey, such as Login, Checkout, or Search for an item, within an application. Flows can represent complete user journeys or discrete functional components that can be composed into larger test scenarios. By modeling real-world user interactions, Flows provide a reliable, repeatable way to verify app behavior across Android, iOS, and Web platforms from a single suite.

#### The anatomy of a Flow

Flows use a human-readable YAML format designed to be maintained by both developers and manual testers without heavy programming knowledge.

A standard Flow consists of two distinct parts separated by three dashes (`---`):

* **Configuration Section**: Defined at the top (above the `---` marker), this includes the `appId` of the app under test, along with optional metadata like `name`, `tags`, and environment variables.
* **Commands Section**: A sequence of declarative commands, such as `tapOn` and `inputText`, that simulate user actions and validate the UI state.

```yaml
# --- Configuration Section ---
appId: com.example.app         # Mandatory: Define the ID of the app under test
name: My Login Flow            # Optional: Customize the Flow name
tags:                          # Optional: Filter which tests to run
  - smoke-test
env:                           # Optional: Map of environment variables
  USERNAME: "user@example.com"

---
# --- Commands Section ---
- launchApp                    # Launches the application
- tapOn: "Username"            # Interacts with the username field
- inputText: ${USERNAME}       # Inputs the environment variable
- tapOn: "Login"               # Taps the login button
- assertVisible: "Welcome"     # Verifies success message appears
```

### Explore Flows capabilities

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-arrow-progress">:arrow-progress:</i></td><td><strong>Flow control and logic</strong></td><td>Build resilient, intelligent journeys. Master the architectural tools used to scale your tests, including modular subflows, conditional branching, repetitive loops, and lifecycle hooks.</td><td><a href="/pages/KsAwaeHz7lKgM9s7EMkI">/pages/KsAwaeHz7lKgM9s7EMkI</a></td></tr><tr><td><i class="fa-node-js">:node-js:</i></td><td><strong>JavaScript</strong></td><td>Extend YAML with custom scripting. Use the integrated JavaScript sandbox to handle complex data, generate random test variables, capture script outputs, and interact with external APIs via HTTP requests.</td><td><a href="/pages/N72iCVWiRWYBZ8BruPC7">/pages/N72iCVWiRWYBZ8BruPC7</a></td></tr><tr><td><i class="fa-gears">:gears:</i></td><td><strong>Workspace management</strong></td><td>Transition from writing commands to designing a testing system. Learn to configure global behaviors with <code>config.yaml</code>, organize repository architectures, and manage test execution and analysis at scale.</td><td><a href="/pages/KFSFrrC0ZCkU35FT0Y7B">/pages/KFSFrrC0ZCkU35FT0Y7B</a></td></tr></tbody></table>

### Next step

Learn how to build resilient, intelligent journeys by mastering [Flow control and logic](/maestro-flows/flow-control-and-logic/flow-control-and-logic-overview).


# Flow control and logic overview

Overview of flow control: conditions, loops, nested flows, and hooks.

While standard Flows represent linear user journeys, real-world testing often requires dynamic behavior. This section covers the tools that allow your tests to adapt to different application states, platform requirements, and data environments.

### Core pillars of Flow architecture

To build scalable tests, you will utilize five pillars:

#### **Selection**

Identify UI elements with precision. Before building the test logic, you must master [how to use Selectors](/maestro-flows/flow-control-and-logic/how-to-use-selectors) to ensure Maestro can reliably find and interact with the correct parts of your application screen.

#### **Modularity**

Avoid repeating yourself, you can use [Nested Flows](/maestro-flows/flow-control-and-logic/nested-flows) to extract common journeys, like Login or Onboarding, into separate files. You can then call these "subflows" across your entire suite using the `runFlow` command.

#### **Conditional execution**

Not every test step should run every time. [Conditions](/maestro-flows/flow-control-and-logic/conditions) allow you to execute actions based on element visibility, platform (iOS vs. Android), or custom JavaScript expressions.

#### **Repetition**&#x20;

Handle dynamic lists or "polling" scenarios with [Loops ](/maestro-flows/flow-control-and-logic/loops)to repeat actions until a goal is met. Complement this with [Wait Commands](/maestro-flows/flow-control-and-logic/wait-commands) to ensure your tests only proceed when the UI is stable, eliminating flakiness from slow network loads.

#### **Environment and l**ifecycle

Manage test data externally using [parameters and constants](/maestro-flows/flow-control-and-logic/parameters-and-constants), allowing the same Flow to run in Staging, QA, or Production. Use [Hooks ](/maestro-flows/flow-control-and-logic/hooks)(`onFlowStart`, `onFlowComplete`) to automate setup and teardown tasks, such as clearing app state or granting [Permissions](/maestro-flows/flow-control-and-logic/permissions).


# How to use Selectors

Learn to identify UI elements using text, ID, position, and state selectors.

Selectors are the foundational logic Maestro uses to identify UI elements. By default, Maestro interacts with the Accessibility Tree, meaning it sees the application much like an end-user or an assistive device would.&#x20;

### Interaction guide

Most Maestro commands, such as `tapOn`, `assertVisible`, `copyTextFrom`, and `scrollUntilVisible`, require a selector to know which part of the screen to act upon. You can define these selectors using two distinct formats.

When you only need to match an element by its visible text, you can use a simple string. This is the fastest way to write readable tests for static content.

```yaml
- tapOn: Login # Maestro automatically interprets this as { text: "Login" }
```

For higher precision, you can combine multiple attributes into a single Selector block. Maestro will only match an element that satisfies all the provided conditions (an **AND** logic). This is essential when dealing with dynamic UIs, multiple similar buttons, or elements that change state.

```yaml
- tapOn:
    id: submit_button     # Technical ID
    enabled: true         # Only if it's clickable
    below: Password       # Located in a specific area
```

### Best practices

To ensure your test suite remains maintainable and flake-free as your app evolves, follow these strategic principles:

* **Prioritize User-Visible Text**: Whenever possible, use `text` selectors. This ensures your tests validate what the user actually sees and improves test readability by making selectors self-documenting. If the text changes and breaks the test, it usually means the user experience has changed as well.
* **Use IDs for Stability**: For icons, images, or apps supporting multiple languages (localization), use `id` (Accessibility Identifiers). These are hidden from the user but remain constant across different languages.
* **Anchor with Relational Logic**: If an element is dynamic or lacks a unique ID, find a stable "anchor" (like a section header) and use relational selectors (e.g., `below: "Personal Information"`) to pinpoint the target.
* **Handle Dynamic Values with Regex**: Since `text` and `id` are regex-based by default, use patterns like `.*` to handle strings that varies.
* **Verify State Before Action**: When tapping a button that depends on an API call or form validation, include `enabled: true` in your selector. Maestro will automatically wait for the button to become interactive before attempting the tap.

### Selector reference

Explore these specialized reference pages to find the right tool for every UI scenario:

| Category                                                          | Best for                                                                               |
| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [Core Selectors](/reference/selectors/core-selectors)             | The essentials: `text`, `id`, `index`, `point`, and `css` (web).                       |
| [Relational Selectors](/reference/selectors/relational-selectors) | Finding elements based on visual position (`above`, `below`) or structure (`childOf`). |
| [Element Traits](/reference/selectors/element-traits)             | Filtering by physical characteristics like `square` or `long-text`.                    |
| [State Selectors](/reference/selectors/state-selectors)           | Verifying functional status (`enabled`, `checked`, `focused`, or `selected`).          |
| [Dimension Matchers](/reference/selectors/dimension-matchers)     | Targeting by physical size (`width`, `height`) with `tolerance`.                       |


# Nested flows

Reuse test logic by calling subflows from parent flows with runFlow.

As your test suite grows, you'll find that certain sequences of commands are repeated across multiple flows. Common examples include logging in, clearing app state, or navigating to a specific screen.

Instead of duplicating these commands in every file, you can define them once in a separate flow and run them using the [`runFlow` ](/reference/commands-available/runflow)command. This approach, known as nesting flows, helps you adhere to the DRY (Don't Repeat Yourself) principle, making your tests easier to read, maintain, and optimize.

### Basic usage

To run a subflow, use the `runFlow` command followed by the path to the flow file.

In this example, the `login.yaml` subflow handles the entire login process for the Wikipedia Android app, including launching the app and navigating the menu. The main flow `profile_test.yaml` runs this subflow to authenticate before performing other checks.

{% tabs %}
{% tab title="tests/profile\_test.yaml (The main flow)" %}

```yaml
appId: org.wikipedia
---
- launchApp
- runFlow: ../common/login.yaml 
- assertVisible: "Explore"
```

{% endtab %}

{% tab title="common/login.yaml (The subflow)" %}

```yaml
appId: org.wikipedia
---
- tapOn:
    id: drawer_icon_menu
- tapOn: LOG IN / JOIN WIKIPEDIA
- tapOn: Username
- inputText: myUsername
- tapOn: Password
- inputText: myPassword
- tapOn: LOG IN
```

{% endtab %}
{% endtabs %}

### Passing arguments

You can make your nested flows dynamic by passing arguments. This allows you to reuse the same logic with different data, such as logging in with different user roles.

Use the `env` key to pass variables to the subflow. Inside the subflow, you can access these variables using the `${VAR_NAME}` syntax.

{% tabs %}
{% tab title="tests/profile\_test.yaml (The main flow)" %}

```yaml
appId: org.wikipedia
---
- launchApp
- runFlow:
    file: ../common/login.yaml
    env:
        USERNAME: "myUser"
        PASSWORD: "myPassword"
- assertVisible: "Explore"
```

{% endtab %}

{% tab title="common/login.yaml (The subflow)" %}

```yaml
appId: org.wikipedia
---
- tapOn:
    id: drawer_icon_menu
- tapOn: LOG IN / JOIN WIKIPEDIA
- tapOn: Username
- inputText: ${USERNAME}
- tapOn: Password
- inputText: ${PASSWORD}
- tapOn: LOG IN
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}

#### Conditional execution

You can optimize your test execution by running nested flows only when specific conditions are met.

To learn more about conditional execution, access the [Conditions ](/maestro-flows/flow-control-and-logic/conditions)page.
{% endhint %}

### Best practices for optimization

Adopting a nested flow strategy directly contributes to an optimized testing process.

#### Keep flows atomic

Each nested flow should perform a single, well-defined task. For example, have separate flows for `login.yaml`, `logout.yaml`, and `onboarding.yaml`. This granular approach allows you to mix and match flows to create complex test scenarios without unnecessary overhead.

#### Organized directory structure

Maintain a clean project structure by grouping reusable flows. A common pattern is to have a `subflows/` or `common/` directory separate from your main test cases.

```
.
├── flows/
│   ├── e2e/
│   │   ├── checkout_flow.yaml
│   │   └── profile_flow.yaml
│   └── subflows/
│       ├── login.yaml
│       └── payment_setup.yaml
```

#### Setup and teardown

Use nested flows for setup and teardown routines. By isolating these steps, you can quickly adjust the initial state for all tests by modifying a single file. For instance, if the login UI changes, you only need to update `subflows/login.yaml`, and all tests using it will automatically work.

### Next steps

Check the [runFlow ](/reference/commands-available/runflow)command for technical details, or explore [Conditions](/maestro-flows/flow-control-and-logic/conditions) to add logic to your flows.


# Wait commands

Control timing with wait commands for animations, network calls, and UI updates.

In a perfect world, apps respond instantly. In reality, network latency, slow animations, and background processing can cause tests to fail because Maestro tries to interact with an element that isn't ready yet.

Maestro provides several ways to handle these timing issues. This guide helps you choose the right strategy to make your Flows resilient and fast.

### Automatic waiting

Before using a dedicated wait command, remember the golden rule of Maestro timing:

> If you expect an element to appear or disappear within a short period, use an assertion.

Maestro’s assertions are smart. They don't just check once and fail, they poll the UI continuously until the element appears or the timer expires. This makes them the most efficient way to wait because the test continues immediately after the condition is met.

* [`assertVisible`](/reference/commands-available/assertvisible): Best for waiting for a screen to load, a success message to appear, or a button to become active.
* [`assertNotVisible`](/reference/commands-available/assertnotvisible): Best for waiting for a loading spinner to disappear or a modal to close.

```yaml
# assertVisible example
- tapOn: "Submit"
- assertVisible: "Success!" # Maestro will wait for this to appear
```

### Wait strategies

When assertions aren't enough, such as for long-running processes or stabilizing complex animations, you can use one of the specific wait commands.

#### Waiting for long processes (`extendedWaitUntil`)

Use [`extendedWaitUntil`](/reference/commands-available/extendedwaituntil) for slow network responses (like processing a payment or generating a large report) that are guaranteed to take longer than a few seconds.

This command optimizes your test because Maestro moves on immediately if the element appears faster than the timeout.

```yaml
- extendedWaitUntil:
    visible: "Payment Confirmed"
    timeout: 30000            # Wait up to 30 seconds
```

{% hint style="success" %}

#### **Set realistic timeouts**

Avoid setting every timeout to 60 seconds. If a screen should load in 5 seconds, let the default assertion handle it. This helps catch performance regressions early.
{% endhint %}

#### Waiting for UI stability (`waitForAnimationToEnd`)

Sometimes elements are visible but still moving (e.g., a list sliding into place or a side menu opening). Interacting too early can cause missed taps.

Use the [`waitForAnimationToEnd`](/reference/commands-available/waitforanimationtoend) command to ensure the test continues only after the animation finishes.

```yaml
- waitForAnimationToEnd:
    timeout: 5000 # Wait up to 5s for movement to stop
```

{% hint style="success" %}

#### Ghost taps

If you experience "ghost taps" (tapping a button that exists but isn't yet clickable), combine your wait logic with [`retryTapIfNoChange: true`](/reference/commands-available/tapon#retry-a-tap-if-the-ui-is-unresponsive) in your `tapOn` command.
{% endhint %}

### Next steps

For full technical details and parameter lists, visit the individual command pages:

* [`assertVisible`](/reference/commands-available/assertvisible): Your primary tool for standard waits.
* [`assertNotVisible`](/reference/commands-available/assertnotvisible): For waiting for elements to disappear.
* [`extendedWaitUntil`](/reference/commands-available/extendedwaituntil): For smart, long-duration waiting.
* [`waitForAnimationToEnd`](/reference/commands-available/waitforanimationtoend): For stabilization after UI transitions.

Or continue learning about flow control by exploring the [Loops](/maestro-flows/flow-control-and-logic/loops) or [Conditions](/maestro-flows/flow-control-and-logic/conditions) guides.


# Loops

Repeat actions using the repeat command for iterative test scenarios.

Automating repetitive tasks is one of the primary benefits of end-to-end testing. Whether you are adding multiple items to a cart, deleting a list of messages, or performing bulk data entry, loops allow you to execute sequences of commands efficiently without duplicating code.

In Maestro, loops are handled via the [`repeat`](/reference/commands-available/repeat) command. This guide will teach you how to use fixed and conditional loops to create dynamic and resilient Flows.

### Loop strategies

Maestro offers three main ways to loop through commands. Choosing the right one depends on whether you know the exact iteration count or if the loop depends on the dynamic state of the UI.

#### Fixed iterations (`times`)

Use this when you have a specific, known number of actions to perform. This is ideal for stress-testing a specific interaction or creating a specific number of items.

```yaml
# Add 5 items to a list
- repeat:
    times: 5
    commands:
      - tapOn: "Add Item"
      - tapOn: "Save"
```

#### Conditional loops (`while`)

Use this when you don't know the exact count, but you know the state that should stop the loop. This is useful for handling dynamic content that may vary between test runs, like pagination.

```yaml
# Delete messages until the "Inbox Empty" text appears
- repeat:
    while:
      notVisible: "Your inbox is empty"
    commands:
      - tapOn: "Delete Message"
      - tapOn: "Confirm"
```

{% hint style="success" %}

#### **UI stability**

If your loop involves fast tapping or rapid screen changes, consider adding a `waitForAnimationToEnd` inside the `commands` list to ensure the UI is ready for the next iteration.
{% endhint %}

#### The smart loop

It is a best practice to provide a safety net in automation. By combining `times` and `while`, you ensure that a loop terminates even if the expected UI state is never reached (e.g., due to a bug or network error).

Suppose you want to dismiss all "Update" notifications, but you want to limit the test to 10 dismissals to prevent an infinite loop if the notifications keep regenerating. In the following example, the loop terminates if the notifications are gone or if it hits 10 attempts, protecting your test from hanging indefinitely.

```yaml
- repeat:
    times: 10
    while:
      visible: "Update available"
    commands:
      - tapOn: "Dismiss"
      - assertNotVisible: "Dismiss" # Ensure it disappears before next loop
```

### Use JavaScript for complex logic

Sometimes, a simple visibility check is not enough. You might need to loop based on a numeric value or a calculation. For this, you can use JavaScript expressions within the `while` parameter.

Depending on how you are using JavaScript, you might need to initialize a variable before the loop. For example, if you are using a counter, you can use [`evalScript`](/reference/commands-available/evalscript) to set a starting value in the Maestro `output` object. After that, you can use the variable in the `while` condition.

In the following example, the counter is initialized and then incremented on each iteration of the loop.

```yaml
- evalScript: ${output.attempt = 0}
- repeat:
    while:
      true: ${output.attempt < 3}
    commands:
      - tapOn: "Refresh Data"
      # Increment the counter after each attempt
      - evalScript: ${output.attempt++}
```

### Integrate nested Flows with loops

You can combine [nested Flows](/maestro-flows/flow-control-and-logic/nested-flows) with loops when you need to perform a complex, multi-step sequence multiple times (e.g., creating five different user accounts or adding a variety of products to a cart). Instead of cluttering your main test with a long list of commands inside a loop, you can encapsulate the logic in a subflow and call it using [`runFlow`](/reference/commands-available/runflow) within the `repeat` block.

Suppose you need to add three different items to a shopping cart. By combining `repeat` with `runFlow` and `env` variables, you can create a clean, data-driven test. The main Flow can use JavaScript to define the data and then loop through it, while the subflow handles the mechanics of finding and adding an item.

{% tabs %}
{% tab title="Main Flow" %}

```yaml
appId: com.example.shop
---
- launchApp
- evalScript: ${output.items = ["Headphones", "Charger", "Phone Case"]}
- evalScript: ${output.index = 0}

- repeat:
    while:
      true: ${output.index < output.items.length}
    commands:
      - runFlow:
          file: subflows/add_item.yaml
          env:
            PRODUCT_NAME: ${output.items[output.index]}
      - evalScript: ${output.index++}

- tapOn: "Cart"
- assertVisible: "3 Items"
```

{% endtab %}

{% tab title="Subflow" %}

```yaml
# subflows/add_item.yaml
- tapOn: "Search"
- inputText: ${PRODUCT_NAME}
- pressKey: Enter
- tapOn: "Add to Cart"
- tapOn: "Back to Home"
```

{% endtab %}
{% endtabs %}

### Next steps

Now that you understand how to use loops in your flows, learn how to use [conditional execution](/maestro-flows/flow-control-and-logic/conditions) or explore all the possibilities of using [JavaScript](/maestro-flows/javascript/javascript-overview) to create tests.


# Conditions

Execute commands conditionally based on visibility, platform, or custom expressions.

Conditions allow you to execute commands or entire Flows only when specific criteria are met. This is useful for handling platform-specific logic (Android vs. iOS vs. Web), managing A/B tests, or dealing with dynamic UI elements like onboarding screens or permission dialogs.

{% hint style="success" %}

#### Keep your tests simple

Overusing conditional logic can make your Flows hard to read and debug. Prefer separate Flows for significantly different scenarios.
{% endhint %}

### Supported conditions

In Maestro, conditions are primarily handled using the `when` argument. It can be attached to several commands. If the condition inside the `when` block evaluates to `true`, Maestro executes the command; otherwise, Maestro simply skips that command and moves on to the next one.

The following table lists the available conditions you can use to define the conditional execution of your Flow.

| Condition    | Description                                                                                                                                         |
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `visible`    | Executed if the element matching the selector is visible. The element must be defined using one or more [Selectors](/reference/selectors).          |
| `notVisible` | Executed if the element matching the selector is **not** visible. The element must be defined using one or more  [Selectors](/reference/selectors). |
| `platform`   | Executed if the current platform matches (`Android`, `iOS`, or `Web`).                                                                              |
| `true`       | Executed if the JavaScript expression evaluates to `true`.                                                                                          |

### Common use cases

#### Handling platform differences

Mobile apps often have different UI or behaviors on Android, iOS, or Web. You can use the `platform` condition to run platform-specific Flows.

In this example, the test executes a specific subflow to handle permissions, depending on whether the device is running Android, iOS, or Web.&#x20;

```yaml
- runFlow:
    when:
      platform: Android
    file: subflows/android-permissions.yaml

- runFlow:
    when:
      platform: iOS
    file: subflows/ios-permissions.yaml
    
- runFlow:
    when:
      platform: Web
    file: subflows/web-permissions.yaml
```

#### Handling dynamic state

Sometimes an element may or may not appear, such as a "Rate this App" popup or a newsletter signup. You can handle these dynamic states using two different patterns:

{% tabs %}
{% tab title="The runFlow / when block" %}
This is the most idiomatically expressive way to handle conditions. It clearly defines the intent "Only run these commands *when* this condition is met." You combine [`runFlow`](/reference/commands-available/runflow) and `when`:&#x20;

```yaml
- runFlow:
    when:
      visible: "Dismiss"
    commands:
      - tapOn: "Dismiss"
```

{% endtab %}

{% tab title="The optional property" %}
Using the `optional` property is simpler for single-command interactions. It allows the step to fail without failing the entire test. Adding a label helps maintain clarity on why the step is there.

```yaml
- tapOn:
    text: "Dismiss"
    optional: true
    label: "Dismiss popup if it exists"
```

{% endtab %}
{% endtabs %}

{% hint style="success" %}
Relying on unstable UI states for conditions can lead to flaky tests. Ensure your `visible` selectors are unique and reliable.
{% endhint %}

#### Using `notVisible` for negative conditions

You can use the `notVisible` condition to handle inverse or “else” scenarios where an action should occur only when a specific element is *not* present on the screen.

For example, the following command taps **Standard Login** only if the **Biometric Login** option is not visible. The `runFlow` command is used here because the `tapOn` command itself does not support conditional logic:

```yaml
- runFlow:
    when:
      notVisible: "Biometric Login"
    commands:
      - tapOn: "Standard Login"
```

This approach allows your tests to adapt to different UI states.

#### Running multiple commands

You don't always need to create a separate file for conditional steps. You can use the `commands` list to define multiple actions inline.

In this example, if the "Welcome to our App" text is visible, the flow executes a sequence of taps to navigate through the onboarding screen.

```yaml
- runFlow:
    when:
      visible: "Welcome to our App"
    commands:
      - tapOn: "Next"
      - tapOn: "Get Started"
```

#### Multiple conditions

You can combine multiple conditions in a single `when` block. Note that all conditions must be met (AND logic) for the commands to execute.

In this example, the **Allow** button is tapped only if the platform is Android AND the **Allow Notifications** text is visible.

```yaml
- runFlow:
    when:
      platform: Android
      visible: "Allow Notifications"
    commands:
      - tapOn: "Allow"
```

#### Advanced logic with JavaScript

For more complex logic, such as feature flags or checking variables, use the `true` condition with a JavaScript expression.

In this example, the `new-feature-test.yaml` subflow is executed only if the `IS_FEATURE_ENABLED` variable evaluates to true.

```yaml
- runFlow:
    when:
      true: ${IS_FEATURE_ENABLED == true}
    file: subflows/new-feature-test.yaml
```

If your JavaScript condition is longer than one line, move the logic into a separate `.js` file and use the `output` variable to keep your YAML clean.&#x20;

The following example shows how to move the logic to an external file. First, create your script logic:

```javascript
// checkFeature.js
output.shouldRunTest = (MAESTRO_PLATFORM === 'Android' && someComplexCalculation() > 10);
```

Then, reference it in your Flow:

```yaml
- runScript: checkFeature.js
- runFlow:
    when:
      true: ${output.shouldRunTest}
    file: subflows/advanced-test.yaml
```

#### Next steps

Now that you understand how to use conditions in your Flows, learn how to use [parameters and constants](/maestro-flows/flow-control-and-logic/parameters-and-constants) or explore all the possibilities of using [JavaScript](/maestro-flows/javascript/javascript-overview) to create tests.


# Parameters and constants

Pass dynamic values to flows using CLI parameters and inline constants.

Maestro allows you to inject data into your Flows to make them dynamic and reusable. Instead of hardcoding values like usernames, passwords, or product IDs, you can use variables that are populated at runtime.

There are two main ways to define these variables:

* **Parameters**: Values passed externally (e.g., from the CLI or CI/CD pipeline).
* **Constants**: Values defined internally within the Flow (e.g., using the `env` key).

Both are accessed using the same `${VARIABLE_NAME}` syntax.

{% hint style="info" %}

#### **Case sensitivity**

Variable names are case-sensitive. `${APP_ID}` is different from `${App_ID}`.
{% endhint %}

### Parameters

Parameters are useful for passing sensitive data (like credentials) or environment-specific configurations (like URLs) that you don't want to commit to your repository.

#### Passing parameters via CLI

You can pass parameters to Maestro using the `-e` flag when running the `test` or `cloud` commands.

```bash
maestro test -e USERNAME=user@example.com -e PASSWORD=secret flow.yaml
```

{% hint style="info" %}

#### **Type safety**

Remember that CLI parameters are passed as strings. If you need a number or boolean, you may need to parse it in JavaScript (e.g., `parseInt(${COUNT})`).
{% endhint %}

In your Flow, you can access these values using the `${VARIABLE_NAME}` syntax. The following Flow uses the provided username and password to log in.

```yaml
appId: com.example.app
---
- launchApp
- tapOn: "Username"
- inputText: ${USERNAME}
- tapOn: "Password"
- inputText: ${PASSWORD}
```

#### Accessing shell variables

Maestro automatically reads shell environment variables prefixed with `MAESTRO_` and makes them available in your flows. This is convenient for CI/CD environments where secrets are often exposed as environment variables.

{% hint style="warning" %}
This feature works only with the Maestro CLI, not Maestro Studio.
{% endhint %}

```bash
export MAESTRO_API_KEY="12345"
maestro test flow.yaml
```

In this example, the API key exported in the shell is captured by an inline evaluation within the Flow.

```yaml
- evalScript: ${output.apiKey = MAESTRO_API_KEY}
```

### Constants

Constants are variables defined directly within your Flow files. They are useful for defining test data, configuration flags, or reusable values that don't need to change between runs.

#### Defining inline constants

You can define constants at the top of your Flow file using the `env` key.

In this example, `DEFAULT_TIMEOUT` and `IS_DEBUG` are set as constants for the entire Flow file.

```yaml
appId: com.example.app
env:
    DEFAULT_TIMEOUT: 5000
    IS_DEBUG: true
---
- launchApp
```

#### Passing arguments to subflows

When using nested Flows, you can pass arguments to reuse the same subflow with different data. Variables defined in `env` are available only to that specific subflow execution.

In the following example, the `login.yaml` subflow is executed with the `USER_ROLE` constant set to `admin`.

```yaml
- runFlow:
    file: subflows/login.yaml
    env:
        USER_ROLE: "admin"
```

{% hint style="info" %}

#### **Priority**

Constants defined in a subflow (either in the `env` header or the `runFlow` command) override parameters with the same name from the parent flow.
{% endhint %}

### Setting default values

You can use JavaScript syntax to provide default values for parameters. This is used for preventing crashes if a parameter is not provided.

In this example, `${USERNAME}` will use the provided value if it exists, otherwise, it defaults to `"guest"`.

```yaml
- inputText: ${USERNAME || "guest"}
```

This technique is particularly powerful in subflows, allowing them to be run standalone (using defaults) or as part of a larger suite (using passed parameters).

```yaml
# subflows/login.yaml
appId: com.example.app
env:
    # Use the passed value, or default to a test account
    USER_ID: ${USER_ID || "test_user_1"}
---
- tapOn: "User ID"
- inputText: ${USER_ID}
```

### Built-in parameters

Maestro provides a set of built-in parameters that are available in all flows.

| Parameter             | Description                                                                                                                                                                       |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MAESTRO_FILENAME`    | The filename of the current flow (e.g., `login.yaml`).                                                                                                                            |
| `MAESTRO_DEVICE_UDID` | The identifier for the device under test                                                                                                                                          |
| `MAESTRO_SHARD_ID`    | Identifies the shard this test is running on. Starts counting at 1, and defaults to 1 when not using [sharding](/maestro-flows/flow-control-and-logic/specify-and-start-devices). |
| `MAESTRO_SHARD_INDEX` | Identifies the shard this test is running on. Starts counting at 0, and defaults to 0 when not using [sharding](/maestro-flows/flow-control-and-logic/specify-and-start-devices). |

### Next steps

To continue learning how to create Flows, check the following pages:

* [Nested flows](/maestro-flows/flow-control-and-logic/nested-flows): Learn more about using `runFlow` to pass environment variables.
* [Maestro CLI overview](/maestro-cli) : Explore more options for the `-e` flag.
* [JavaScript overview](/maestro-flows/javascript/javascript-overview): Learn how to manipulate parameters using JavaScript logic.


# Specify and start devices

Learn how to launch Android and iOS virtual devices using the Maestro CLI, identify connected hardware, and target specific devices for local testing.

When managing multiple simulators, emulators, or physical devices, Maestro needs to know which one to target. This guide covers how to spin up new virtual devices using Maestro, find their identifiers, and target specific hardware for your tests.

### Start a device

Maestro allows you to create and launch Android emulators or iOS simulators directly from the CLI. These devices are configured to approximate the environment hosted on [Maestro Cloud](/maestro-cloud), ensuring your Flows are compatible when you scale up.

To view all available options and configurations available, run:

```bash
maestro start-device
```

Maestro will list the devices, platforms, and OS versions available, similar to the following example:

```
Supported device types: iPhone11 (iOS), Pixel 6 (Android)
      --device-model=<deviceModel>
                       Device model to run against
                       iOS: iPhone-11, iPhone-11-Pro, etc. Run command: maestro list-devices
                       Android: pixel_6, pixel_7, etc. Run command: maestro list-devices
      --device-os=<deviceOs>
                       OS version to use:
                         iOS: iOS-18-2, iOS-26-2 etc.
                         Android: android-33, android-34, etc.
      --platform=<platform>
                       Platforms: android, ios, web
```

To list all supported local device models and OS versions, run:

```bash
maestro list-devices
```

To list the device models and OS versions available on Maestro Cloud, run:

```bash
maestro list-cloud-devices
```

{% tabs %}
{% tab title="Android" %}
To create and launch a default Android emulator (Pixel 6, Google API 30), run:

```bash
maestro start-device --platform android
```

If the device already exists, Maestro will simply launch it.

{% hint style="info" %}
**Compatibility**

The device configurations created by this command are limited to specific OS versions and models supported by Maestro.
{% endhint %}
{% endtab %}

{% tab title="iOS" %}
To create and launch a default iOS simulator (iPhone 11, iOS 15.5), run:

```bash
maestro start-device --platform ios
```

If the device already exists, Maestro will simply launch it.

{% hint style="info" %}
**Cloud Compatibility**

The device configurations created by this command are limited to specific OS versions and models recommended for Maestro Cloud. Using these defaults helps prevent compatibility issues when moving from local development to cloud execution.
{% endhint %}
{% endtab %}
{% endtabs %}

### Find the device identifier

Once your devices are running, you need to obtain their unique identifier (ID) to target them specifically.

{% tabs %}
{% tab title="Android" %}
To list available Android devices, run the following command in your terminal:

```bash
adb devices
```

From the output, locate the device identifier for the device you want to use with Maestro.
{% endtab %}

{% tab title="iOS " %}
To list available iOS simulators, run the following command in your terminal:

```bash
xcrun simctl list devices booted
```

From the output, locate the device identifier for the device you want to use with Maestro.
{% endtab %}

{% tab title="Web" %}
It is not possible to list web devices. Maestro always launches its own instance of Chrome, so you don’t need to worry about device configuration for web tests.
{% endtab %}
{% endtabs %}

### Target a specific device

To run a test on a specific device, use the `--device` flag. This flag must be provided before the `test` command.

When running a Flow with the [Maestro CLI](https://docs.maestro.dev/maestro-cli/), you can explicitly define the target device. For example, to run `flow.yaml` on an iOS simulator with the identifier `5B6D77EF-2AE9-47D0-9A62-70A1ABBC5FA2`, use the following command:

```bash
maestro --device 5B6D77EF-2AE9-47D0-9A62-70A1ABBC5FA2 test flow.yaml
```

If you are using Maestro Studio, you can select a device through the interface. At the top of Maestro Studio, click **No device connected** to see a list of all available devices.

<figure><img src="/files/xhboNADIr71tMJHuxW3p" alt=""><figcaption></figcaption></figure>

### Run tests in parallel (Sharding)

If you have multiple devices running, you can speed up your local execution by "sharding" your tests. This allows you to utilize all available hardware simultaneously. You have two options to use sharding:

* **`--shard-all`**
* **`--shard-split`**

{% hint style="info" %}
**Maestro Cloud**

[Maestro Cloud](https://docs.maestro.dev/maestro-cloud/) handles device allocation and parallelization automatically. Sharding flags are primarily for local development and local CI runners.
{% endhint %}

#### **Strategy A: `--shard-all`**

Use this to run the exact same test collection across multiple devices. This is ideal for cross-platform validation or checking for flaky tests.

```bash
# Runs the entire .maestro folder on 3 devices at once
maestro test --shard-all 3 .maestro
```

#### **Strategy B: `--shard-split`**

Use this to divide your suite. If you have 9 tests and 3 devices, Maestro will run 3 unique tests on each device, finishing the run in roughly one-third of the time.

```bash
# Splits the test suite into 3 chunks and distributes them
maestro test --shard-split 3 .maestro
```

You can explicitly specify which devices to use for sharding by passing a comma-separated list to the `--device` flag. For example, if you have three devices running (`emulator-5554`, `emulator-5555`, `emulator-5556`) but only want to shard across two of them:

```bash
maestro test --device "emulator-5554,emulator-5556" --shard-split 2 ./myTests
```

{% hint style="warning" %}
**`--shard-all` and `--shard-split`**

To use these flags, you must have the required number of devices already booted and ready. If you request 3 shards but only 2 devices are connected, Maestro will return an error.
{% endhint %}

#### Screenshots when sharding

When sharding, you're using the same workspace on multiple devices at the same time. Taking a screenshot with the same name will cause overwriting. Look at the [available environment variables](/maestro-flows/flow-control-and-logic/parameters-and-constants#built-in-parameters) to differentiate.

```yaml
- takeScreenshot: "LoginScreen-shard_${MAESTRO_SHARD_INDEX}-device_${MAESTRO_DEVICE_UDID}.png"
```

#### Related content

* [Maestro CLI commands and options](/maestro-cli/maestro-cli-commands-and-options): Full list of available flags and commands.
* [Test reports and artifacts](/maestro-flows/workspace-management/test-reports-and-artifacts): Learn how reports are generated when running in parallel.
* [Maestro Cloud](https://docs.maestro.dev/maestro-cloud/): Scale your tests to dozens of devices without managing hardware.


# Hooks

Learn how to use onFlowStart and onFlowComplete hooks for setup and cleanup automation.

In automated testing, you often need to perform specific setup or cleanup tasks for every test. Instead of manually adding a `runFlow` to the start or end of every file, Maestro provides Hooks.

Hooks provide a configuration section to place setup and teardown logic, separate from the steps in the test. This ensures a consistent environment, reduces boilerplate code, and simplifies maintenance.

#### Types of Hooks

Maestro supports two primary hooks defined in the configuration section (above the `---` marker in your Flow file). These hooks apply every time you run the Flow:

| Hook             | When it runs                                         | Ideal Use Case                                                                  |
| ---------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------- |
| `onFlowStart`    | Before every individual Flow begins.                 | Resetting app state, logging in, or handling dynamic permissions.               |
| `onFlowComplete` | After every individual Flow finishes (Pass or Fail). | Clearing cookies, logging out, reporting custom metrics, or deleting test data. |

Here's how you could use these hooks in your `flow.yaml`:

```yaml
# flow.yaml
appId: my.app
onFlowStart:
  - runFlow: setup.yaml
  - runScript: setup.js
  - <any other command>
onFlowComplete:
  - runFlow: teardown.yaml
  - runScript: teardown.js
  - <any other command>
---
- launchApp
```

{% hint style="success" %}

#### Best practices

* Since hooks run for every single Flow, a slow hook will significantly increase your total suite execution time.
* Be careful not to call a Flow that itself triggers the same hook, causing an infinite loop.
* Use the `when` block within your hook sub-flow to perform actions only on specific platforms (e.g., clearing the iOS Keychain).
* Use `onFlowComplete` hook to ensure your app is in a "neutral" state for the next test, such as navigating back to the home screen, logging out, or deleting test data from your environment.
  {% endhint %}

### Usage example

Let's walk through how to set up setup and teardown logic so that every test in your workspace starts with an authenticated user.

#### **Step 1: Create your login subflow**

Create a reusable file at `subflows/login.yaml`:

```yaml
# subflows/login.yaml
- tapOn: "Username"
- inputText: "maestro_user"
- tapOn: "Login"
```

#### **Step 2: Register the hook**

Open your `flow.yaml` file and add the hooks to the configuration section:

```yaml
# flow.yaml
appId: com.example.app

onFlowStart:
  runFlow: subflows/login.yaml
```

#### **Step 3: Run your tests**

Now, when you run your test using `maestro test .`, it will execute the `login.yaml` sequence before starting the logic in your specific test file.

### Dynamic hooks

You can pass environment variables into your hooks just like a standard `runFlow`. This is useful for switching roles (e.g., User vs. Admin) across your entire suite.

```yaml
# flow.yaml
appId: com.example.app

onFlowStart:
  runFlow:
    file: subflows/login.yaml
    env:
      ROLE: "admin"
```

### **Handling hook failures**

It is important to understand how Maestro behaves when a hook encounters an error. Maestro’s logic remains consistent with industry-standard testing frameworks like **JUnit** (`@Before`/`@After`) and **XCTest** (`setUp`/`tearDown`).

If a hook fails, Maestro prioritizes test integrity and environment cleanup.

| Scenario                   | Result / Behavior                                                                                                                                                                                                                                                                             |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`onFlowStart` fails**    | <ol><li>The entire Flow is immediately marked as <strong>Failed</strong> (🔴).</li><li>The main body of the Flow execution is <strong>skipped</strong>.</li><li> The <strong><code>onFlowComplete</code></strong> hook is <strong>still executed</strong> to ensure cleanup occurs.</li></ol> |
| **`onFlowComplete` fails** | The Flow is marked as **Failed** (🔴), even if the main body of the test passed.                                                                                                                                                                                                              |

#### Related content

* [Nested flows](/maestro-flows/flow-control-and-logic/nested-flows): Understand the underlying `runFlow` command used by hooks.
* [Sequential execution](/maestro-flows/workspace-management/sequential-execution): Learn how hooks interact when running tests in a specific order.
* [Parameters and constants](/maestro-flows/flow-control-and-logic/parameters-and-constants): See how to manage variables within your hooks.


# Test in different locales

Test your app in multiple languages and regions using locale configuration.

Testing how your app handles different languages and regional formats is critical for a global user base. In Maestro, the locale is a global device setting. Because changing the system language requires a device-level configuration change, it is handled via the CLI only at runtime.&#x20;

{% hint style="info" %}
To define the locale, provide the `--device-locale` flag at execution time using the [Maestro CLI](/maestro-cli). For automated workflows, you can use CI wrappers to configure the locale for [Maestro Cloud](https://docs.maestro.dev/maestro-cloud/) runs.&#x20;

Note that there is no place within a Flow itself (such as `launchApp` or `config.yaml`) to define the locale.
{% endhint %}

{% hint style="info" %}

#### Web support

Maestro can change the locale for Android and iOS devices, but it does not control the internal language settings of the Chrome browser used for Web tests.
{% endhint %}

#### Set the locale

To run a Flow in a specific language, use the `--device-locale` flag when executing commands from the terminal. The value passed to `--device-locale` must be a combination of an [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) language code and an [ISO-3166-1](https://en.wikipedia.org/wiki/ISO_3166-1) country code, separated by an underscore (`_`).

You can use `--device-locale` with the `maestro start-device` and `maestro cloud` commands. It is not possible to define the locale when running the `maestro test` command.

Refer to [Locales supported by Maestro](/maestro-flows/flow-control-and-logic/test-in-different-locales/locales-supported-by-maestro) for the full list of supported locales.

The following examples show how to start devices with specific locales.

{% tabs %}
{% tab title="Android" %}

```bash
# Create a new Android emulator with the locale set to France (French)
maestro start-device \
  --platform android \
  --device-locale fr_FR
```

{% endtab %}

{% tab title="iOS" %}

```bash
# Create a new iOS simulator with the locale set to Italy (Italian)
maestro start-device \
  --platform ios \
  --device-locale it_IT
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}

#### What happens to the device?

When you start a device and define a locale, Maestro attempts to change the system language and region of the connected simulator or emulator before running any Flow.
{% endhint %}

After starting the device with the desired locale, you can run your tests normally. Maestro continues using the locale defined during device initialization.

```bash
maestro test your_flow.yaml
```

#### Combine locales and tags

If certain tests should only run in specific languages (for example, tests that validate French translations), a recommended approach is to combine `--device-locale` with [tags](/maestro-flows/workspace-management/test-discovery-and-tags). This ensures that only tests relevant to the selected locale are executed, making test runs faster and more efficient.

First, define a tag in the Flow. In the following example, the `french` tag indicates that the Flow is intended to validate the app using the French translation.

```yaml
# flows/login_fr.yaml
appId: com.example.app
tags:
  - french
---
- launchApp
- assertVisible: "Bienvenue"
```

After tagging your Flows, specify the tag when running tests using the `--include-tags` flag.

```bash
# Start the Android device using the French locale
maestro start-device --platform android --device-locale fr_FR
# Run only tests tagged as French
maestro test --include-tags french .maestro/
```

#### Related Documentation

* [Locales supported by Maestro](/maestro-flows/flow-control-and-logic/test-in-different-locales/locales-supported-by-maestro): Check the full list of supported locales.
* [Maestro CLI overview](/maestro-cli): Learn how to execute flows using the CLI.
* [Test discovery and tags](/maestro-flows/workspace-management/test-discovery-and-tags): Learn how to group and filter your localization tests.


# Locales supported by Maestro

Complete list of locale codes supported by Maestro for internationalization testing.

This reference page provides a list of the Language and Country codes supported by Maestro when using the `--device-locale` flag.

{% hint style="info" %}
Access the [Test in different locales](/maestro-flows/flow-control-and-logic/test-in-different-locales) guide for more information.
{% endhint %}

### Android locales

On Android, Maestro combines language and country codes to set the device environment. You can typically use just the language code, or a combination for regional specificity (e.g., `pt-BR`).

#### **Android language codes**

| Country code | Country name   |
| ------------ | -------------- |
| AU           | Australia      |
| AT           | Austria        |
| BE           | Belgium        |
| BR           | Brazil         |
| GB           | Britain        |
| BG           | Bulgaria       |
| CA           | Canada         |
| HR           | Croatia        |
| CZ           | Czech Republic |
| DK           | Denmark        |
| EG           | Egypt          |
| FI           | Finland        |
| FR           | France         |
| DE           | Germany        |
| GR           | Greece         |
| HK           | Hong-Kong      |
| HU           | Hungary        |
| IN           | India          |
| ID           | Indonesia      |
| IE           | Ireland        |
| IL           | Israel         |
| IT           | Italy          |
| JP           | Japan          |
| KR           | Korea          |
| LV           | Latvia         |
| LI           | Liechtenstein  |
| LT           | Lithuania      |
| NL           | Netherlands    |
| NZ           | New Zealand    |
| NO           | Norway         |
| PH           | Philippines    |
| PL           | Poland         |
| PT           | Portugal       |
| CN           | PRC            |
| RO           | Romania        |
| RU           | Russia         |
| RS           | Serbia         |
| SG           | Singapore      |
| SK           | Slovakia       |
| SI           | Slovenia       |
| ES           | Spain          |
| SE           | Sweden         |
| CH           | Switzerland    |
| TW           | Taiwan         |
| TH           | Thailand       |
| TR           | Turkey         |
| UA           | Ukraine        |
| US           | USA            |
| VN           | Vietnam        |
| ZA           | Zimbabwe       |

#### **Android country codes**

| Country code | Country name   |
| ------------ | -------------- |
| AU           | Australia      |
| AT           | Austria        |
| BE           | Belgium        |
| BR           | Brazil         |
| GB           | Britain        |
| BG           | Bulgaria       |
| CA           | Canada         |
| HR           | Croatia        |
| CZ           | Czech Republic |
| DK           | Denmark        |
| EG           | Egypt          |
| FI           | Finland        |
| FR           | France         |
| DE           | Germany        |
| GR           | Greece         |
| HK           | Hong-Kong      |
| HU           | Hungary        |
| IN           | India          |
| ID           | Indonesia      |
| IE           | Ireland        |
| IL           | Israel         |
| IT           | Italy          |
| JP           | Japan          |
| KR           | Korea          |
| LV           | Latvia         |
| LI           | Liechtenstein  |
| LT           | Lithuania      |
| NL           | Netherlands    |
| NZ           | New Zealand    |
| NO           | Norway         |
| PH           | Philippines    |
| PL           | Poland         |
| PT           | Portugal       |
| CN           | PRC            |
| RO           | Romania        |
| RU           | Russia         |
| RS           | Serbia         |
| SG           | Singapore      |
| SK           | Slovakia       |
| SI           | Slovenia       |
| ES           | Spain          |
| SE           | Sweden         |
| CH           | Switzerland    |
| TW           | Taiwan         |
| TH           | Thailand       |
| TR           | Turkey         |
| UA           | Ukraine        |
| US           | USA            |
| VN           | Vietnam        |
| ZA           | Zimbabwe       |

### iOS locales

iOS locales are typically provided as a single combined string, like `en_US`.

| Locale code | Locale name             |
| ----------- | ----------------------- |
| en\_AU      | Australia (English)     |
| nl\_BE      | Belgium (Dutch)         |
| fr\_BE      | Belgium (French)        |
| pt-BR       | Brazil (Portuguese)     |
| ms\_BN      | Brunei Darussalam       |
| zh\_CN      | China (Simplified)      |
| zh-Hans     | China (Simplified)      |
| zh-Hant     | China (Traditional)     |
| en\_CA      | Canada (English)        |
| fr\_CA      | Canada (French)         |
| cs\_CZ      | Czech Republic          |
| fi\_FI      | Finland                 |
| fr\_FR      | France                  |
| de\_DE      | Germany                 |
| el\_GR      | Greece                  |
| zh\_HK      | Hong Kong               |
| hu\_HU      | Hungary                 |
| hi\_IN      | India (Hindi)           |
| en-IN       | India (English)         |
| id\_ID      | Indonesia               |
| en-IE       | Ireland                 |
| he\_IL      | Israel                  |
| it\_IT      | Italy                   |
| ja\_JP      | Japan                   |
| ko\_KR      | Korea                   |
| es-419      | Latin America (Spanish) |
| ms\_MY      | Malaysia                |
| es-MX       | Mexico (Spanish)        |
| nl\_NL      | Netherlands             |
| en\_NZ      | New Zealand             |
| nb\_NO      | Norway                  |
| tl\_PH      | Philippines             |
| pl\_PL      | Poland                  |
| zh\_CN      | PRC                     |
| ro\_RO      | Romania                 |
| ru\_RU      | Russia                  |
| en\_SG      | Singapore               |
| sk\_SK      | Slovakia                |
| en-ZA       | South Africa (English)  |
| es\_ES      | Spain                   |
| sv\_SE      | Sweden                  |
| zh\_TW      | Taiwan                  |
| th\_TH      | Thailand                |
| tr\_TR      | Turkey                  |
| uk\_UA      | Ukraine                 |
| en\_GB      | UK (English)            |
| es\_US      | USA (Spanish)           |
| en\_US      | USA (English)           |
| vi\_VN      | Vietnam                 |


# Permissions

Configure app permissions on launch or mid-flow for iOS and Android testing.

Managing system permissions is a common challenge in mobile test automation. Since OS prompts (like "Allow Camera Access") typically only appear once, your test journey can become inconsistent if the app state isn't reset.

Maestro solves this by allowing you to explicitly configure permissions either at launch or during the Flow, ensuring a predictable environment every time.

### Configure permissions on launch

The easiest way to manage permissions is during the [`launchApp`](/reference/commands-available/launchapp) command. By default, Maestro grants all permissions, but you can override this behavior to test specific scenarios.

To customize launch permissions, you need to specify them when calling `launchApp`. The following example denies all permissions but explicitly allows the camera and location:

```yaml
- launchApp:
    permissions:
      all: deny
      camera: allow
      location: allow
```

### Changing permissions mid-flow

Sometimes you need to change permissions while the app is running, for example, to test how your app handles a permission denial or to prepare for a specific feature flow like scanning a QR code. Use the [`setPermissions`](/reference/commands-available/setpermissions) command for this purpose.

```yaml
- setPermissions:
    permissions:
      notifications: allow
```

{% hint style="info" %}

#### Browser limitation

Maestro can manage permissions for iOS and Android applications, but it has no control over Chrome’s system permissions.
{% endhint %}

### Available permissions

Maestro uses standardized names to make your Flows cross-platform. For example, using `bluetooth` on Android targets both `BLUETOOTH_CONNECT` and `BLUETOOTH_SCAN` automatically.

The following table list all permissions available in iOS and Android.

| Permission      | iOS | Android |
| --------------- | --- | ------- |
| `bluetooth`     | ❌   | ✅       |
| `calendar`      | ✅   | ✅       |
| `camera`        | ✅   | ✅       |
| `contacts`      | ✅   | ✅       |
| `health`        | ❌   | ❌       |
| `homekit`       | ✅   | ❌       |
| `location`      | ✅   | ✅       |
| `medialibrary`  | ✅   | ✅       |
| `microphone`    | ✅   | ✅       |
| `motion`        | ✅   | ❌       |
| `notifications` | ✅   | ✅       |
| `phone`         | ❌   | ✅       |
| `photos`        | ✅   | ❌       |
| `reminders`     | ✅   | ❌       |
| `siri`          | ✅   | ❌       |
| `sms`           | ❌   | ✅       |
| `speech`        | ✅   | ❌       |
| `storage`       | ❌   | ✅       |
| `usertracking`  | ✅   | ❌       |

{% hint style="success" %}
To grant all available permissions, use `all: allow` to represent all the permissions that the app can request.
{% endhint %}

#### **Android custom permissions**

If a specific Android permission isn't listed above, you can use the full Android Permission ID. The following example adds the `ADD_VOICEMAIL` permission:

```yaml
- setPermissions:
    permissions:
      com.android.voicemail.permission.ADD_VOICEMAIL: allow
```

{% hint style="info" %}
Note that `all: allow` also covers custom permissions, so you don't need to specify them individually unless you want to deny everything else.
{% endhint %}

#### Android special permissions

Not all permissions are prompted for in the app, like location is. Some require the user to leave the app and grant the permission within the Settings app.

`android.permission.MANAGE_EXTERNAL_STORAGE` permission has been required since Android 12 if an app wished to access files that weren't its own (e.g. file browsers, virus scanners).

Maestro can manage this like other permissions without additional user interaction, acting like an app's "second use" rather than first use. If it's declared in the app's AndroidManifest.xml then it'll be set automatically. If you want fine grained control, do as with custom permissions:

```yaml
- launchApp:
    clearState: true
    permissions:
      android.permission.MANAGE_EXTERNAL_STORAGE: deny
```

### Permission values

You can set permissions to one of the following states:

| Value   | Description                                                                                                                        |
| ------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `allow` | Grants the permission. On iOS, this also automatically dismisses the system prompt.                                                |
| `deny`  | Denies the permission. Android will request permission during the Flow execution if the app requires access to a specific feature. |
| `unset` | Resets the permission state, causing the system to prompt permission requests when running the Flow.                               |

Permission values can come from a variable or a JavaScript expression instead of being hard-coded:

```yaml
appId: com.example.app
env:
  CAMERA_PERMISSION_STATE: deny
---
- launchApp:
    permissions:
      camera: ${CAMERA_PERMISSION_STATE}
```

This lets one Flow cover both the granted and the denied journey, driven by `--env` or by the `env` block of the calling Flow.

{% hint style="info" %}

#### **Push notifications on iOS**

Unlike other permissions, iOS does not grant notification permissions silently. When the app requests permissions, the system prompt appears, and Maestro automatically taps **Allow**.

On Android, the permission is granted silently without a prompt.
{% endhint %}

#### iOS-specific values

iOS supports additional granular values for certain permissions.

| Permission | Value     | Description                                     |
| ---------- | --------- | ----------------------------------------------- |
| `location` | `always`  | Grants **Always Allow** location access.        |
|            | `inuse`   | Grants **While Using the App** location access. |
|            | `never`   | Same as `deny`.                                 |
| `photos`   | `limited` | Grants **Limited Access** to the photo library. |

### Usage examples

#### Deny all permissions

To ensure your app handles permission denied states, start your Flow by denying everything. This forces you to handle the edge cases where a user declines a system permission prompt.

```yaml
- launchApp:
    permissions:
        all: deny
```

#### Deny all but allow specific ones

This is a common pattern for testing specific features in isolation.

```yaml
- launchApp:
    permissions:
        all: deny
        medialibrary: allow
```

### Related resources

* [`launchApp`](/reference/commands-available/launchapp): See the full reference for launching apps.
* [`setPermissions`](/reference/commands-available/setpermissions): Command for altering permissions configuration mid-flow.


# Labeling and error handling

Configure label and optional in Maestro to improve test reporting, documentation, and resilient error handling for UI flows.

Beyond their core actions, most Maestro commands share two attributes, `label` and `optional`. These allow you to refine how tests are reported, documented, and how they behave when faced with unexpected UI states.

### Command labeling

The `label` attribute allows you to replace the default technical output in the console and test reports with a custom, human-readable description.

Some of the reasons to include a label include:

* **Intent over Implementation**: Instead of showing a technical selector (like a resource ID), a label describes the business logic of the step.
* **Security and privacy**: Mask sensitive data (like passwords or PII) so they don't appear in shared console outputs, team dashboards, or screen recordings.
* **Maintainability**: Labels act as "live" documentation that stays with the command, making it easier for collaborators to understand the flow's purpose.

The following code snippet shows how you can use labels to improve the test reporting:

```yaml
- tapOn:
    id: "buy-now-btn-v2"
    label: "Confirm purchase on the final checkout screen"

- inputText:
    text: "super-secret-password-123"
    label: "Enter the secure test user password"

- swipe:
    direction: LEFT
    label: "Navigate through the onboarding tutorial"
```

If you use labels, the output will be clearer to read and understand:

```shellscript
✅ Input text "super-secret-password-123" ## Standard output
✅ Enter the secure test user password    ## Output using labels
```

{% hint style="info" %}
While labels hide sensitive text from the UI and console summaries, the actual values remain visible in raw internal debug logs.
{% endhint %}

### Error handling with `optional`

By default, Maestro follows a "fail-fast" philosophy, which means that if a command fails to find an element or execute an action, the Flow stops immediately. The `optional: true` attribute overrides this, allowing the test to continue even if a specific step fails.

Some common use cases for `optional` include:

* **Non-critical UI**: Verifying elements that don't block the user journey, such as a "new feature" badge or a footer link.
* **Transient content**: Handling banners, pop-ups, or A/B test variations that might only appear for specific users or regions.
* **Environmental handling**: Dealing with system dialogs or sync messages that appear inconsistently and aren't critical to your test's objective.

The following code snippet shows an example of how you can use `optional`:

```yaml
- launchApp: com.example.example
- assertVisible:
    text: Summer sale is here!
    optional: true # If the banner isn't there, the test continues
- tapOn: Sign up now!
```

If an optional command fails, it is marked with a warning icon in the output, but the execution proceeds to the next step:

```shellscript
✅ Launch app "com.example.example"
⚠️ Assert that "Summer sale is here!" is visible (warned)
✅ Tap on "Sign up now!"
```

Maestro uses the following rules to define the default value of `optional` for the available commands:

* Standard Commands: Default to `optional: false`.
* AI Commands: Commands such as [`assertNoDefectsWithAI`](/reference/commands-available/assertnodefectswithai) and [`assertWithAI`](/reference/commands-available/assertwithai) default to `optional: true` due to their probabilistic nature.

{% hint style="info" %}
While `optional` can be added to any command, it has no practical effect on actions that cannot technically "fail" to execute, such as `back`, `stopRecording`, or `clearState`.
{% endhint %}


# Detect Maestro

Detect when your app is running under Maestro automation for test-specific behavior.

There are times when your application needs to behave differently during a test. Whether you want to bypass a 2FA screen, disable analytics to avoid polluting production data, or point to a mock server, detecting Maestro within your app's code is a common requirement.

### **Why detect Maestro?**

Detecting when your app is under test is a way to handle scenarios that are otherwise difficult to automate:

* **Bypassing 2FA:** Modify authentication flows to use fixed codes, avoiding the need for a physical SIM card or email inbox.
* **Controlling Content Persistence:** Keep short-lived messages (like temporary banners) on the screen longer so Maestro has enough time to detect and interact with them.
* **Environment Switching:** Automatically point your app to a mock server or a staging database to keep production data clean.
* **Disabling Custom Animations:** If your app uses specialized animations that aren't caught by the [`waitForAnimationToEnd`](/reference/commands-available/waitforanimationtoend), you can turn them off manually to prevent "ghost taps."

### Mobile (iOS and Android)

The gold standard for detecting Maestro on mobile is using [`launchApp arguments`](/reference/commands-available/launchapp#pass-launch-arguments). This approach is reliable, explicit, and works seamlessly in both local environments and [Maestro Cloud](https://docs.maestro.dev/cloud/run-maestro-tests-in-the-cloud).

{% stepper %}
{% step %}

#### **Pass the argument in your Flow**

In your Maestro Flow, use the `arguments` parameter within the `launchApp` command to send a custom flag.

```yaml
- launchApp:
    appId: "com.example.app"
    arguments:
      isMaestro: "true"
```

{% endstep %}

{% step %}

#### **Detect the argument in your code**

Your application can then check for this flag during its initialization phase.

{% tabs %}
{% tab title="Android (Kotlin/Java)" %}

```kotlin
val isMaestro = intent.getStringExtra("isMaestro") == "true"
if (isMaestro) {
    // Disable analytics or use mock data
}
```

{% endtab %}

{% tab title="iOS (Swift)" %}

```swift
if ProcessInfo.processInfo.arguments.contains("isMaestro") {
    // Apply test-only configurations
}
```

{% endtab %}

{% tab title="React Native" %}
For React Native, you can use a library like `react-native-launch-arguments` to retrieve the parameters passed during startup.

```javascript
import { LaunchArguments } from 'react-native-launch-arguments';

if (LaunchArguments.value().isMaestro === "true") {
    // Apply test-only configurations
}
```

{% endtab %}

{% tab title="Flutter" %}
For Flutter, the most straightforward approach is to use a package like `flutter_launch_arguments` to retrieve the parameters passed from Maestro without having to manually set up platform channels:

```dart
import 'package:flutter_launch_arguments/flutter_launch_arguments.dart';

Future<void> getArguments() async {
  final fla = FlutterLaunchArguments();

  final foo = await fla.getString('foo');
  final isFooEnabled = await fla.getBool('isFooEnabled');
  final fooValue = await fla.getDouble('fooValue');
  final fooInt = await fla.getInt('fooInt');
}
```

{% endtab %}
{% endtabs %}

{% hint style="danger" %}

#### Checking for open ports (Deprecated)

In the past, developers checked if ports `7001` (Android) or `22087` (iOS) were open. These were Maestro-specific ports that were used to detect Maestro.

**This method is now deprecated**. It is unsupported in Maestro Cloud and may be removed in future updates. Maestro strongly recommends using the [`launchApp arguments`](/reference/commands-available/launchapp#pass-launch-arguments) approach.
{% endhint %}
{% endstep %}
{% endstepper %}

### Web

For Web apps, Maestro simplifies detection by injecting a property directly into the global execution context.

Maestro automatically defines `window.maestro` while a test is running. You can check for this property anywhere in your frontend code using `window.maestro`:&#x20;

```javascript
if (window.maestro) {
  console.log("Maestro test is running!");
}
```

#### Related content

* [`launchApp`](/reference/commands-available/launchapp): Full technical reference for passing launch arguments.


# JavaScript overview

Extend Maestro flows with JavaScript for complex logic and data manipulation.

While Maestro’s YAML syntax is designed to handle the majority of UI interactions declaratively, certain testing scenarios require complex logic that goes beyond simple linear steps. The JavaScript integration allows you to make use of a full-scale scripting engine directly from your Flows, enabling you to handle dynamic data, complex assertions, and external integrations.

### Why use JavaScript in your Flows?

JavaScript provides the flexibility needed to create resilient, data-driven tests without sacrificing the stability. By using JavaScript, you can:

* **Handle Dynamic Data**: Generate unique identifiers, format dates, or manipulate strings for input fields.
* **Complex Control Flow**: Implement logic that YAML cannot express easily, such as complex mathematical calculations or multi-step conditional branching.
* **External Connectivity**: Sync your UI tests with your backend by making API calls to fetch test data or verify database states.
* **Enhanced Assertions**: Write custom logic to verify UI states that require calculation or data parsing.

### How it works

To ensure that tests remain portable and secure, Maestro executes JavaScript in a restricted sandbox. This means the scripts run in a "clean" environment without direct access to your local file system or external Node.js libraries. This architecture ensures that a Flow written on one machine will behave identically on a teammate's computer or in Maestro Cloud.

{% hint style="info" %}

#### JavaScript engine support

In Maestro, the GraalJS engine is enabled by default, allowing you to use modern ECMAScript (ES6+) features.

Rhino is also supported, but it must be explicitly enabled. Maestro discourage the Rhino usage.
{% endhint %}

### Explore JavaScript capabilities

Navigate through these guides to master scripting within your automation suite:

* [Run and debug JavaScript](/maestro-flows/javascript/run-and-debug-javascript): Maestro provides three ways to run JavaScript. You can also use standard `console.log` statements to debug your logic during execution.
* [Manage data and states](/maestro-flows/javascript/manage-data-and-states): The global `output` object allows you to share data across your entire Flow. This ensures that a value captured or generated in one script can be used by any subsequent YAML command or JavaScript expression.
* [Make HTTP requests](/maestro-flows/javascript/make-http-requests): With the built-in HTTP client, you can perform `GET`, `POST`, `PUT`, and `DELETE` requests directly from your scripts.&#x20;
* [Generate synthetic data](/maestro-flows/javascript/generate-synthetic-data): The `faker` object integration enables the generation of randomized data to ensure every test run uses fresh data and avoids account collisions.


# Run and debug JavaScript

Execute JavaScript in Flows using inline expressions, evalScript, or runScript with console.log debugging.

This guide explains how to execute JavaScript within your Maestro Flows and how to use logging to debug your scripts.

### Execution Methods

Maestro provides three ways to run JavaScript, depending on the complexity of the logic you need to perform.

#### 1. In-line expressions

For simple logic or dynamic values, use the `${}` syntax directly inside existing Maestro commands. This is ideal for string concatenation or basic math.

```yaml
- launchApp
- inputText: ${'User_' + faker.name().firstName()} # Generates a dynamic username
- tapOn: ${maestro.platform === 'ios' ? 'Allow' : 'While using the app'} # Conditional logic
```

{% hint style="info" %}
Learn how to [generate synthetic data](/maestro-flows/javascript/generate-synthetic-data) using JavaScript.
{% endhint %}

#### 2. The `evalScript` Command

Use [`evalScript`](/reference/commands-available/evalscript) for logic-only steps that do not directly interact with a UI element, such as setting a variable or performing a calculation.

```yaml
- evalScript: ${output.timestamp = new Date().getTime()} # Store data for later use
- evalScript: ${console.log('Test execution started')} # Inline logging
```

#### 3. The `runScript` Command

For complex logic, reusable functions, or long scripts, use [`runScript`](/reference/commands-available/runscript) to execute an external `.js` file.

You can pass environment variables to your script using the `env` attribute, in the same way you pass parameters to subflows. First, use `runScript` to define the JavaScript file and the variables to be shared with it:

```yaml
- runScript:
    file: setupUser.js
    env:
       userRole: 'admin' # Pass a parameter to the script
```

The JavaScript file can then read the environment variable directly to perform necessary actions:

```javascript
// Access the passed parameter directly by its name
const role = userRole; 
console.log(`Setting up user with role: ${role}`);
```

### Logging and debugging

Maestro supports standard `console.log` statements to help you debug your scripts and track Flow execution.

When logging with JavaScript in Maestro, keep these points in mind:

* **Multiple arguments are not supported:** Running `console.log('My variable is', variable)` will only output `My variable is`.
* **Use template literals or concatenation:** To log variables alongside text, use template literals (in external files) or string concatenation.
* **Log Destination:** Anything output via `console.log` is captured in the `maestro.log` file, prefaced with `JsConsole`.

The following code snippet shows examples of both methods:

```javascript
console.log('Value: ' + myVar)   // concatenation
console.log(`Value is ${myVar}`) // template literals
```

#### Logging with `evalScript` command <a href="#logging-with-evalscript-command" id="logging-with-evalscript-command"></a>

If you want to log something inline, you can use [`evalScript`](/reference/commands-available/evalscript) to output it to the console without creating a separate file.

```yaml
- evalScript: '${console.log("Value: " + myVar)}'
```

{% hint style="info" %}

#### Don't use template literals in `evalScript`

Standard JavaScript template literals (using backticks \`\`\` and `${}`) will **not** work inside `evalScript` because the command itself is already wrapped in a `${...}` block.&#x20;

```yaml
# Example of incorrect use
- evalScript: console.log(`Value is ${myVar}`)
```

Use string concatenation instead.
{% endhint %}

#### Logging in external files

When using `runScript`, you can organize your logs within your JavaScript files just as you would in a standard development environment. Here, template literals are fully supported.

The following Flow runs a script which will log a status message:

{% tabs %}
{% tab title="Flow" %}

```yaml
- runScript:
    file: script.js
```

{% endtab %}

{% tab title="JavaScript file (script.js)" %}

```js
// script.js
const status = 'Success';
console.log(`Operation status: ${status}`); // Outputs 'Operation status: Success'
```

{% endtab %}
{% endtabs %}

### Next steps

Now that you already know how to run and debug JavaScript code in Maestro Flows, access the following guides:

* [Manage data and states](/maestro-flows/javascript/manage-data-and-states): Learn how to use the `output` object.
* [Generate synthetic data](/maestro-flows/javascript/generate-synthetic-data): Use `faker` to create dynamic test data.
* [Test reports and artifacts](/maestro-flows/workspace-management/test-reports-and-artifacts): Learn how to access the `maestro.log` using the `--debug-output` flag to see your console logs.


# Manage data and states

Share data between scripts using the global output object, namespaces, and the maestro.copiedText property.

Sharing data between UI elements and JavaScript scripts is essential for creating dynamic, resilient tests. This guide covers how to use the global `output` object, manage namespaces, and capture UI text for use in your logic.

### The `output` object

Maestro provides a single, global JavaScript object called `output` that persists throughout the entire execution of a Flow. Any data assigned to this object in one script or expression is immediately accessible to all subsequent scripts and Maestro commands.

You can store strings, numbers, booleans, or complex objects within `output`. You can access or add information to the `output` object using standard JavaScript notation. In the following example, the value `'Hello World'` is assigned to `result` (an arbitrary key name):

```javascript
// myScript.js
output.result = 'Hello World'
```

Once the Flow runs `myScript.js`, the value is assigned to the global object. You can then use that value in subsequent commands, such as `inputText`:

```yaml
- runScript: myScript.js
- inputText: ${output.result}
```

### Output Namespacing

Because the `output` object is global, different scripts can accidentally overwrite each other’s data if they use the same variable names. To prevent this, use namespaces (sub-objects named after your specific script or feature).

For example, instead of assigning variables directly to the root of `output`, group them by context:

{% tabs %}
{% tab title="authScript.js" %}

```javascript
output.auth = {
    token: "abc-123",
    expiry: 3600
};
```

{% endtab %}

{% tab title="profileScript.js" %}

```javascript
output.profile = {
    username: "MaestroUser",
    role: "Admin"
};
```

{% endtab %}
{% endtabs %}

You can then access this data using namespaced notation:

```yaml
- inputText: ${output.auth.token}
- assertVisible: ${output.profile.username}
```

### Shared Functions

The `output` object can store functions as well as data. This is a powerful pattern for defining reusable logic, such as API helpers or complex string formatters, at the start of a Flow so they can be called from any subsequent script.

In this example, we define a token generation function in `apiUtils.js` and make it globally available via the `output.utils` namespace:

```javascript
// apiUtils.js
function generateToken(prefix) {
    return prefix + "_" + Math.random().toString(36);
}

output.utils = {
    generateToken: generateToken
};
```

By loading the script in `onFlowStart`, the `generateToken` function becomes a reusable utility for the rest of the test execution:

```yaml
onFlowStart:
    - runScript: apiUtils.js
---
# Call the shared function to generate a new session token
- evalScript: ${output.sessionToken = output.utils.generateToken('session')}
```

### The `maestro` Object

The `maestro` object is a built-in utility that provides information about the current test environment and captured UI data.

| Property             | Description                                                                                                                                                                                               |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `maestro.copiedText` | Contains the text retrieved by the most recent [`copyTextFrom`](/reference/commands-available/copytextfrom) command.                                                                                      |
| `maestro.platform`   | <p>Identifies the OS:<br><br>- <code>ios</code><br>- <code>android</code><br>- <code>web</code><br><br>This is useful for <a href="/pages/PSh3jkLSoQSvjzMpuuNY">conditional</a> cross-platform logic.</p> |

#### Capturing UI Text

To move text from your application into your JavaScript logic:

1. Use the [`copyTextFrom`](/reference/commands-available/copytextfrom) command to store the content in the `maestro.copiedText` variable.
2. Access that content in your JavaScript code or Maestro commands using `maestro.copiedText`.

The following example copies content from a `userName` element and uses it to send a dynamic message:

```yaml
- copyTextFrom: 
    id: userName # Target the userName element
- inputText: ${'Hello ' + maestro.copiedText} # Greets the user using the captured name
```

### Next steps

Now that you already knows how to manage data when using JavaScript in Maestro Flows, access the following guides:

* [Generate synthetic data](/maestro-flows/javascript/generate-synthetic-data): Use `faker` to create dynamic test data.
* [Make HTTP requests](/maestro-flows/javascript/make-http-requests): Use the built-in HTTP client for API interactions.


# Make HTTP requests

Make HTTP API calls from your flows to set up test data or verify backend state.

Maestro includes a built-in JavaScript HTTP client that allows you to interact with external APIs directly from your test flows. This is particularly useful for setting up test data, authenticating users, or verifying backend state without navigating through the UI.

### Making Requests

The Maestro HTTP client is a wrapper around okhttp3 and provides simple methods for common HTTP verbs.

You can use the following shorthand methods for standard requests:

* `http.get(url, config)`
* `http.post(url, config)`
* `http.put(url, config)`
* `http.delete(url, config)`

For other methods (like `PATCH` or `OPTION`), use the generic `request` function:

```javascript
const response = http.request('https://example.com', {
    method: "PATCH",
    body: JSON.stringify({ status: "active" })
});
```

### Working with JSON

Maestro provides a global `json()` function to parse HTTP response bodies into JavaScript objects.

For example, assume that `https://api.example.com/user/1` returns the following JSON response:

```json
{
  "id": 1,
  "email": "user@example.com",
  "profile": {
    "name": "Jane Doe",
    "age": 32
  }
}
```

You can parse the response body and access fields directly:

```js
// script.js
const response = http.get('https://api.example.com/user/1');

// Parse the body and access fields directly
const userData = json(response.body);
output.username = userData.profile.name;
```

In this example, the user’s name is read from `profile.name` and assigned to `output.username`.

### Configuration Options

When making a request, you can pass a configuration object to define headers, bodies, or form data.

#### Headers

Pass custom headers, such as authentication tokens or content types, using the `headers` property.

```javascript
const response = http.get('https://example.com', {
    headers: {
        'Authorization': 'Bearer ' + output.token,
        'Content-Type': 'application/json'
    }
});
```

#### Request Body

For `POST`, `PUT`, or `PATCH` requests, provide the payload in the `body` parameter. Remember to stringify JSON objects.

```javascript
const response = http.post('https://example.com/login', {
    body: JSON.stringify({
        username: "test_user",
        password: "password123"
    })
});
```

#### Multipart Form Data

To upload files or send multipart data, use the `multipartForm` parameter. When sending files, the following parameters are available:

* `filePath`: The path to the file. It is **required** for file uploads.
* `mediaType`: The MIME type of the file (optional).

```javascript
// script.js
const response = http.post('https://example.com/myEndpoint', {
    multipartForm: {
      "uploadType": "import",
      "data": {
        "filePath": filePath,
        "mediaType": "text/csv"
      }
    },
})
```

{% hint style="info" %}
If both `body` and `multipartForm` are provided in one request, the `body` parameter will be ignored.
{% endhint %}

### The `response` object

Every HTTP call returns a `response` object containing the following fields:

| Field Name | Type    | Description                                                  |
| ---------- | ------- | ------------------------------------------------------------ |
| `ok`       | Boolean | `true` if the request was successful (status 200-299).       |
| `status`   | Number  | The HTTP status code (e.g., `200`, `404`).                   |
| `body`     | String  | The raw string body of the response.                         |
| `headers`  | Object  | HTTP response headers (multiple values are comma-separated). |

### Usage example

A common pattern is to use the API to seed test data before executing UI interactions. This ensures that the specific data you need is present in the environment without relying on manual UI setup steps, which are often slow and flaky.

In this example, we create a new appointment via a `POST` request and then verify that it appears in the app.

The following script sends a request to an appointments API and stores the `appointmentTitle` in the [`output` object](/maestro-flows/javascript/manage-data-and-states#the-output-object) to be used by the main flow.

```javascript
// create_appointment.js
const response = http.post('https://my-api.com/v1/appointments', {
    body: JSON.stringify({ 
        title: "Maestro Health Check",
        date: "2026-02-10"
    }),
    headers: { 'Content-Type': 'application/json' }
});

const data = json(response.body);

// Store the title globally so the Flow can assert on it
output.appointmentTitle = data.title;

```

Once the script has run, the stored values can be accessed from any subsequent step in the Flow:

```yaml
# flow.yaml
appId: com.example.app
---
- launchApp
- runScript: create_appointment.js
- tapOn: "My Appointments"
# Assert that the data we just created via API is now visible in the UI
- assertVisible: ${output.appointmentTitle}
```

### Next step

Learn how to [generate synthetic test](/maestro-flows/javascript/generate-synthetic-data) data to create dynamic, realistic data at runtime, making your tests more flexible, reusable, and less dependent on hard-coded values.


# Generate synthetic data

Generate random test data with the built-in faker object for names, emails, numbers, and more.

In Maestro, you can use the built-in DataFaker integration to generate dynamic, randomized data for your test. This is useful for bypassing unique-field constraints (like sign-up forms) and creating realistic testing environments without manual data entry.

### The `faker` object

Maestro provides a global `faker` object available within the JavaScript engine. This object is a wrapper around the DataFaker library and follows the same usage patterns and providers found in its Java documentation.

{% hint style="info" %}

#### DataFaker

For additional information about DataFaker, check its [documentation](https://www.datafaker.net/documentation/getting-started/#usage).
{% endhint %}

### Common data providers

The `faker` object provides access to a wide variety of data types, ranging from standard user information to specialized domains.&#x20;

{% hint style="info" %}

#### Providers available

Check the DataFaker to see all [Fake Data Providers](https://www.datafaker.net/documentation/providers/).
{% endhint %}

#### Basic identity data

Generate standard user information for forms:

* First Name: `faker.name().firstName()`
* Full Name: `faker.name().fullName()`
* Credit Card: `faker.finance().creditCard()` or via expression: `faker.expression('#{finance.creditcard}')`

#### Numbers&#x20;

Generate random ranges or specific number patterns:

* Between Range: `faker.expression("#{number.numberBetween '1' '10'}")`
* Digits: `faker.number().digits(5)`

#### Placeholder&#x20;

DataFaker also supports "fun" providers for non-critical placeholder text:

* Lord of the Rings: `faker.lordOfTheRings().character()` or `faker.lordOfTheRings().location()`

### Usage examples

You can use `faker` within `evalScript` to store values in the `output` object, or directly inside UI commands.

{% tabs %}
{% tab title="In-line data generation" %}

```yaml
- launchApp
- inputText:
    text: ${faker.name().firstName()}
- inputText:
    text: ${faker.internet().emailAddress()}
```

{% endtab %}

{% tab title="Complex expressions" %}
Using expressions allows you to combine multiple faker providers into a single string:

```yaml
- evalScript: '${output.bio = faker.expression("#{name.fullName} lives in #{address.city}")}'
- console.log: ${output.bio}
```

{% endtab %}
{% endtabs %}


# Workspace management overview

Organize your Maestro workspace with config.yaml, test architecture, tags, and reporting for scalable automation.

Moving from your first test to a full-scale automation suite requires shifting your focus from "how to write a command" to "how to design a system". A well-architected Maestro workspace ensures your tests stay fast, reliable, and easy to maintain as your application evolves.

### The anatomy of a workspace

When organizing your workspace, consider the following four pillars:

{% stepper %}
{% step %}

#### **Configuration**

A Maestro workspace is centered around the [`config.yaml`](/reference/workspace-configuration) file, which settings that apply to your entire test suite or workspace directory. Access the [Project configuration](/maestro-flows/workspace-management/project-configuration) guide to learn how to create and manage configuration files for your suite.&#x20;
{% endstep %}

{% step %}

#### **Architecture**

Before writing dozens of tests, you must choose a repository structure that matches your app's business logic. Whether you choose a **User Journey** or **Feature Test** model, learning how to [organize your test architecture](/maestro-flows/workspace-management/design-your-test-architecture) ensures your tests remain scalable and maintainable.
{% endstep %}

{% step %}

#### **Advanced execution**

As your suite grows, you need control over how tests are discovered and executed:

* [Tags](/maestro-flows/workspace-management/test-discovery-and-tags): Categorize Flows (e.g., `smoke`, `production_ready`) to run specific subsets of tests.
* [Sequential order](/maestro-flows/workspace-management/sequential-execution): While Maestro favors isolated, non-deterministic execution, you can force a strict sequence when validating complex multi-step events.
  {% endstep %}

{% step %}

#### **Analysis**

Maestro doesn't just tell you if a test passed, it provides the why:

* [Reports and artifacts](/maestro-flows/workspace-management/test-reports-and-artifacts): Generate JUnit (XML) for CI/CD pipelines or human-readable HTML reports with failure screenshots.
* [Record your Flow](/maestro-flows/workspace-management/record-your-flow): Record Maestro tests as MP4 videos for debugging and sharing.
* [AI insights](/maestro-flows/workspace-management/ai-test-analysis): Use the `--analyze` flag to automatically detect spelling errors, layout breaks, and internationalization issues that traditional assertions might miss.
  {% endstep %}
  {% endstepper %}

{% hint style="success" %}

#### Best practice

Each Flow should be able to run on a completely reset device, even if you are using sequential execution.
{% endhint %}

### Next steps

If you are setting up a new repository, Maestro recommends following this path:

1. Configure your  `config.yaml` using the instructions in [Project configuration](/maestro-flows/workspace-management/project-configuration).
2. Define your folder structure based on the strategies in [Design your test architecture](/maestro-flows/workspace-management/design-your-test-architecture).
3. Tag your critical tests to organize and filter execution as described in [Test discovery and tags](/maestro-flows/workspace-management/test-discovery-and-tags).


# Project configuration

Configure your Maestro workspace with config.yaml for test suite settings.

The `config.yaml` file acts as the central "brain" of your Maestro workspace. While it is optional, it becomes essential when your project grows, allowing you to define global rules for your test suite, manage environment variables, and configure platform-specific behaviors.

{% hint style="info" %}
**Maestro Studio**

Unlike the Maestro CLI, Maestro Studio does not currently support `config.yaml`. However, Studio will include your configuration file in uploads to Maestro Cloud when you run an entire workspace.
{% endhint %}

### When do I need a config file?

If you are just running a single Flow file locally, you don't need a configuration. You should create a `config.yaml` if:

* You have a deep directory structure and need to define test discovery.
* You need to handle environment variables across multiple Flows.
* You want to configure cloud-specific behaviors like disabling system animations.

### Setting up your configuration

{% hint style="info" %}

#### Default behavior and hierarchy

When you point Maestro at a directory, it looks for a file named `config.yaml` in the root of that directory. If no `--config` flag is provided and the file is missing, Maestro runs with default settings.
{% endhint %}

{% stepper %}
{% step %}

#### Create the configuration file

You can place your configuration file anywhere in your repository, but for simplicity, it should be at the root of the Maestro workspace (commonly within a `.maestro/` folder).

{% hint style="info" %}
Ensure the file is named exactly `config.yaml`.
{% endhint %}
{% endstep %}

{% step %}

#### Configure where flows are stored

Use the `flows` block to define where in your repository the test flows are stored. Typically this will be a single line, but the config permits for a list of locations. Simple globbing syntax is permitted here, where `*` means the contents of a folder, but `**` includes all subfolders too.

```yaml
# config.yaml
flows:
  - e2e/*
  - smoke/**
```

{% endstep %}

{% step %}

#### Configure the platform behavior

To ensure your tests are stable and flake-free, you can configure platform-specific behaviors.

```yaml
# config.yaml
platform:
  ios:
    snapshotKeyHonorModalViews: false # iOS: Includes background elements in the hierarchy
```

{% hint style="info" %}

#### **Cloud-only flags**

Some flags, like `disableAnimations`, are a cloud-only feature and does not affect local emulators or simulators.
{% endhint %}
{% endstep %}
{% endstepper %}

{% hint style="info" %}
&#x20;If you need a list of all configurations available to configure your test suite, access the [Workspace configuration](/reference/workspace-configuration) reference.
{% endhint %}

### Working with multiple configs

While `config.yaml` is the default, you can create multiple configuration files for different scenarios (e.g., `smoke-config.yaml` or `ci-config.yaml`). To run a test suite with a specific configuration, use the `--config` flag when running the tests with the Maestro CLI:

```bash
maestro test --config .maestro/ci-config.yaml tests/
```

### Next steps

If you need a full list of every available key and configuration available, access the  [Workspace configuration](/reference/workspace-configuration) reference page.

To plan your test architecture access [Design your test architecture](/maestro-flows/workspace-management/design-your-test-architecture). On the other hand, if you need to organize your tests, learn how to use the `flows` key in your config to manage [Test discovery and tags](/maestro-flows/workspace-management/test-discovery-and-tags).


# Design your test architecture

Plan your test architecture with guides on repository configuration and test suite structure.

Moving from your first test to a full-scale automation suite requires shifting your focus from "how to write a command" to "how to design a system." A well-architected Maestro suite ensures that your tests stay fast, reliable, and easy to maintain as your application evolves.

To build this system, you need to address two distinct layers:&#x20;

* Where your tests live&#x20;
* How they are structured

To help you navigate these layers, you can explore the best practices into the following specialized guides:

1. [Repository configuration](/maestro-flows/workspace-management/design-your-test-architecture/repository-configuration): Learn where to store test files or choosing a high-level organization model (User Journeys vs. Features). It will help you decide on your folder structure and repo strategy, preventing refactoring later.
2. [Structuring your test suite](https://maestro.dev/blog/maestro-best-practices-structuring-your-test-suite): Check this guide if you are ready to write YAML files and want to know the rules for naming, sub-folders, and using tags to filter execution. Learn the "Maestro Way" of writing modular, tagged, and parallel-ready tests.


# Repository configuration

Organize your test repository using User Journey or Feature Test patterns for scalable automation.

Many teams adopt a monorepo approach, co-locating Maestro test suites with app source code. This strategy offers several technical advantages:

* **Version synchronization**: Tests remain coupled to the codebase version they validate, ensuring historical commits maintain test compatibility
* **Atomic changes**: Feature branches can bundle app logic modifications with corresponding test updates in a single pull request
* **Simplified CI/CD**: Build pipelines can reference tests directly from the same repository checkout without managing separate test artifact versions

### Repository structure patterns

A well-structured repository ensures that your test suite remains maintainable as your application grows. Most Maestro projects are organized around one of two core strategies:&#x20;

| **Strategy**      | **User Journeys (Goal-Driven)**             | **Feature Tests (Habit-Driven)**     |
| ----------------- | ------------------------------------------- | ------------------------------------ |
| Primary Objective | Funnel completion                           | Friction reduction                   |
| Best For          | E-commerce, Fintech, Food Delivery          | Social Media, Content, Entertainment |
| User Motivation   | Complete a specific task (e.g., Order food) | Continuous engagement and discovery  |
| Success Metric    | Reaching the end of a funnel                | Zero-friction interactions           |

Choosing the right structure helps ensure your automation remains relevant as your app grows.

#### User journeys (Goal-driven Apps)

This approach is best for E-commerce, food delivery, or fintech (e.g., DoorDash, Uber, Banking) Apps. These apps exist to help users complete a specific task. When a user is hungry, they open the app to order food,  once they are fed, they stop using it.

* **The Goal**: Success is defined by the user reaching the end of a funnel (e.g., Order Confirmed).
* **The Strategy**: Organize your repository by journeys. Focus on end-to-end flows that track variations of the main task (e.g., Delivery vs. Collection, applying coupons, or external payment processors).

This pattern often partitions test suites by user segmentation (new vs. existing) and execution context.

```
├── flows
│   ├── config.yaml
│   ├── tests
│   │   ├── new_users
│   │   │   ├── register.yaml
│   │   │   └── shopping_first_time_discount.yaml
│   │   └── existing_users
│   │       ├── account_settings.yaml
│   │       ├── deeplink_from_email.yaml
│   │       ├── deeplink_from_notification.yaml
│   │       ├── login.yaml
│   │       ├── shopping.yaml
│   │       └── shopping_bulk_buy_discount.yaml
│   └── utils
│       └── set_discount_code.js
└── src
    └── app
        └── <code here>
```

{% hint style="success" %}

#### Technical implementation note

To organize your project into a hierarchical structure, you must define the search path in your `config.yaml` using the `flows` key. For example, setting `flows: tests/**` tells Maestro to discover all Flow files within that specific subdirectory using glob syntax. This provides granular control over which tests are executed during local runs or CI/CD pipelines.
{% endhint %}

#### Feature tests (Habit-driven Apps)

This approach is best for social media, content aggregators, or entertainment (e.g., Reddit, Instagram, Spotify) apps. These apps succeed when they reduce friction and keep users engaged. Success is a cumulative experience of many small, slick interactions.

* **The Goal**: Success is defined by high engagement and zero friction.
* **The Strategy**: Organize your repository by individual features. You need deep coverage for components because one slow spinner or broken interaction can cause a user to drop off.

Feature-based organization establishes a structural isomorphism (direct mapping) between test suites and app modules:

```
├── flows
│   ├── config.yaml
│   ├── account
│   │   └── set_display_name.yaml
│   ├── auth
│   │   ├── login.yaml
│   │   ├── login_invalid.yaml
│   │   └── login_locked_account.yaml
│   ├── basket
│   │   ├── add_to_cart.yaml
│   │   └── update_cart.yaml
│   └── checkout
│       ├── complete_purchase.yaml
│       └── save_for_later.yaml
└── src
    └── app
        ├── account
        │   └── <code here>
        ├── auth
        │   └── <code here>
        ├── basket
        │   └── <code here>
        └── checkout
            └── <code here>
```

### Nest steps

Learn the "Maestro Way" of writing modular, tagged, and parallel-ready tests exploring the [Maestro Best Practices: Structuring your Test Suite](https://maestro.dev/blog/maestro-best-practices-structuring-your-test-suite) article.


# Test discovery and tags

Organize tests with tags and control which flows run using include/exclude filters.

As your test suite grows from a few files to hundreds of Flows, managing which tests to run and where they are located becomes critical. Maestro uses a combination of Inclusion patterns and metadata tagging to give you precise control over your execution.

### Test discovery via patterns

By default, when you point the Maestro CLI at a folder, it only executes Flow files located at the top level of that directory. It ignores subfolders to prevent accidental execution of nested or utility Flows.

To change this behavior and organize your repository into subdirectories (like `/auth`, `/checkout`, or `/journeys`), you must define Inclusion Patterns in your `config.yaml`.

The `flows` key uses glob syntax to define which files should be included in a test suite run.

```yaml
# config.yaml
appId: com.example.app
flows:
  - "*"            # Default: Includes all Flows in the root folder only
  - "auth/*"       # Includes all Flows in the auth subfolder
  - "tests/**"     # Recursive: Includes all Flows in tests and its subfolders
```

### Filter test via tags

Tags allow you to categorize your Flows based on their purpose, priority, or execution environment without changing your folder structure.

To add tags to a Flow, you need to define them  in the configuration block at the top of your individual Flow files:

```yaml
# flows/login_smoke.yaml
appId: com.example.app
tags:
  - smoke
  - registration
---
- launchApp
# ... rest of the flow
```

When running your tests using the [Maestro CLI](/maestro-cli), you can use the `--include-tags` or `--exclude-tags` flags to filter the execution on the fly.

* **Include**: `maestro test . --include-tags=smoke` (Runs only smoke tests).
* **Exclude**: `maestro test . --exclude-tags=wip` (Runs everything *except* work-in-progress tests).

#### **Tag logic**

When providing multiple tags in a comma-separated list, Maestro uses **OR** logic for filtering within a single flag.

* **Multiple Includes**: `--include-tags "auth,checkout"` Will run any test tagged `auth` OR `checkout`.
* **Multiple Excludes**: `--exclude-tags "experimental,stagingOnly"` will skip any test tagged `experimental` **OR** any test tagged `stagingOnly`.

{% hint style="info" %}

#### AND operator

Currently, there is no option to run only tests that match a combination of multiple tags (AND logic) within a single flag (e.g., running only tests that are tagged both `smoke` and `auth`).
{% endhint %}

If you use both `--include-tags` and `--exclude-tags` together, they perform an **AND** operation between the two groups:

1. Maestro identifies all Flows containing the included tags.
2. It then removes any Flows that contain the excluded tags.

Consider you have two Flows:

* **Flow A**: tags: `[dev, pull-request]`
* **Flow B**: tags: `[dev]`&#x20;

{% tabs %}
{% tab title="Flow A" %}

```
# flowA.yaml
appId: com.example.app
tags: 
  - dev
  - pull-request
```

{% endtab %}

{% tab title="Flow B" %}

```
# flowB.yaml
appId: com.example.app
tags: 
  - dev
```

{% endtab %}
{% endtabs %}

| Command                                          | Result           | Why?                                                      |
| ------------------------------------------------ | ---------------- | --------------------------------------------------------- |
| `--include-tags=dev`                             | **Flow A & B**   | Both contain the `dev` tag.                               |
| `--include-tags=dev,pull-request`                | **Flow A & B**   | Uses **OR** logic, both contain at least one of the tags. |
| `--exclude-tags=pull-request`                    | **Flow B**       | Flow A is removed due to the `pull-request` tag.          |
| `--include-tags=dev --exclude-tags=pull-request` | **Flow B**       | Both are included via `dev`, then Flow A is excluded.     |
| `--exclude-tags=dev`                             | **No flows run** | Both flows contain `dev`  and are removed.                |

### Global tag configuration

While individual tags in a Flow file categorize what a test is, defining tags in your `config.yaml` sets the policy for your entire workspace. It acts as a permanent security gate, ensuring that specific types of tests are always included or ignored without you having to remember complex CLI flags every time.

```yaml
# config.yaml
includeTags:
  - production_ready
excludeTags:
  - experimental
  - flaky
```

You can use global tags to:

* **Target environment**: You can create different config files for different environments. For example, a `staging-config.yaml` might globally include tags for internal-only features that shouldn't run in production.
* Enforce quality gates: If you have 10 tests that are currently "flaky" due to a known environment bug, you don't want to delete them. By tagging them as `flaky` in the Flow and adding `excludeTags: [flaky]` to your `config.yaml`, you ensure no one on the team accidentally runs them and breaks the build.

{% hint style="info" %}
CLI flags will always take precedence over global settings in the `config.yaml`.
{% endhint %}

#### Example

Imagine a repository structured by feature:

```
.
├── config.yaml
├── auth/
│   ├── login.yaml (tags: [smoke])
│   └── signup.yaml
└── payments/
    ├── credit_card.yaml (tags: [smoke])
    └── paypal.yaml
```

To run your `smoke` suite across all features, your `config.yaml` should look like this:

```yaml
# config.yaml
appId: com.example.app
flows:
  - "**"          # Look everywhere
includeTags:
  - smoke         # But only run the smoke tests
```

### Next steps

Now that you've discovered your tests, learn how to manage their [sequential execution](/maestro-flows/workspace-management/sequential-execution).


# Sequential execution

Run flows in a specific order using executionOrder for dependent test scenarios.

By default, Maestro executes Flows in a non-deterministic order. This is intentional because testing Flows in isolation ensures they are robust and not dependent on side effects from previous tests.

However, there are scenarios, particularly in Goal-Driven Apps, where you need to validate a strict sequence of events (e.g., creating an account, then verifying a profile, then placing an order). In these cases, you can use the `executionOrder` configuration.

### Configuring the sequence

To force an order, add the `executionOrder` block to your `config.yaml`. You can identify Flows using either their file name (without the `.yaml` extension) or the name property defined inside the Flow itself.

```yaml
# config.yaml
executionOrder:
  continueOnFailure: false # Default is true
  flowsOrder:
    - signup_flow          # Step 1
    - verify_email_flow    # Step 2
    - complete_profile     # Step 3
```

When you configure `executionOrder`, Maestro will:

1. First, run the flows listed in `flowsOrder` sequentially, in the specified order.
2. After that sequence completes, run any discovered Flows that are not included in `flowsOrder` in a non-deterministic order.

#### Handling failures

The `continueOnFailure` flag determines how the sequence behaves if a step fails:

* `true` (Default): If `signup_flow` fails, Maestro will still attempt to run `verify_email_flow`. This is useful for independent tests that happen to be ordered for convenience.
* `false`: If `signup_flow` fails, the entire remaining sequence is cancelled. This is essential for dependent tests where Step 2 cannot possibly succeed if Step 1 fails.

### Best practices and isolation

Even when running in sequence, you should treat your Flows as isolated units. A good rule of thumb is:&#x20;

> Each Flow should be able to run on a completely reset device.

{% hint style="warning" %}
Do not rely on one Flow leaving the app in a specific state for the next one.&#x20;

If you need to ensure a specific state (e.g., being logged in), use a [Hook](/maestro-flows/flow-control-and-logic/hooks) or a [Nested Flow](/maestro-flows/flow-control-and-logic/nested-flows) at the start of your test.
{% endhint %}

| **Requirement**  | **Recommendation**                                        |
| ---------------- | --------------------------------------------------------- |
| Logic dependency | Use `runFlow` to nest the required setup inside the test. |
| Simple ordering  | Use `executionOrder` in `config.yaml`.                    |
| Stop on error    | Set `continueOnFailure: false`.                           |

### Example: Partial sequencing

If you have five Flows (`A`, `B`, `C`, `D`, `E`) but only care that `A` and `B` run first, you can use the following configuration for the `executionOrder`:

```yaml
executionOrder:
  flowsOrder:
    - flowA
    - flowB
```

Maestro will run `A`, then `B`, and then run `C`, `D`, and `E` in any order.

### Next steps

Now that you have defined how and when your tests run, learn how to view the results:

* [Test reports and artifacts](/maestro-flows/workspace-management/test-reports-and-artifacts): Learn how to generate JUnit and HTML reports.
* [AI test analysis](/maestro-flows/workspace-management/ai-test-analysis): Use AI to find UI and logic issues in your completed runs.


# Test reports and artifacts

Generate test reports, screenshots, and logs for debugging and CI integration.

Once your tests have finished executing, Maestro provides structured feedback through reports and visual artifacts, like screenshots and screen recordings. These files are essential for debugging failures locally and integrating test results into your CI/CD pipelines.

### Output directory

Maestro automatically stores screenshots, logs, and metadata for every session. By default, these are stored in a specific folder depending on your operating system:

* **macOS and Linux**: `~/.maestro/tests`
* **Windows**: `%userprofile%\.maestro\tests`

However, you can customize this location for better organization in your CI environment. You can set the output directory using the Maestro CLI flag or permanently in your `config.yaml`.

{% tabs %}
{% tab title="Maestro CLI" %}

```bash
maestro test --test-output-dir=build/maestro-results ./e2e
```

{% endtab %}

{% tab title="config.yaml" %}

```yaml
# config.yaml
testOutputDir: build/maestro-results
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}

#### Priority rule

The CLI flag always overrides the `config.yaml` setting, which in turn overrides the default path.
{% endhint %}

### Generating reports

Maestro supports industry-standard formats to ensure compatibility with tools like Jenkins, GitHub Actions, and Azure DevOps, as well as most Testcase Management Systems.

{% hint style="info" %}

#### CLI-dependent

To generate reports, you must use the `--format` flag when running a test with the [Maestro CLI](https://docs.maestro.dev/maestro-cli/).

It is not possible to define report generation directly in the `config.yaml` file.
{% endhint %}

#### **JUnit (XML) reports**

JUnit is the standard for CI/CD integration and for test reporting. To generate a JUnit report, use the `--format junit` flag.

You can specify an output file using the `--output` flag. If omitted, Maestro will generate a `report.xml` file in your current working directory. Note that these reports are not included in the `--test-output-dir` or `--debug-output` folders.

```bash
maestro test --format junit --output build/report.xml ./e2e
```

#### **HTML reports**

HTML reports provide a human-readable summary, including screenshots of failed steps. Similar to JUnit, use the `--output` flag to define a specific destination.

For simple reports, use the `html` format. There's a more detailed report available with `html-detailed` that includes steps.

```bash
maestro test --format html --output build/report.html ./e2e
maestro test --format html-detailed --output build/detailed-report.html ./e2e
```

#### Custom properties

You can add custom metadata to your JUnit report using the `properties` field in your Flow header. These properties appear as `<property>` child elements on the `<testcase>` in the XML output.

```yaml
appId: com.example.app
name: Login Flow
properties:
    testCaseId: "TC-101"
    priority: "High"
---
- launchApp
```

#### Controlling JUnit XML attributes

Two reserved property keys let you override the `id` and `classname` attributes on the `<testcase>` element:

| Key              | JUnit attribute | Default   |
| ---------------- | --------------- | --------- |
| `junitId`        | `id`            | Flow name |
| `junitClassname` | `classname`     | Flow name |

By default, `id`, `name`, and `classname` are all set to the flow's `name`. Set `junitId` when you want a stable identifier independent of the display name, and `junitClassname` when your CI tooling groups or deduplicates results by class.

```yaml
appId: com.example.app
name: Login Flow
properties:
    junitId: TC-LOGIN-001
    junitClassname: com.example.tests.LoginTest
---
- launchApp
```

{% hint style="info" %}
`junitId` and `junitClassname` are reserved — they set XML attributes on the `<testcase>` element and are not emitted as `<property>` child elements. All other properties are emitted as `<property>` elements.
{% endhint %}

#### Maestro Cloud metadata in test reports

When you generate a report for tests executed on [Maestro Cloud](/maestro-cloud), Maestro adds properties that link each result back to the Cloud dashboard. You don't need to configure anything; they are added to the report automatically.

| Property         | Element       | Description                                             |
| ---------------- | ------------- | ------------------------------------------------------- |
| `cloud.uploadId` | `<testsuite>` | The ID of the upload that produced this suite.          |
| `cloud.url`      | `<testsuite>` | Link to the upload in the Maestro Cloud dashboard.      |
| `cloud.runId`    | `<testcase>`  | The ID of the individual Flow run.                      |
| `cloud.runUrl`   | `<testcase>`  | Link to that Flow's run in the Maestro Cloud dashboard. |

```xml
<?xml version='1.0' encoding='UTF-8'?>
<testsuite name="Test Suite" tests="1" failures="0" time="27.521" timestamp="2026-07-27T10:54:35">
  <properties>
    <property name="cloud.uploadId" value="mupload_01kyhfysy0ecrbjt1t6n0p8bgm"/>
    <property name="cloud.url" value="https://app.maestro.dev/.../upload/mupload_01kyhfysy0ecrbjt1t6n0p8bgm"/>
  </properties>
  <testcase id="Login Flow" name="Login Flow" classname="Login Flow" time="27.406" timestamp="2026-07-27T10:54:35" status="SUCCESS">
    <properties>
      <property name="cloud.runId" value="run_01kyhfysykehzbz8x1ztx0w53k"/>
      <property name="cloud.runUrl" value="https://app.maestro.dev/.../flow/run_01kyhfysykehzbz8x1ztx0w53k"/>
    </properties>
  </testcase>
</testsuite>
```

This extra data has two primary uses:

* Most CI report viewers surface `<property>` values on the test detail page, which gives you a direct jump from a failing test in CI to its screenshots and logs in Cloud.
* An agent using the Maestro MCP can use this report as key information to investigate failures

HTML reports contain the same information to hyperlink to the suite and to each Flow run.

These properties are only present when the tests executed on Maestro Cloud. A local `maestro test` session has no Cloud counterpart, so they are omitted rather than left blank.

#### Timestamps and durations

Both report formats record when execution started and how long it took:

* The `timestamp` attribute is populated for local sessions as well as Maestro Cloud, in the local timezone, and is truncated to whole seconds so that strict JUnit XSD validators and CI importers accept it.
* The suite-level `time` for a local session is the wall-clock time elapsed across the session, not the sum of the individual Flow durations.
* HTML reports render start times in a human-readable form rather than as raw epoch or ISO-8601 values.

#### What's inside the Artifact Folder?

Each session gets a timestamped folder inside the output directory, holding the log for the session as a whole. On iOS it also holds the raw XCTest runner log, which each Flow copies into its own folder as `logs/device-xctest.log` when it finishes. Each Flow then gets its own folder inside that, named after the Flow. Sharded sessions add a `-shard-N` suffix, and a numeric suffix is added if two Flows would otherwise collide. Every Flow folder is a self-contained bundle:

```
<output-directory>/2026-07-31_173617/    # session
├── maestro.log                          # log for the whole session
├── xctest_runner_2026-07-31_173620.log  # iOS only: raw XCTest runner log
└── login_flow/                          # one folder per test
    ├── manifest.json                    # index of every artifact in this folder
    ├── commands.json                    # one entry per executed step
    ├── logs/
    │   ├── maestro.log                  # log scoped to this Flow
    │   ├── device-logcat.txt            # Android: device log
    │   ├── device-simulator.log         # iOS: device log
    │   ├── device-xctest.log            # iOS: copy of the session XCTest log
    │   ├── crash-report.txt             # if the app under test crashed
    │   └── anr-report.txt               # Android only: if the app stopped responding
    ├── screenshots/                     # step screenshots
    ├── screen-hierarchy/                # view hierarchy JSON on failing steps
    ├── takeScreenshot/                  # your takeScreenshot output
    ├── startRecording/                  # your startRecording output
    └── ai-analysis/                     # screenshots analyzed by AI commands
```

| Entry               | Contents                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `manifest.json`     | An index of everything in the folder. Each entry gives a `kind`, a `format`, and a `relativePath`, plus `sizeBytes` for a single file or `count` for a folder of them. Read this rather than scanning the directory — it is the documented contract, and it carries a `$schema` you can validate against.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `commands.json`     | A JSON array with one entry per executed step, in order. Each carries the command, its `status`, `duration`, `sequenceNumber`, and any error, plus an `artifacts` list naming the files that step produced. This is where per-step attribution lives; the manifest does not carry it. A retried or repeated step appears once per attempt, and Maestro's own implicit setup steps appear alongside the ones you wrote.                                                                                                                                                                                                                                                                                                                                                                                         |
| `logs/`             | `maestro.log` scoped to this Flow, plus the device logs, whose filenames depend on the platform: Android writes `device-logcat.txt`, iOS writes `device-simulator.log` and `device-xctest.log`. Each device log's `metadata.source` in the manifest names the stream it came from — `emulator`, `simulator`, or `xctest`. A crash lands here as `crash-report.txt` on both platforms, though its contents differ: Android writes the stack trace taken from logcat, while iOS copies the simulator's `.ips` crash report verbatim, so that file holds JSON despite the `.txt` extension. On Android an ANR also lands, as `anr-report.txt`. Both are collected when the Flow ends, cover only the app under test, and only report an event from that Flow's own run, so there is at most one of each per Flow. |
| `screenshots/`      | Step screenshots, named `step-<NNN>-<type>-<detail>.png`. In a normal `maestro test` session only the failing step is captured, so a passing Flow has no `screenshots/` folder at all.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `screen-hierarchy/` | The view hierarchy as JSON, named to match the screenshot of the same step, for working out why a selector did not match. Captured alongside the step screenshots.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `takeScreenshot/`   | Screenshots your Flow asked for with [`takeScreenshot`](/reference/commands-available/takescreenshot). Paths in that command resolve inside this folder.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `startRecording/`   | Videos your Flow asked for with [`startRecording`](/reference/commands-available/startrecording). Paths in that command resolve inside this folder.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `ai-analysis/`      | The screenshots that AI commands analyzed, together with the defects they reported. See [AI test analysis](/maestro-flows/workspace-management/ai-test-analysis).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              |

{% hint style="info" %}
The tree above shows every entry Maestro can produce, not a single session's output. Entries only appear when that session created them, so the Android and iOS device logs never sit side by side, and a passing Flow that takes no screenshots of its own gets just `manifest.json`, `commands.json`, and `logs/`.

Running with `--analyze` captures considerably more: a screenshot before every action step rather than only the failing one, a `final.png` of the screen the test ended on after any `onFlowComplete` teardown, and a `screen-recording.mp4` of the whole test.
{% endhint %}

**Controlling output directories**

The contents of your artifact folders depend on which output directory you configure and whether you use one or both CLI flags.

| Case                     | Feature                                                                                                                                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Neither flag specified   | Maestro uses the default output directories described in the [Output directory](#output-directory) section.                                                                                            |
| `--test-output-dir` only | Screenshots & Video, `commands.json`, and AI Reports under this directory. The session-wide `maestro.log` stays in the default output directory; each Flow's own `logs/maestro.log` is under this one. |
| `--debug-output` only    | `maestro.log`, while screenshots, video, `commands.json`, and AI Reports go to the default test output tree.                                                                                           |
| Both flags specified     | Unless they both point to the same directory, `--debug-output` receives only `maestro.log`, and `--test-output-dir` receives screenshots, video, `commands.json`, and AI Reports.                      |

### Next steps

Now that you can see your results, take your debugging to the next level. Learn how to generate an automated insights reports that identifies UI and spelling bugs using [AI test analysis](/maestro-flows/workspace-management/ai-test-analysis).


# Record your Flow

Record a Flow as an MP4 with Maestro CLI and share runs for debugging or demos.

Maestro allows you to generate high-quality screen recordings of your tests without needing third-party software. The `record` command programmatically stitches the app screen and Flow output into a professional MP4, making it easy to debug failures or showcase features.

<figure><img src="/files/YbJ0ed36luUlwrEapfaY" alt=""><figcaption></figcaption></figure>

### Why record your Flows?

* **Debugging**: Visually identify race conditions or rendering issues by seeing exactly where a test fails.
* **Collaboration**: Share a clear video of a bug or user journey with developers and stakeholders.
* **Documentation**: Maintain a visual record of your application's critical paths for compliance or training.

### How to record

To ensure the best performance and privacy, it is recommended to use Local Rendering. This processes the video directly on your machine.

```bash
maestro record --local YourFlow.yaml
```

After rendering, the video will be available in the same directory as the [reports and artifacts](/maestro-flows/workspace-management/test-reports-and-artifacts):

* **macOS and Linux**: `~/.maestro/tests`
* **Windows**: `%userprofile%\.maestro\tests`

{% embed url="<https://vimeo.com/775621555/736930f7f9?fe=cm&fl=pl&turnstile=0.v5Dvsunbspg6mWzDMUSHFs0dMs0yAkm68v2PnKbKZbbCDrOqeOLFVw7a0MDCeXvmnW8VegEoLLgaCXp384i5Zsdw_-C9fdftxSav1doTnmY7rd7yUrocp8Nt5uXZnhtq2HrH4XYHOs41wjwywt-DI8xquooSNTamIjkZ3Bhvizb-F5riR4BsvLxhJGgdDIdAu4QlcFA95sQwEExtLRvrPFMeMzUBsoeGBecion0aVfg_3XmCmkm3fypkyOVMJElpjXb_8tK7WxWXQKmqDFOKf4Gm-Aowm12ohdCbZsc6LFN0XrwaPg4zZv8eYuqcrekH0PfcVcPnVM4uSYXy3Rpo_J1dDMTQyxadLRBX_rc8olaBC-MyI4hBvyDWbfm1N6VapDlTh0p5ahB5P_MGNoNdDyvckhJn5o_1C3TvzatOpRosZEav3RI5CN0CdvxoUm5Q4ci46wrkaO-3mDeef2Mu8uqwBzhyXIg_1n_WyPKRd56jO0FW_B9P8kwo4rdEVEBrivbqzEa46ypxiZwps3oVgkgSUDB6T1QlgL9evbNWo40QW26ut8vSseur3VBCMPC9ZjqwXGC5PevVGYZvfoZuirg-m3yDLBK1Rc74Z4RQJMIreAhF6G5xRJFtW56ntaD-PLcP-pxoy_aESMTDiolfcYD9CNy4LG5BRXSGPhSx7vlsWkNwbmz_pgfm40WSEAm900ypHSL5N4lXlJubejTV_yyEk5GpItHBvXAU9D3ZncstuVj8jsV8pMzYONiMbMHwCJ9lnES1yeql2Rp1Tf02l9owTIFPZ1UdgQ2AnH-XAjZy_vi0oT8gyKZ53oeLqRoQNqkSORIBZouPt2N5bnZ0oWW_AwEkU021KAvKFWdBKjDWduhrdwfwqAzwvl54i0hBTtUPRQaNXZkyi3_DBV6afpvz1d_3-19Gqp22NHlPH_voGzCgr2NDjibcwbRBWds6.2fM6XVw1thFNfTonAE7BUg.f69c9372bcce305de3604bbfb8f35e381ea503296a0c3decd7c88e7db84e967b>" %}

{% hint style="info" %}

#### Deprecation notice

The standard `maestro record` (remote) command is being deprecated. In future releases, local rendering will become the default behavior. We recommend switching to the `--local` flag now to prepare for this change.
{% endhint %}

#### Legacy (Remote rendering)

If you run `maestro record` without the `--local` flag, Maestro currently sends the raw screen capture and Flow output to mobile.dev servers to be processed.

{% hint style="info" %}

#### Privacy and security

* **Signed URLs**: Remote recordings generate a [signed URL](https://cloud.google.com/storage/docs/access-control/signed-urls) valid for 60 minutes.
* **Auto-Deletion**: All videos sent to our servers are automatically deleted after 24 hours.
  {% endhint %}

### Related content

You can also record your tests using [startRecording](/reference/commands-available/startrecording) or take screenshots for specific steps using [takeScreenshot](/reference/commands-available/takescreenshot).


# AI test analysis

Use AI-powered analysis to understand test failures and improve test coverage.

{% hint style="warning" %}
This is an experimental feature powered by LLM technology. We appreciate your feedback as we continue improving it.
{% endhint %}

Maestro provides an AI-powered analysis layer that goes beyond simple "pass/fail" results. By using the `--analyze` flag and custom AI commands, Maestro examines your test logs, command metadata, and screenshots to provide actionable insights into your app's functionality, UI polish, and internationalization.

### Authentication&#x20;

Because AI commands are processed through the Maestro infrastructure, you must authenticate with the Maestro Cloud backend.

* **Account Requirement**: Users need a Maestro Cloud account to use AI features.
* **Plan Support**: A free account is sufficient to enable AI commands; it does not require a paid Maestro Cloud plan. Note that while AI commands are enabled on a free account, running tests on Maestro Cloud itself still requires a Cloud Plan.
* **Login Methods**:
  * **CLI/Studio**: Running `maestro login` establishes an authentication session shared between the Maestro CLI and Maestro Studio. Logging into one automatically authenticates you for both.
  * **Environment Variable**: Alternatively, you can export your Maestro Cloud API key as an environment variable:<br>

    ```shellscript
    export MAESTRO_CLOUD_API_KEY=<your_maestro_key>
    ```

{% hint style="info" %}

#### AI usage in Maestro

Maestro has updated how AI features are provided. Users no longer need to "bring their own AI" by providing external service keys or selecting specific models.

* **Managed Model**: All AI commands are now routed directly through Maestro Cloud.
* **Automatic Configuration**: Environment variables like `MAESTRO_CLI_AI_KEY` and `MAESTRO_CLI_AI_MODEL` are no longer used. Maestro automatically manages the underlying third-party AI providers to ensure the best performance.
  {% endhint %}

### Ways to use AI

Maestro provides two main ways to use AI to evaluate your app.

#### Automated analysis

Use the `--analyze` flag to generate a comprehensive Insights Report that identifies UI regressions, spelling errors, and layout breaks.

```bash
maestro test login_flow.yaml --analyze
```

If Maestro detects any issues, it will compile a report as an HTML file and display a link to the report in the terminal.

```bash
🔎 Analyzing Flow(s)...

To view the report, open the following link in your browser:
file:///path/to/your/insights-report.html

Analyze support is in Beta. We would appreciate your feedback in our Slack channel: #community-chat
```

<figure><img src="/files/0F3fIWDKF76j2cjQ7zjW" alt=""><figcaption></figcaption></figure>

If your app is in great shape, you'll see a success message:

```bash
Hey, we analyzed your flow for spelling, grammar, and internationalization issues, and good news 🙌 we didn't find any issues!
```

#### **AI-powered assertions**

You can integrate AI directly into your YAML Flow logic using specialized commands:

* [`assertWithAI`](/reference/commands-available/assertwithai): Verify complex UI states using natural language (e.g., "Verify the user is shown a success message in Spanish").
* [`assertNoDefectsWithAI`](/reference/commands-available/assertnodefectswithai): Perform a visual audit of the current screen to find common UI issues.

#### Disable analysis notifications

If you want to prevent the `Analyzing Flow...` notification from appearing in your terminal output (e.g., in a clean CI log), you can set an environment variable:

```bash
export MAESTRO_CLI_ANALYSIS_NOTIFICATION_DISABLED=true
```

### Next steps

Learn more about specific assertions in the [`assertWithAI`](/reference/commands-available/assertwithai) and [`assertNoDefectsWithAI`](/reference/commands-available/assertnodefectswithai) command reference pages.


# Commands overview

API Reference covering Maestro commands, selectors for UI elements, and workspace configuration for global settings.

The API Reference provides detailed documentation on the core components of Maestro, including commands, selectors, and workspace configuration.

### Explore API capabilities

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-code">:code:</i></td><td><strong>Commands</strong></td><td>Explore the full list of available commands to interact with your application, from simple taps to complex logic.</td><td><a href="/pages/EhKJmG3upspLn0J3rvEd">/pages/EhKJmG3upspLn0J3rvEd</a></td></tr><tr><td><i class="fa-crosshairs">:crosshairs:</i></td><td><strong>Selectors</strong></td><td>Learn how to identify and target UI elements effectively using different selector strategies.</td><td><a href="/pages/5WlrDHblGj9kwJw5bTLR">/pages/5WlrDHblGj9kwJw5bTLR</a></td></tr><tr><td><i class="fa-gears">:gears:</i></td><td><strong>Workspace Configuration</strong></td><td>Configure global settings for your Maestro workspace to customize behavior across all flows.</td><td><a href="/pages/WDWCAl6raG6I9MiguTCg">/pages/WDWCAl6raG6I9MiguTCg</a></td></tr></tbody></table>


# Commands available


# addMedia

Add images or videos to the device gallery for media picker testing.

The `addMedia` command adds one or more media files from your workspace to the device's gallery on both Android and iOS. This makes the files accessible to your application during a test flow.

The command accepts a list of strings, where each string is the relative path to a media file in your workspace.

### Usage examples

The following example adds a PNG image and an MP4 video to the device's gallery.

```yaml
- addMedia:
    - "./assets/foo.png"
    - "./assets/foo.mp4"
```

### Supported formats

This command supports the following file formats:

* `PNG`
* `JPEG`
* `JPG`
* `GIF`
* `MP4`


# assertNoDefectsWithAI

AI-powered visual testing to detect UI defects and anomalies.

{% hint style="warning" %}
This is an experimental feature that uses LLM technology. All feedback is welcome.
{% endhint %}

The `assertNoDefectsWithAI` command takes a screenshot of the current view and sends it to an LLM to analyze for common visual defects. The command checks for issues such as text or UI elements that are cut off, overlapping, or not centered correctly within their containers.

Use this command as a general smoke test to verify that UI elements in your application render as expected.

### Command specifications

The `assertNoDefectsWithAI` accepts only one parameter:

| Parameter  | Type    | Description                                                                                     |
| ---------- | ------- | ----------------------------------------------------------------------------------------------- |
| `optional` | boolean | **Optional.** Determines if the Flow should continue if the assertion fails. Default is `true`. |

{% hint style="info" %}
Since `assertNoDefectsWithAI` is an experimental feature, `optional` is set to `true` by default to prevent unstable AI responses from breaking your CI/CD pipelines. If you want a failed AI assertion to stop the test, you must explicitly set `optional: false`.
{% endhint %}

### Output

The command generates an analysis report in both `HTML` and `JSON` formats. The output files are saved in the directory for the specific test run.

```
~/.maestro
└── tests
    ├── 2024-08-20_213616
    │   ├── ai-(My first flow).json
    │   ├── ai-(My second flow).json
    │   ├── ai-report-(My first flow).html
    │   ├── ai-report-(My second flow).html
```

The HTML report provides a visual summary of the findings.

![AI analysis report showing a screenshot with highlighted defects.](https://2384395183-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fn5KVIOjVkVjYRyVWZ0yT%2Fuploads%2Fgit-blob-c2909fc1a2e650255b19d003fb4aa19d0f25f02c%2Fai_demo.png?alt=media)

### Usage examples

The following video demonstrates the command in action.

{% embed url="<https://youtu.be/tfawnGqEhF0>" %}

### Related content

Access the [AI test analysis](/maestro-flows/workspace-management/ai-test-analysis) to learn how to configure the workspace to use the AI based solutions.


# assertNotVisible

Assert that a UI element is not present or visible on the screen.

The `assertNotVisible` command asserts that a UI element is not visible on the screen. If the element is currently visible, this command waits for it to disappear before proceeding.

{% hint style="info" %}

#### Maestro's fluent assertion

If the element is visible when the command is first called, Maestro will not immediately fail the test. Instead, it will automatically wait and retry for up to 7 seconds, giving the UI time to update or for animations to complete.

If you expect an element to take longer than 7 seconds to disappear, use the [`extendedWaitUntil`](/reference/commands-available/extendedwaituntil) command.
{% endhint %}

### Parameters

This command accepts the same selectors as `tapOn`. The following table lists commonly used parameters.

| Parameter  | Type      | Description                                                            |
| ---------- | --------- | ---------------------------------------------------------------------- |
| `text`     | `string`  | The text content of the element.                                       |
| `id`       | `string`  | The ID of the element.                                                 |
| `enabled`  | `boolean` | Specifies if the element is enabled (`true`) or disabled (`false`).    |
| `checked`  | `boolean` | Specifies if the element is checked (`true`) or unchecked (`false`).   |
| `focused`  | `boolean` | Specifies if the element has keyboard focus (`true`) or not (`false`). |
| `selected` | `boolean` | Specifies if the element is selected (`true`) or not (`false`).        |

For a complete list of all available selectors, see the [Selectors](/reference/selectors) reference.

### Usage examples

The following examples demonstrate how to use `assertNotVisible`.

#### Assert an element with specific text is not visible

This example uses the shorthand syntax to assert that an element with the text `My Button` is not on the screen.

```yaml
- assertNotVisible: "My Button"
```

#### Assert an element matching multiple properties is not visible

This example asserts that an enabled element with the text `My Button` is not visible. The command passes if no visible element matches both the `text` and `enabled` criteria. The test only fails if an element that is both enabled and has the text `My Button` is currently visible on the screen.

```yaml
- assertNotVisible:
    text: "My Button"
    enabled: true
```

### Related commands

* [`assertVisible`](/reference/commands-available/assertvisible)
* [`assertTrue`](/reference/commands-available/asserttrue)


# assertScreenshot

Perform visual regression testing of your application against existing screenshots

The `assertScreenshot` command takes a screenshot and matches it against a known good image, performing a visual regression test.

The assertion will fail if the comparison image is too dissimilar, or doesn't exist.

### Syntax

```yaml
- assertScreenshot: splash.png
```

or

```yaml
- assertScreenshot:
    path: screen.png
    cropOn:
      id: banner
    thresholdPercentage: 98
```

### Parameters

| Parameter             | Type             | Description                                                                                                                                                                                                                                                  |
| --------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `path`                | String           | Path to the reference screenshot that the current screen will be compared against. Can be a JavaScript expression. Was likely created by a [takeScreenshot](/reference/commands-available/takescreenshot) invocation in a previous run.                      |
| `cropOn`              | Element Selector | Optional. A selector to narrow the screenshot before comparison. The comparison screenshot must also have been cropped. For a complete list of all available selectors, see the [Selectors](https://docs.maestro.dev/api-reference/selectors) documentation. |
| `thresholdPercentage` | Number           | Optional. Percentage match required to pass this assertion. Default is `95`. Can be a variable or a JavaScript expression, as long as it resolves to a number.                                                                                               |
| `label`               | String           | Optional. A message to display when executing the evaluation.                                                                                                                                                                                                |

### Usage Examples

#### Assert against a reference screenshot

This example compares the current screen against `splash.png`, using the default threshold of 95%.

```yaml
- assertScreenshot: splash.png
```

#### Set the threshold from a variable

The threshold is interpolated before the comparison runs, so you can tune it per environment or per device without editing the Flow.

```yaml
appId: com.example.app
env:
  THRESHOLD_PERCENTAGE: 80
---
- assertScreenshot:
    path: ./screenshot.png
    thresholdPercentage: ${THRESHOLD_PERCENTAGE}
```

{% hint style="info" %}
The value must resolve to a number. If it resolves to something else — an empty string from an unset variable, for example — the assertion fails with `Invalid thresholdPercentage for assertScreenshot` rather than falling back to the default.
{% endhint %}


# assertTrue

Assert that a JavaScript expression evaluates to true.

Asserts that a given expression evaluates to a truthy value. A value is [truthy](https://developer.mozilla.org/en-US/docs/Glossary/Truthy) (in JavaScript terms) if it is not `false`, `0`, an empty string (`""`), `null`, `undefined`, or `NaN`.

### Syntax

You can use the shorthand approach, providing only the expression, or you can use the `condition` and `label`.

```yaml
- assertTrue: ${value}
# or
- assertTrue:
    condition: ${value}
    label: Variable 'value' is set
```

### Parameters

You can provide the expression directly or use the `condition` and `label` parameters for more complex assertions.

| Parameter   | Type       | Description                                                              |
| ----------- | ---------- | ------------------------------------------------------------------------ |
| `condition` | Expression | The expression to evaluate. The test passes if the expression is truthy. |
| `label`     | String     | An optional message to display when executing the evaluation.            |

### Usage examples

The following examples show how to use the `assertTrue` command.

#### Assert a JavaScript expression

This example uses `assertTrue` to verify that the text content of two separate views is identical. The `label` states the purpose of the assertion.

```yaml
- copyTextFrom: View A
- evalScript: ${output.viewA = maestro.copiedText}

- copyTextFrom: View B
- evalScript: ${output.viewB = maestro.copiedText}

- assertTrue:
    condition: ${output.viewA == output.viewB}
    label: View A and View B show the same text
```

#### Fail a test with a custom message

This example uses the `condition` and `label` parameters to intentionally fail a test and provide a descriptive reason.

```yaml
- assertTrue:
    condition: ${false}
    label: This will always fail
```

### Related commands

* [assertVisible](/reference/commands-available/assertvisible)
* [assertNotVisible](/reference/commands-available/assertnotvisible)


# assertVisible

Assert that a UI element is visible on screen with automatic retry.

The `assertVisible` command asserts that a UI element is visible on the screen. If the element is not immediately visible, this command waits for it to appear before timing out.

{% hint style="info" %}

#### Maestro's fluent assertion

If the element is not visible when the command is first called, Maestro will not immediately fail the test. Instead, it will automatically wait and retry for up to 7 seconds, giving the UI time to update or for animations to complete.

If you expect an element to take longer than 7 seconds to appear, use the [`extendedWaitUntil`](/reference/commands-available/extendedwaituntil) command.
{% endhint %}

### Parameters

The `assertVisible` command uses a selector to identify the target UI element. You can specify the selector as a single string (shorthand for the `text` property) or as a YAML object with multiple properties.

The following table lists common selector properties. For a complete list of all available selectors, see the [Selectors](/maestro-flows/flow-control-and-logic/how-to-use-selectors) documentation.

| Selector   | Description                                                            |
| ---------- | ---------------------------------------------------------------------- |
| `text`     | The text content of the element.                                       |
| `id`       | The ID of the element.                                                 |
| `enabled`  | Specifies if the element is enabled (`true`) or disabled (`false`).    |
| `checked`  | Specifies if the element is checked (`true`) or unchecked (`false`).   |
| `focused`  | Specifies if the element has keyboard focus (`true`) or not (`false`). |
| `selected` | Specifies if the element is selected (`true`) or not (`false`).        |

### Usage examples

#### Assert an element is visible by its text

This example asserts that an element containing the text `My Button` is visible on the screen.

```yaml
- assertVisible: "My Button"
```

#### Assert an element is visible and enabled

This example asserts that an element with the text `My Button` is both visible and enabled.

```yaml
- assertVisible:
    text: "My Button"
    enabled: true
```

The command fails if either of the following conditions is true:

* No element with the text `My Button` is found.
* An element with the text `My Button` is found, but it is disabled.

### Related commands

* [assertNotVisible](/reference/commands-available/assertnotvisible)
* [assertTrue](/reference/commands-available/asserttrue)


# assertWithAI

Use AI to verify complex UI states with natural language assertions.

{% hint style="warning" %}
This is an experimental feature that uses LLM technology. All feedback is welcome.
{% endhint %}

The `assertWithAI` command uses Large Language Models to validate the visual state of an application. The command captures a screenshot of the current screen and uploads it to an LLM along with a natural language assertion. The model evaluates the screenshot and returns a boolean result indicating whether the assertion is true.

This command is intended for scenarios where standard element-based assertions are difficult or impossible to implement, such as identifying complex visual patterns or dynamic content like two-factor authentication prompts.

### Command specifications

The following table describes the fields available for the `assertWithAI` command.

| Field       | Type    | Description                                                                                     |
| ----------- | ------- | ----------------------------------------------------------------------------------------------- |
| `assertion` | string  | The natural language description of the expected UI state to be evaluated by the LLM.           |
| `optional`  | boolean | **Optional.** Determines if the Flow should continue if the assertion fails. Default is `true`. |

{% hint style="info" %}
Since `assertWithAI` is an experimental feature, `optional` is set to `true` by default to prevent unstable AI responses from breaking your CI/CD pipelines. If you want a failed AI assertion to stop the test, you must explicitly set `optional: false`.
{% endhint %}

### Output artifacts

The command generates detailed reports in both HTML and JSON formats. These files are stored in a timestamped subdirectory within the Maestro configuration folder.

The following structure shows the location and naming convention for these artifacts:

```
~/.maestro
└── tests
    ├── 2024-08-20_213616
    │   ├── ai-(My first flow).json
    │   ├── ai-(My second flow).json
    │   ├── ai-report-(My first flow).html
    │   ├── ai-report-(My second flow).html

```

The reports provide visual confirmation of the assertion logic applied by the model.

### Usage examples

The following examples demonstrate how to implement `assertWithAI` for different validation scenarios.

#### Basic visibility check

This example validates that specific text fields are present on the screen.

```yaml
- assertWithAI:
    assertion: Login and password text fields are visible.

```

#### Complex visual validation

Use natural language to describe complex UI elements, such as a two-factor authentication (2FA) screen. To require the test to pass, set `optional: false`, so the test fails if the assertion is false.

```yaml
- assertWithAI:
    assertion: A two-factor authentication prompt, with space for 6 digits, is visible.
    optional: false
```

#### Video demonstration

The following video demonstrates the command in action.

{% embed url="<https://www.youtube.com/watch?v=tfawnGqEhF0>" %}

### Related content

Access the [AI test analysis](/maestro-flows/workspace-management/ai-test-analysis) to learn how to configure the workspace to use the AI based solutions.


# back

Press the system back button to navigate to the previous screen.

Navigates the user to the previous screen.

{% hint style="info" %}
This command is currently supported only on Android and Web.
{% endhint %}

### Usage examples

To navigate to the previous screen, use the `back` command.

```yaml
- back
```


# clearKeychain

Clear iOS keychain data for the app under test.

The `clearKeychain` command clears all data from the iOS Keychain.&#x20;

{% hint style="warning" %}
This command only applies to iOS and has no effect on Android or Web.
{% endhint %}

### Syntax

The command takes no arguments.

```yaml
- clearKeychain
```

### Why use this command?

The iOS Keychain is a secure storage container used by apps to persist login credentials, tokens, and other sensitive information.

In automated testing, the Keychain can become a source of state leakage, where a previous test run leaves a user logged in, causing subsequent tests to fail or behave unexpectedly. Using `clearKeychain` ensures your app starts from a clean, default state.

#### Automate Keychain clearing

If you want to clear the Keychain every time the app starts without adding a separate command, you can use the `clearKeychain` parameter directly within the [`launchApp`](/reference/commands-available/launchapp) command:

```yaml
- launchApp:
    clearKeychain: true
```

### Related content

You can use [Hooks](/maestro-flows/flow-control-and-logic/hooks) to automate Keychain clearing across all your tests.


# clearState

Clear app data, cache, and preferences to reset to fresh install state.

Clears all data for a specified mobile application.

### Usage examples

To clear the state of the current app, use add the `clearState` command alone.

```yaml
- clearState
```

If you want to clear the state of a specific app by its ID, add the app ID:

```yaml
- clearState: app.id
```

Or for a website, when testing on the web platform:

```yaml
- clearState: https://example.com
```

You can also add a `label` when invoking `clearState` to improve the readability of the report.

```yaml
- clearState:
    appId: app.id
    label: Reset ExampleApp
```

### Platform-specific behavior

The command's behavior differs based on the mobile operating system.

{% tabs %}
{% tab title="Android" %}
On Android, the `clearState` command is equivalent to running:

```
adb shell pm clear {package name}
```

It removes all app-related data from the device, such as shared preferences, databases, and accounts.
{% endtab %}

{% tab title="iOS" %}
On iOS, the `clearState` command causes Maestro to reinstall the entire app, resulting in a completely fresh install and removing all existing data folders.

As described in [Stack Overflow](https://stackoverflow.com/a/56746729/7009800) discussion, it's also possible to `xcrun simctl` to perform the same action.
{% endtab %}

{% tab title="Web" %}
The command clears all browser data (cookies, local storage, etc.) for the [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) of the current web app, or of the provided web app URL.
{% endtab %}
{% endtabs %}


# copyTextFrom

Copy text content from a UI element to clipboard or variable.

The `copyTextFrom` command copies text from a UI element and stores it in memory. This command requires a [Selector](/reference/selectors) to identify the target element.

### Accessing the copied text

When you copy content using the `copyTextFrom` command, you have two ways to use the text after it has been copied:

1. Use the [`pasteText`](/reference/commands-available/pastetext) command to immediately insert the content into a focused field.
2. To run an equality check, perform complex logic, or store the text for later use in your flow, use the `maestro.copiedText` variable.

### Usage examples

The following examples demonstrate how to use the copied text.

#### Paste with the `pasteText`  command

This example copies text from an element with the ID `someId` and pastes it into a search field.

```yaml
appId: com.example.app
---
- launchApp
- copyTextFrom:
    id: "someId"
- tapOn:
    id: "searchFieldId"
- pasteText
```

#### Access with JavaScript

The copied text is available in JavaScript through the `maestro.copiedText` property. This example copies the text from `someId` and uses the [`inputText`](/reference/commands-available/inputtext) command to add the content into the search field.

```yaml
appId: com.example.app
---
- launchApp
- copyTextFrom:
    id: "someId"
- tapOn:
    id: "searchFieldId"
- inputText: ${'Pasted using JavaScript: ' + maestro.copiedText}
```

#### **Store and validate**

You can assign the copied value to a custom variable to perform assertions or use it later in a Flow after other commands have overwritten the clipboard.&#x20;

This example demonstrates how to copy text from a UI element, store the copied value in a variable using `maestro.copiedText`, and then validate that the captured text matches the expected value.

```yaml
# Copy text from an element
- copyTextFrom:
    id: "my_element"

# Store the value for later use
- evalScript: ${output.myElementText = maestro.copiedText}

# Check that the element had the correct value
- assertTrue: ${output.myElementText == "Correct Text"}
```

### Related content

Explore how the [`pasteText`](/reference/commands-available/pastetext) command works, or learn how to use the available [Selectors](/maestro-flows/flow-control-and-logic/how-to-use-selectors) to define the desired element when copying content.


# doubleTapOn

Perform a double-tap gesture on a UI element or screen coordinates.

Double-taps a UI element or a specific point on the screen.

### Arguments

The `doubleTapOn` command accepts a [Selector](/reference/selectors) and a `delay` :

| Argument                         | Description                                                                                                                                                                                                                          |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Selector](/reference/selectors) | The element to double-tap. This can be a string representing the element's text, the accessibility ID, or an object specifying a selector. Accepts the same selectors as the [`tapOn`](/reference/commands-available/tapon) command. |
| `delay`                          | (Optional) The delay in milliseconds between the first and second tap. Defaults to `100`.                                                                                                                                            |

### Usage examples

You can use the shorthand approach by providing the visible text of the element. This example double-taps an element with the visible text `My Button`:

```yaml
- doubleTapOn: My Button
```

The implementation above produces the same result as using [`tapOn`](/reference/commands-available/tapon) with the `repeat` configuration.

```yaml
- tapOn:
    text: My Button
    repeat: 2
    delay: 100
```

If you need to use a different selector, or if you need to change the delay between the first and second tap, use this approach. This example uses an `id` selector to find the element and specifies a custom delay between taps:

```yaml
- doubleTapOn:
    id: "someId"
    delay: 200
```

### Related commands

* [tapOn](/reference/commands-available/tapon)


# eraseText

Delete text from input fields by character count or clear entirely.

The `eraseText` command deletes characters from the currently focused text field by simulating backspace key presses. By default, it removes up to 50 characters, making it an efficient way to clear input fields.

### Syntax

You can use the command without arguments to perform a standard clear, or specify a precise number of characters to delete (up to a maximum of 100).

```yaml
# Removes up to 50 characters (Default)
- eraseText 

# Removes a specific number of characters (Up to 100)
- eraseText: 10
```

### Clearing large text blocks

While `eraseText` works well for short inputs, clearing long paragraphs or very large fields by hitting backspace 50 times can be slow. To optimize your Flow, especially on iOS, you can use the following sequence to select and delete everything at once:

```yaml
# 1. Select the entire text block
- longPressOn: "<your_input_id>"
- tapOn: "Select All"

# 2. Perform a single backspace to clear the selection
- eraseText: 1
```

{% hint style="success" %}
When text is already selected (e.g., after **Select All**), using `- eraseText: 1` is faster than the default `- eraseText`, as it only needs to trigger a single backspace to delete the entire highlighted selection.
{% endhint %}


# evalScript

Evaluate inline JavaScript expressions within the flow context.

The `evalScript` command executes a single line of JavaScript directly within a Maestro flow. This is useful for performing simple computations or data manipulations without creating a separate JavaScript file.

The command accepts a single string argument representing the JavaScript expression to evaluate.

### Syntax&#x20;

To use the `evalScript` command, you need to provide the single-line JavaScript expression to evaluate. The result can be assigned to the `output` scope for use in subsequent steps:

```yaml
- evalScript: ${output.myVar = myExpression}
```

### Usage examples

The following example uses `evalScript` to convert an environment variable to uppercase and stores the result in `output.uppercaseName`.

```yaml
appId: com.example
env:
    MY_NAME: John
---
- launchApp
- evalScript: ${output.uppercaseName = MY_NAME.toUpperCase()}
- inputText: ${output.uppercaseName}
```

### Related content

Access the [JavaScript guides](/maestro-flows/javascript/javascript-overview) to learn how to use JavaScript when creating Flows.


# extendedWaitUntil

Wait for an element with a custom timeout longer than the default.

The `extendedWaitUntil` command pauses the test flow until a specified element becomes visible or not visible on the screen. The command completes as soon as the condition is met. If the condition is not met before the timeout expires, the command fails.

{% hint style="success" %}
Use `extendedWaitUntil` before `assertVisible` or `assertNotVisible` when your app needs longer than the default 7-second timeout. The command keeps checking until the condition is met or your extended timeout is reached, so it does not stop after 7 seconds.
{% endhint %}

### Arguments

| Argument     | Description                                                                                           |
| ------------ | ----------------------------------------------------------------------------------------------------- |
| `visible`    | The selector for the element to wait for. The command proceeds when the element is visible.           |
| `notVisible` | The selector for the element to wait for. The command proceeds when the element is no longer visible. |
| `timeout`    | The maximum time to wait, in milliseconds.                                                            |

For a complete list of available selectors, see the [Selectors](/reference/selectors) reference.

### Usage examples

#### Wait for an element to be visible

This example waits up to 10 seconds for an element containing the text "My text that should be visible" to appear.

```yaml
- extendedWaitUntil:
    visible: "My text that should be visible"
    timeout: 10000
```

#### Wait for an element to be not visible

This example waits up to 10 seconds for an element with the ID `elementId` to disappear.

```yaml
- extendedWaitUntil:
    notVisible: 
        id: "elementId"
    timeout: 10000
```

### Related content

Learn [how to use wait commands](/maestro-flows/flow-control-and-logic/wait-commands) in Maestro to create reliable tests.




---

[Next Page](/llms-full.txt/1)

