This is the full developer documentation for Marathon # Marathon documentation > Cloud testing infrastructure and mobile development tools. [Marathon CloudRun your mobile test suite on cloud infrastructure. Marathon provisions devices, distributes tests, and delivers results, targeting 15 minutes per run.Get started →](/cloud/getting-started/overview/)[EmuAndroid emulator and device companion. GPU-accelerated mirroring, network inspection, accessibility auditing, and MCP server for AI agents.Get started →](/emu/getting-started/installation/) Running the open-source runner yourself? See the [Marathon OSS documentation](https://marathonlabs.github.io/marathon/). # Pricing > Understand the Marathon Cloud pay-as-you-go pricing model. Marathon Cloud uses a transparent, pay-as-you-go pricing model based on the actual time your tests spend running on our virtual devices. There are no upfront costs or subscription fees. ## Tiers [Section titled “Tiers”](#tiers) | Tier | Price | Features | | -------------- | ----------------------------- | -------------------------------------------- | | **Starter** | Free | 50 free device hours on signup. | | **Cloud** | $2/hr (Android) / $3/hr (iOS) | Pay-as-you-go, billed monthly. | | **Enterprise** | Custom | Dedicated support and custom configurations. | ## Billing rates [Section titled “Billing rates”](#billing-rates) Marathon Cloud charges based on the platform used: * **Android**: $2.00 per hour per virtual device. * **iOS**: $3.00 per hour per virtual device. Billing is calculated with second-level precision, ensuring you only pay for the resources you consume. ## How time is measured [Section titled “How time is measured”](#how-time-is-measured) Billing starts when the first test begins execution on a virtual device. Billing ends when the last test on that device completes. Time between batches on a device is billed. ### What is included [Section titled “What is included”](#what-is-included) * Actual test execution time. * Time spent running test hooks (e.g., `setUp`, `tearDown`). * Time spent capturing screenshots, logs, and videos during the test. ### What is excluded [Section titled “What is excluded”](#what-is-excluded) * Device provisioning and boot time. * Application installation and uninstallation. * Time spent uploading test results and artifacts to the cloud. ## Pricing example [Section titled “Pricing example”](#pricing-example) Consider a test suite that takes 120 minutes of total execution time. Marathon Cloud aims to finish every suite in 15 minutes. ### Android run [Section titled “Android run”](#android-run) 1. **Calculate device count**: 120 minutes / 15 minutes = 8 devices. 2. **Calculate total device hours**: 8 devices × 0.25 hours (15 minutes) = 2 hours. 3. **Calculate total cost**: 2 hours × $2.00/hour = **$4.00**. ### iOS run [Section titled “iOS run”](#ios-run) 1. **Calculate device count**: 120 minutes / 15 minutes = 8 devices. 2. **Calculate total device hours**: 8 devices × 0.25 hours (15 minutes) = 2 hours. 3. **Calculate total cost**: 2 hours × $3.00/hour = **$6.00**. In these scenarios, you pay a small fee to have your entire test suite completed in 15 minutes. ## Billing and payments [Section titled “Billing and payments”](#billing-and-payments) ### Payment methods [Section titled “Payment methods”](#payment-methods) Marathon Cloud accepts all major credit cards through our secure payment processor. Manage payment methods and view invoices from the user menu → **Profile** → **Go to Stripe portal** in the [Marathon Cloud console](https://cloud.marathonlabs.io/profile). ### Invoicing [Section titled “Invoicing”](#invoicing) Invoices are generated at the end of each billing cycle. Each invoice provides a detailed breakdown of your usage, including the number of test runs, total device hours, and the resulting charges. ## Cost optimization [Section titled “Cost optimization”](#cost-optimization) ### Parallelization [Section titled “Parallelization”](#parallelization) Marathon Cloud automatically optimizes your test distribution to meet the 15-minute target. This ensures you get the fastest possible feedback without manually managing device counts. ### Flakiness reduction [Section titled “Flakiness reduction”](#flakiness-reduction) By using the built-in retry mechanism, Marathon Cloud prevents unnecessary costs associated with manual re-runs of flaky test suites. You only pay for the automated retries that the platform manages. Note Refer to the [retries](/cloud/configuration/retries/) guide to learn how Marathon Cloud handles test failures efficiently. # SAML SSO > Verify your email domain and configure SAML 2.0 single sign-on for your Marathon Cloud team. Marathon Cloud supports SAML 2.0 single sign-on with any identity provider (IdP) that issues SAML assertions: Okta, Microsoft Entra ID, Google Workspace, OneLogin, and others. SSO is configured per verified email domain. Users whose email is on that domain sign in through your IdP and are provisioned into your team automatically. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * The **Owner** role in your Marathon Cloud team. The **Domains & SAML** page is only shown to owners. * Admin access to your IdP. * Access to DNS for the email domain. ## Verify your domain [Section titled “Verify your domain”](#verify-your-domain) 1. Open the user menu in the top-right corner of the console and choose **Domains & SAML** (`/domains`). 2. Click **Add domain**, enter the domain (for example `example.com`), and click **Add domain**. 3. The verification page shows the DNS record to create at your registrar: | Field | Value | | -------------------------------- | ------------------------------------------------ | | Type | `TXT` | | Alias or host name | `@` | | Destination or points to address | `MARATHON=` | | TTL | `3600` | 4. After the record propagates, click **Verify**. If the page reports **TXT record not found**, wait for propagation and try again. Marathon Cloud also re-checks unverified domains hourly. ## Configure SAML [Section titled “Configure SAML”](#configure-saml) 1. From the verified domain, click **Open SAML configuration**, or use **SAML configuration** on the domains list. 2. ### Create the application in your IdP [Section titled “Create the application in your IdP”](#create-the-application-in-your-idp) The page shows the **ACS URL** to use in your IdP: ```text https://cloud.marathonlabs.io/api/v2/saml/acs?domain= ``` Use the same URL as the **Entity ID** / **Audience** in your IdP. Both SP-initiated and IdP-initiated sign-in are supported. 3. ### Map attributes [Section titled “Map attributes”](#map-attributes) Configure the assertion to include these attribute names exactly: | Attribute | Required | Description | | ------------ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `email` | Yes | The user’s email. Must be on the verified domain. | | `first_name` | No | Set on the user’s first sign-in. | | `last_name` | No | Set on the user’s first sign-in. | | `role` | No | Team role, applied on every sign-in: `owner`, `editor`, or `reader`. The prefixed forms `marathon_owner`, `marathon_editor`, `marathon_reader` are also accepted. Any other value, or no value, leaves an existing user’s role unchanged and creates new users as `reader`. | Microsoft Entra ID’s default claim URIs (`http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email`, `.../first_name`, `.../last_name`, `.../role`) are accepted as fallbacks. 4. ### Enter IdP details [Section titled “Enter IdP details”](#enter-idp-details) Either **Upload IdP Metadata** (the XML your IdP exports), or fill in the fields manually: * **SAML SSO URL**: your IdP’s sign-in endpoint (HTTP-Redirect binding). * **Entity ID**: your IdP’s issuer. * **Public certificate**: the IdP’s X.509 signing certificate, PEM text. 5. ### Test and save [Section titled “Test and save”](#test-and-save) Click **Test configuration**. A **Saml configuration test successful** message means the values parse and a service-provider configuration can be built; it does not perform a sign-in. Click **Save**. 6. ### Sign in [Section titled “Sign in”](#sign-in) On the sign-in page choose **Use single sign-on**, enter an email on the verified domain, and click **Log in**. You are redirected to your IdP and back with a Marathon Cloud session. ## How users are provisioned [Section titled “How users are provisioned”](#how-users-are-provisioned) Accounts are created just-in-time on first SSO sign-in with the email, name, and role from the assertion, and added to the team that owns the verified domain. On later sign-ins only `role` is re-applied; name changes in your IdP are not synced. Users with a matching domain can still use the SSO page even if they were originally invited with a password. ## Removing access [Section titled “Removing access”](#removing-access) Disable or unassign the user in your IdP. They cannot start a new session afterwards; an existing session lasts until it expires. To revoke immediately, also remove them from the team on the **Users** page. Caution Keep at least one owner who can sign in with email and password, so an IdP outage or misconfiguration cannot lock the team out. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### `SAML_DOMAIN_NOT_CONFIGURED` on sign-in [Section titled “SAML\_DOMAIN\_NOT\_CONFIGURED on sign-in”](#saml_domain_not_configured-on-sign-in) The domain is not verified, or one of SSO URL, Entity ID, or certificate is empty. All four must be present for SSO to activate. ### `EMAIL_NOT_PROVIDED` [Section titled “EMAIL\_NOT\_PROVIDED”](#email_not_provided) The assertion has no `email` attribute (or Microsoft `.../claims/email`). Check the attribute mapping in your IdP. ### Names or roles are blank [Section titled “Names or roles are blank”](#names-or-roles-are-blank) Attribute names are case-sensitive and use underscores: `first_name`, `last_name`, `role`. `firstName` / `givenName` style names are not read. ### Sign-in loops or signature errors [Section titled “Sign-in loops or signature errors”](#sign-in-loops-or-signature-errors) The certificate saved in Marathon Cloud does not match the one your IdP signs with. Re-paste the current signing certificate; rotate it before the old one expires. # Android setup > Configure and run Android UI tests on Marathon Cloud. Marathon Cloud supports native Android testing frameworks and cross-platform solutions. You can run tests written in Espresso, UIAutomator, Kakao, Kaspresso, and Flutter (via Patrol). ## Quick start [Section titled “Quick start”](#quick-start) ```bash marathon-cloud run android \ --api-key $MARATHON_CLOUD_API_KEY \ --application app-debug.apk \ --test-application app-debug-androidTest.apk ``` ## Supported frameworks [Section titled “Supported frameworks”](#supported-frameworks) Marathon Cloud executes standard Android instrumentation tests. The following frameworks are fully supported: * **Espresso**: The standard framework for Android UI testing. * **UIAutomator**: For cross-app functional UI testing. * **Kakao/Kaspresso**: DSL wrappers around Espresso for better readability and stability. * **Cucumber**: For behavior-driven development. * **Flutter (Patrol)**: For testing Flutter applications with native features. ## Build your test APKs [Section titled “Build your test APKs”](#build-your-test-apks) You must provide two APK files: the application under test and the instrumentation test APK. 1. Open your terminal in the root of your Android project. 2. Run the Gradle assembly tasks for both the app and the test app. ```bash ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest ``` 3. Locate the generated APKs in the build output directory. Typically, these are found at: * `app/build/outputs/apk/debug/app-debug.apk` * `app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk` ## Run tests [Section titled “Run tests”](#run-tests) ```bash marathon-cloud run android \ --api-key YOUR_API_KEY \ --application path/to/app-debug.apk \ --test-application path/to/app-debug-androidTest.apk \ --os-version 15 \ --system-image google_apis ``` The examples below omit `--api-key`; the CLI reads it from `MARATHON_CLOUD_API_KEY` when the flag is absent. ### Device selection [Section titled “Device selection”](#device-selection) | Parameter | Description | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--os-version` | Android version: `8`, `8.1`, `9`, `10`, `11`, `12`, `13`, `14`, `15`, `16`, `17`. | | `--system-image` | `default`, `google_apis`, or `google_apis_playstore`. | | `--arch` | `amd64` (default) or `arm64`. Pick `arm64` when the app ships native libraries without an `x86_64` ABI or you want to match production hardware. | | `--device` | Device type ID from `marathon-cloud devices android`. Use `watch` or `tv` for those form factors; omit for a phone. See [supported devices](/cloud/reference/supported-devices/). | Not every combination is valid, and the CLI rejects invalid ones before uploading: * Android `15`, `16`, `17` require `google_apis` or `google_apis_playstore`. * Android `8` to `9` require `google_apis`. * `watch` requires `google_apis` and Android `11`, `13`, or `15`; `tv` requires `google_apis` and Android `10` to `14`. * `arm64` requires Android `9` or later. `default` images are `arm64` only; use `google_apis` or `google_apis_playstore` for `amd64`. `--flavor` is deprecated; only native instrumentation tests are supported and the flag is ignored. ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Multi-module and library testing [Section titled “Multi-module and library testing”](#multi-module-and-library-testing) `--application-bundle` replaces `--application` + `--test-application` for multi-module projects and can be repeated to run several modules in one run. `--library-bundle` is for library modules that have a test APK but no app APK. ```bash marathon-cloud run android \ --application-bundle app/build/outputs/apk/debug/app-debug.apk,app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \ --application-bundle feature/build/outputs/apk/debug/feature-debug.apk,feature/build/outputs/apk/androidTest/debug/feature-debug-androidTest.apk ``` ```bash marathon-cloud run android \ --library-bundle mylib/build/outputs/apk/androidTest/debug/mylib-debug-androidTest.apk ``` ### Device features and sensors [Section titled “Device features and sensors”](#device-features-and-sensors) Camera emulation requires a `google_apis` or `google_apis_playstore` image. `--mock-location` is a switch and takes no value. ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --os-version 15 \ --system-image google_apis \ --mock-location \ --front-camera emulated \ --back-camera virtualscene ``` ### Instrumentation arguments and pulled files [Section titled “Instrumentation arguments and pulled files”](#instrumentation-arguments-and-pulled-files) `--instrumentation-arg` is repeatable and passed through to the instrumentation runner. `--pull-files` copies files off the device after each batch into `device-files.tar` in the artifacts; the path is relative to the chosen root. ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --code-coverage \ --instrumentation-arg "debug=true" \ --instrumentation-arg "clearPackageData=true" \ --pull-files "EXTERNAL_STORAGE:screenshots" \ --pull-files "APP_DATA:files/reports" ``` `ROOT` is `EXTERNAL_STORAGE` (`/sdcard`) or `APP_DATA` (the app’s private data directory). Files with the same path from different devices overwrite each other in the archive. ### Isolated execution [Section titled “Isolated execution”](#isolated-execution) `--isolated` puts every test in its own batch so no test shares a batch with another. Use it to confirm whether a failure depends on tests that ran before it; it makes the run longer and more expensive. See [batching](/cloud/configuration/batching/). ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --isolated ``` ### Limiting parallelism [Section titled “Limiting parallelism”](#limiting-parallelism) Marathon Cloud sizes the device pool to finish in about 15 minutes, which can mean many devices hitting your backend at once. If your tests share fixtures or your staging backend cannot take the load, cap the pool: ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --concurrency-limit 4 ``` Treat this as a stopgap; see [limiting concurrency](/cloud/configuration/batching/#limiting-concurrency). ### Gating CI [Section titled “Gating CI”](#gating-ci) The command exits `1` if any test fails and `0` otherwise. Add `--result-file result.json` for a machine-readable summary and `--output ./results` to download JUnit XML and videos. See [exit codes](/cloud/cli/parameters/#exit-codes). Note Refer to the [CLI parameters](/cloud/cli/parameters/) page for a full list of available options. # CI/CD integration > Run Marathon Cloud on every pull request from GitHub Actions, CircleCI, Bitrise, or any Docker-capable CI, and gate the pipeline on the result. Marathon Cloud ships an official GitHub Action, a Bitrise step, and a Docker image. All of them wrap the same CLI, so anything you can do from a terminal you can do from CI. Working examples live in the [MarathonLabs/samples](https://github.com/MarathonLabs/samples) repository. Examples pin CLI 1.0.68, `action-test` 1.0.21, `setup-marathon-cloud` 2.0.2, and the Bitrise step 1.0.1. ## Quick start [Section titled “Quick start”](#quick-start) A complete GitHub Actions job: build, run on Marathon Cloud, publish JUnit, fail the check on test failures. ```yaml name: ui-tests on: [pull_request] jobs: marathon: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - run: ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest - name: Run tests on Marathon Cloud uses: MarathonLabs/action-test@1.0.21 with: version: 1.0.68 apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }} platform: Android application: app/build/outputs/apk/debug/app-debug.apk testApplication: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk osVersion: 15 systemImage: google_apis output: marathon link: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} branch: ${{ github.head_ref }} - name: Publish JUnit if: always() uses: mikepenz/action-junit-report@v5 with: report_paths: marathon/tests/omni/marathon_junit_report.xml ``` The Marathon step exits non-zero when a test fails, so the job goes red without extra wiring. `if: always()` on the publish step ensures the report is uploaded either way. ## Gate your pipeline on results [Section titled “Gate your pipeline on results”](#gate-your-pipeline-on-results) The CLI, and every integration built on it, exits `1` when any test fails and `0` when all tests pass. For richer gating, set `resultFile` and read the JSON in a later step: ```yaml - uses: MarathonLabs/action-test@1.0.21 with: version: 1.0.68 apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }} platform: Android application: app-debug.apk testApplication: app-debug-androidTest.apk osVersion: 15 systemImage: google_apis resultFile: marathon-result.json ignoreTestFailures: true - name: Summarize run: | jq -r '"\(.passed) passed, \(.failed) failed, \(.ignored) ignored — \(.report)"' marathon-result.json >> "$GITHUB_STEP_SUMMARY" test "$(jq -r .state marathon-result.json)" = "passed" ``` `ignoreTestFailures: true` keeps the run step green so the summary step executes; the final `test` restores the red check. See [result file](/cloud/cli/parameters/#result-file) for the schema. ## Supported platforms [Section titled “Supported platforms”](#supported-platforms) * GitHub Actions **`MarathonLabs/action-test`** installs the CLI and runs it in one step. `version` (the CLI version to install) and `apiKey` are required alongside your run inputs. **Android** ```yaml - uses: MarathonLabs/action-test@1.0.21 with: version: 1.0.68 apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }} platform: Android application: app/build/outputs/apk/debug/app-debug.apk testApplication: app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk osVersion: 15 systemImage: google_apis output: marathon ``` **iOS** ```yaml - uses: MarathonLabs/action-test@1.0.21 with: version: 1.0.68 apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }} platform: iOS application: build/Build/Products/Debug-iphonesimulator/YourApp.app testApplication: build/Build/Products/Debug-iphonesimulator/YourAppUITests-Runner.app osVersion: 18.4 output: marathon ``` **Maestro** ```yaml - uses: MarathonLabs/action-test@1.0.21 with: version: 1.0.68 apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }} platform: Maestro/Android application: app-debug.apk testApplication: flows/ flows: flows/smoke/ output: marathon ``` **Inputs** | Input | Description | | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `version` | CLI version to install. Required. | | `apiKey` | Marathon Cloud API token. Required. | | `platform` | `Android`, `iOS`, `Maestro/Android`, or `Maestro/iOS`. Required. | | `application` | Application binary. Required. | | `testApplication` | Test binary, or flows directory for Maestro. Required. | | `osVersion`, `device`, `systemImage` | Device selection. Android 15+ needs `systemImage: google_apis` or `google_apis_playstore`. | | `output`, `outputGlob` | Download artifacts into `output`, optionally filtered by glob. | | `resultFile` | Write the [result file](/cloud/cli/parameters/#result-file). | | `name`, `link`, `branch`, `project` | Run metadata. | | `wait`, `ignoreTestFailures` | Exit behaviour. | | `filterFile`, `isolated`, `analyticsReadOnly` | Execution control. | | `retryQuotaTestUncompleted`, `retryQuotaTestPreventive`, `retryQuotaTestReactive`, `noRetries` | [Retries](/cloud/configuration/retries/). | | `pullFiles` | Android: files to pull from the device. | | `grantedPermission`, `xctestrunEnv`, `xctestrunTestEnv`, `xctestplanFilterFile`, `xctestplanTargetName` | iOS. | | `maestroEnv`, `flows` | Maestro. | Flags without an input (`--concurrency-limit`, `--code-coverage`, `--batch-isolation`, `--test-timeout-*`, `--application-bundle`, `--library-bundle`, `--instrumentation-arg`) are available by installing the CLI with `MarathonLabs/setup-marathon-cloud` and calling `marathon-cloud` from a `run:` step: ```yaml - uses: MarathonLabs/setup-marathon-cloud@2.0.2 with: version: 1.0.68 - run: | marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --os-version 15 \ --system-image google_apis \ --concurrency-limit 4 \ --output marathon \ --no-progress-bars env: MARATHON_CLOUD_API_KEY: ${{ secrets.MARATHON_CLOUD_API_KEY }} ``` * CircleCI Run the CLI from the official Docker image with the Docker executor. ```yaml version: 2.1 jobs: ui-tests: docker: - image: marathonlabs/marathon-cloud:1.0.68 steps: - checkout - attach_workspace: at: . - run: name: Run tests on Marathon Cloud command: | marathon-cloud run android \ --application app/build/outputs/apk/debug/app-debug.apk \ --test-application app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \ --os-version 15 \ --system-image google_apis \ --output marathon \ --link "$CIRCLE_BUILD_URL" \ --branch "$CIRCLE_BRANCH" \ --no-progress-bars - store_test_results: path: marathon/tests/omni - store_artifacts: path: marathon workflows: test: jobs: - build - ui-tests: requires: [build] ``` Set `MARATHON_CLOUD_API_KEY` in a CircleCI context or project environment variable; the CLI reads it without a flag. The `build` job should persist the APKs to the workspace. Caution The `marathonlabs/marathon-cloud-orb` orb is pinned to CLI 0.3.0 and uses the legacy flag syntax. Do not use it with the current platform. * Bitrise **`run-tests-using-marathon-cloud`** is in the official Bitrise StepLib. On Linux stacks it runs the Docker image; on macOS stacks it installs the CLI from Homebrew. ```yaml - run-tests-using-marathon-cloud@1.0.1: inputs: - api_key: $MARATHON_CLOUD_API_KEY - platform: Android - application: $BITRISE_APK_PATH - test_application: $BITRISE_TEST_APK_PATH - os_version: 15 - system_image: google_apis - output: $BITRISE_DEPLOY_DIR/marathon ``` **Inputs**: `api_key`, `platform` (`Android` or `iOS`), `application`, `test_application`, `os_version`, `system_image`, `device`, `run_name`, `link`, `output`, `filter_file`, `isolated`, `ignore_test_failures`, `xctestplan_filter_file`, `xctestplan_target_name`, `docker_image`. Point the **Deploy to Bitrise.io** step’s test results at `$BITRISE_DEPLOY_DIR/marathon/tests/omni`. * Docker **`marathonlabs/marathon-cloud`** on Docker Hub, tagged `latest` and per CLI version. The image’s working directory is `/work`; mount your build output there. ```bash docker run --rm \ -v "$(pwd)":/work \ -e MARATHON_CLOUD_API_KEY \ marathonlabs/marathon-cloud:1.0.68 \ run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --os-version 15 \ --system-image google_apis \ --output marathon \ --no-progress-bars ``` This is the path for GitLab CI, Jenkins, Buildkite, and any other Docker-capable runner. Pin the version tag rather than `latest`. ## Best practices [Section titled “Best practices”](#best-practices) ### Keep secrets out of config [Section titled “Keep secrets out of config”](#keep-secrets-out-of-config) Store the API token in your CI’s secret store and expose it as `MARATHON_CLOUD_API_KEY`. Never commit it. See [API keys](/cloud/getting-started/api-key/). ### Link runs back to CI [Section titled “Link runs back to CI”](#link-runs-back-to-ci) Set `link` to the CI job or pull request URL, and `branch` and `name`, so a run in the console can be traced to the job that produced it. ### Publish JUnit [Section titled “Publish JUnit”](#publish-junit) Set `output` and point your CI’s test-report publisher at `/tests/omni/marathon_junit_report.xml`. Per-test videos and logs are under `/video` and `/logs`. See [artifacts](/cloud/reference/artifacts/). ### Disable progress bars [Section titled “Disable progress bars”](#disable-progress-bars) Pass `--no-progress-bars` when calling the CLI directly so CI logs stay readable. ### Pin versions [Section titled “Pin versions”](#pin-versions) Pin the action, step, and CLI version. The [releases page](https://github.com/MarathonLabs/marathon-cloud-cli/releases) lists changes. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Tests need to reach internal services [Section titled “Tests need to reach internal services”](#tests-need-to-reach-internal-services) Devices run in Marathon Cloud’s network and there is no tunnel into yours. Point tests at a staging environment reachable from the internet. Backends that fall over under parallel load are a common cause of “passes locally, fails in Cloud”; see [limiting concurrency](/cloud/configuration/batching/#limiting-concurrency). ### Job timeouts [Section titled “Job timeouts”](#job-timeouts) Budget for upload time plus the run. Marathon Cloud targets 15 minutes for the run itself; a 30-minute job timeout is a safe floor for most suites. # CLI installation > Install the Marathon Cloud CLI on macOS, Linux, and Windows. The Marathon Cloud CLI is the primary tool for interacting with the platform. It allows you to submit test runs, monitor progress, and retrieve reports from your terminal or CI/CD environment. The current release is 1.0.68; examples below use that version. ## Quick start [Section titled “Quick start”](#quick-start) ```bash brew tap malinskiy/tap && brew install malinskiy/tap/marathon-cloud ``` ## Installation methods [Section titled “Installation methods”](#installation-methods) * macOS **Homebrew** (recommended) ```bash brew tap malinskiy/tap brew install malinskiy/tap/marathon-cloud ``` **Manual download** Download the universal macOS archive from the [releases page](https://github.com/MarathonLabs/marathon-cloud-cli/releases). It extracts into a versioned directory: ```bash tar xzf marathon-cloud-v1.0.68-universal-apple-darwin.tar.gz sudo mv marathon-cloud-v1.0.68-universal-apple-darwin/marathon-cloud /usr/local/bin/ ``` * Linux **Homebrew** ```bash brew tap malinskiy/tap brew install malinskiy/tap/marathon-cloud ``` **Manual download** Prebuilt binaries are available for amd64, arm64, armhf, and i686 in both gnu and musl variants. Download from the [releases page](https://github.com/MarathonLabs/marathon-cloud-cli/releases), then: The archive extracts into a versioned directory containing the binary, a man page, and shell completions. ```bash tar xzf marathon-cloud-v1.0.68-x86_64-unknown-linux-musl.tar.gz sudo mv marathon-cloud-v1.0.68-x86_64-unknown-linux-musl/marathon-cloud /usr/local/bin/ ``` Debian/Ubuntu packages (`.deb`) are published for each release in both glibc (`marathon-cloud_*.deb`) and musl (`marathon-cloud-musl_*.deb`) variants: ```bash sudo dpkg -i marathon-cloud_v*_amd64.deb ``` **Docker** ```bash docker pull marathonlabs/marathon-cloud:latest alias marathon-cloud='docker run -v "$(pwd)":/work -it --rm marathonlabs/marathon-cloud:latest' marathon-cloud --help ``` The alias mounts your current directory so the CLI can access your APK/IPA files. * Windows Download the Windows zip from the [releases page](https://github.com/MarathonLabs/marathon-cloud-cli/releases) and extract it. Add the directory containing `marathon-cloud.exe` to your system `PATH`. Or use PowerShell. Assets are named `marathon-cloud-v-x86_64-pc-windows-msvc.zip` (or `i686` for 32-bit): ```powershell $version = "1.0.68" Invoke-WebRequest -Uri "https://github.com/MarathonLabs/marathon-cloud-cli/releases/download/$version/marathon-cloud-v$version-x86_64-pc-windows-msvc.zip" -OutFile "marathon-cloud.zip" Expand-Archive -Path "marathon-cloud.zip" -DestinationPath "." ``` Tip Every release includes a `checksums.txt` file with SHA256 hashes for verifying download integrity. ## Verify installation [Section titled “Verify installation”](#verify-installation) ```bash marathon-cloud --version ``` ## Shell completions [Section titled “Shell completions”](#shell-completions) Homebrew installs completions automatically. For manual installs: * Bash ```bash source <(marathon-cloud completions bash) echo 'source <(marathon-cloud completions bash)' >> ~/.bashrc ``` * Zsh ```bash source <(marathon-cloud completions zsh) ``` * Fish ```bash marathon-cloud completions fish | source ``` ## Update [Section titled “Update”](#update) * Homebrew ```bash brew update brew upgrade marathon-cloud ``` * Manual Download the latest version from the [releases page](https://github.com/MarathonLabs/marathon-cloud-cli/releases) and replace the existing binary. ## Next steps [Section titled “Next steps”](#next-steps) * Obtain an [API key](/cloud/getting-started/api-key/) to authenticate your requests. * Explore the [CLI parameters](/cloud/cli/parameters/) reference for command usage. # CLI parameters > Reference for all Marathon Cloud CLI commands, options, exit codes, and the result file. The `marathon-cloud` CLI submits test runs, lists device configurations, and downloads artifacts. This page documents every command and flag as of CLI 1.0.68. ## Commands [Section titled “Commands”](#commands) | Command | Description | | --------------------- | ----------------------------------------------- | | `run android` | Run Android instrumentation tests. | | `run ios` | Run XCTest / XCUITest suites on iOS simulators. | | `run maestro android` | Run Maestro flows on Android. | | `run maestro ios` | Run Maestro flows on iOS. | | `devices android` | List available Android device configurations. | | `download` | Download artifacts and reports for a run. | | `completions` | Generate shell completion scripts. | There is no `devices ios` command; the iOS device list is fixed and documented in [supported devices](/cloud/reference/supported-devices/). ## Exit codes [Section titled “Exit codes”](#exit-codes) The CLI is designed to gate a pipeline. With the default `--wait true`: | Exit code | Meaning | | --------- | ----------------------------------------------------------------------------------------------------------------------- | | `0` | All tests passed, or `--ignore-test-failures` was set. | | `1` | At least one test failed, or the run could not be submitted or completed (validation error, upload failure, API error). | With `--wait false` the CLI exits `0` as soon as the run is accepted, and the result reflects submission only. ## Result file [Section titled “Result file”](#result-file) Pass `--result-file ` to get a machine-readable summary. The format follows the extension: `.json` (default) or `.yaml` / `.yml`. With `--wait true` (default), the file is written when the run finishes: ```json { "id": "01J8ZQ6W5K3R2YV7N4X1M9P0QA", "report": "https://cloud.marathonlabs.io/runs/01J8ZQ6W5K3R2YV7N4X1M9P0QA/report", "state": "passed", "passed": 412, "failed": 0, "ignored": 3, "billable_time": 7260.5 } ``` `state` is `passed` or `failure`. `billable_time` is the total device time in seconds, summed across all devices used by the run. With `--wait false`, only the run ID is written: ```json { "id": "01J8ZQ6W5K3R2YV7N4X1M9P0QA" } ``` Use the ID with `marathon-cloud download --id ` later in the pipeline. ## Common run flags [Section titled “Common run flags”](#common-run-flags) These flags are accepted by every `run` subcommand. | Flag | Description | Default | | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | | `--api-key` | Marathon Cloud API token. Required. Also read from `MARATHON_CLOUD_API_KEY`. | | | `--base-url` | API base URL. | `https://cloud.marathonlabs.io/api` | | `-o`, `--output` | Local directory to download reports and artifacts into after the run. | | | `--wait` | Wait for the run to complete and exit with its result. | `true` | | `--name` | Human-readable run name shown in the console. | | | `--link` | URL back to the CI job or pull request. | | | `--branch` | Branch name recorded with the run. | | | `--project` | Project slug, when your organization uses multiple projects. | | | `--filter-file` | YAML [filter file](/cloud/configuration/filtering/). | | | `--isolated` | Run every test in its own batch. See [batching](/cloud/configuration/batching/). | `false` | | `--ignore-test-failures` | Exit `0` even when tests fail. | `false` | | `--code-coverage` | Collect code coverage. | `false` | | `--concurrency-limit` | Cap the number of devices used in parallel. An escape hatch for suites or backends that cannot handle parallel execution; it makes runs slower. See [batching](/cloud/configuration/batching/#limiting-concurrency). | unlimited | | `--result-file` | Path to write the [result file](#result-file). | | | `--no-progress-bars` | Disable interactive progress output. Recommended in CI. | `false` | | `--retry-quota-test-uncompleted` | Allowed re-runs per test for executions that did not complete (device or infrastructure failure). | platform default | | `--retry-quota-test-preventive` | Allowed preventive retries per test. See [retries](/cloud/configuration/retries/). | platform default | | `--retry-quota-test-reactive` | Allowed reactive retries per test after a failure. | platform default | | `--no-retries` | Disable all retries. Sets every quota to `0`. Conflicts with the `--retry-quota-*` flags. | `false` | | `--analytics-read-only` | Use historical data for scheduling but do not record this run into it. Useful for experiments that should not skew future runs. | `false` | `--wait`, `--isolated`, `--ignore-test-failures`, `--code-coverage`, and `--analytics-read-only` accept an optional value: `--isolated`, `--isolated true`, and `--isolated false` are all valid. The remaining boolean flags (`--no-retries`, `--no-progress-bars`, `--mock-location`, `--profiling`) are switches and take no value. ## marathon-cloud run android [Section titled “marathon-cloud run android”](#marathon-cloud-run-android) ```bash marathon-cloud run android \ --application app.apk \ --test-application tests.apk ``` | Flag | Description | Default | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | | `-a`, `--application` | Application APK. | | | `-t`, `--test-application` | Instrumentation test APK. | | | `--application-bundle` | Multi-module bundle as `app.apk,test.apk`. Repeatable. Alternative to `--application` + `--test-application`. | | | `--library-bundle` | Test APK for a library module without a separate app APK. Repeatable. | | | `--os-version` | Android version: `8`, `8.1`, `9`, `10`, `11`, `12`, `13`, `14`, `15`, `16`, `17`. Versions `15`+ require a `google_apis*` image; versions below `10` require `google_apis`. | platform default | | `--system-image` | `default`, `google_apis`, `google_apis_playstore`. | platform default | | `--arch` | Emulator CPU architecture: `amd64` or `arm64`. `arm64` needs Android `9` or later; `default` images are `arm64` only. See [architecture](/cloud/reference/supported-devices/#architecture). | `amd64` | | `--device` | Device type ID from `marathon-cloud devices android`. Use `watch` or `tv` for those form factors; anything else is treated as a phone. See [supported devices](/cloud/reference/supported-devices/#android) for valid OS and image combinations. | phone | | `--flavor` | Deprecated. Only `native` is supported; the flag is ignored. | | | `--instrumentation-arg` | `KEY=VALUE` passed to the instrumentation runner. Repeatable. | | | `--pull-files` | Files to pull from the device after each batch, as `ROOT:PATH` where `ROOT` is `EXTERNAL_STORAGE` or `APP_DATA` and `PATH` is relative to that root, e.g. `EXTERNAL_STORAGE:Documents/results`. Repeatable. | | | `--mock-location` | Allow mock location providers. Switch; takes no value. | `false` | | `--front-camera` | `none`, `emulated`. Requires a `google_apis*` image. | `none` | | `--back-camera` | `none`, `virtualscene`, `emulated`. Requires a `google_apis*` image. | `none` | | `--profiling` | Collect performance profiling data. | `false` | ## marathon-cloud run ios [Section titled “marathon-cloud run ios”](#marathon-cloud-run-ios) ```bash marathon-cloud run ios \ --application YourApp.app \ --test-application YourAppUITests-Runner.app ``` | Flag | Description | Default | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | | `-a`, `--application` | Application bundle: a `.app` directory, or a `.zip` / `.ipa` containing it. Required. | | | `-t`, `--test-application` | Test runner bundle: a `.app` or `.xctest` directory, or a `.zip` / `.ipa` containing it. Required. | | | `--os-version` | iOS version: `18.2`, `18.4`, `26.1`. Inferred from `--device` when omitted. | | | `--device` | `iPhone-11`, `iPhone-16`, `iPhone-16-Plus`, `iPhone-16-Pro`, `iPhone-16-Pro-Max`, `iPhone-17`, `iPhone-17-Pro`, `iPhone-17-Pro-Max`. Not every device is available on every iOS version; see [supported devices](/cloud/reference/supported-devices/#ios). | | | `--xcode-version` | Deprecated and hidden. Xcode is selected from `--os-version`; passing this flag prints a warning. | | | `--batch-isolation` | `default` or `uninstall_app` (remove the app between batches). | `default` | | `--xctestrun-env` | `KEY=VALUE` environment variable for the test runner process. Repeatable. | | | `--xctestrun-test-env` | `KEY=VALUE` environment variable for the app under test. Repeatable. | | | `--xctestplan-filter-file` | `.xctestplan` file used to select tests. | | | `--xctestplan-target-name` | Target name inside the test plan. | | | `--test-timeout-default` | Per-test timeout in seconds. | `300` | | `--test-timeout-max` | Upper bound in seconds for per-test timeouts set inside the test plan. | | | `--granted-permission` | Permission to pre-grant. Repeatable. Values: `calendar`, `contacts-limited`, `contacts`, `location`, `location-always`, `photos-add`, `photos`, `media-library`, `microphone`, `motion`, `notifications`, `reminders`, `siri`. | | Directories are zipped by the CLI before upload. ## marathon-cloud run maestro [Section titled “marathon-cloud run maestro”](#marathon-cloud-run-maestro) Maestro runs accept the [common run flags](#common-run-flags) plus the flags below. `--system-image`, `--flavor`, and the Android device-feature flags are not available; Maestro runs always use `google_apis` images. * Android ```bash marathon-cloud run maestro android \ --application app.apk \ --test-application flows/ \ [FLOW...] ``` | Flag | Description | Default | | -------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------- | | `-a`, `--application` | Application APK. Required. | | | `-t`, `--test-application` | Directory containing Maestro YAML flows. Required. | | | `--os-version` | Android version, same values as `run android`. | platform default | | `--arch` | `amd64` or `arm64`. `arm64` needs Android `9` or later. | `amd64` | | `--device` | Device type ID from `marathon-cloud devices android`. | phone | | `--maestro-env` | `KEY=VALUE` made available to flows as `${KEY}`. Repeatable. | | | `FLOW...` | Positional paths (files or directories) inside `--test-application` to run. Runs everything when omitted. | | * iOS ```bash marathon-cloud run maestro ios \ --application YourApp.app \ --test-application flows/ \ [FLOW...] ``` | Flag | Description | Default | | -------------------------- | --------------------------------------------------------------------------------------------------------- | ------- | | `-a`, `--application` | Application bundle (`.app`, `.zip`, or `.ipa`). Required. | | | `-t`, `--test-application` | Directory containing Maestro YAML flows. Required. | | | `--os-version` | iOS version, same values as `run ios`. | | | `--device` | iOS device, same values as `run ios`. | | | `--maestro-env` | `KEY=VALUE` made available to flows as `${KEY}`. Repeatable. | | | `FLOW...` | Positional paths (files or directories) inside `--test-application` to run. Runs everything when omitted. | | ## marathon-cloud devices android [Section titled “marathon-cloud devices android”](#marathon-cloud-devices-android) Prints the Android device catalog as YAML, including the device type IDs accepted by `--device`. ```bash marathon-cloud devices android --api-key $MARATHON_CLOUD_API_KEY ``` | Flag | Description | Default | | -------------------- | ------------------------------------ | ----------------------------------- | | `--api-key` | Marathon Cloud API token. Required. | | | `--base-url` | API base URL. | `https://cloud.marathonlabs.io/api` | | `--no-progress-bars` | Disable interactive progress output. | `false` | ## marathon-cloud download [Section titled “marathon-cloud download”](#marathon-cloud-download) Download artifacts for an existing run. See [artifacts](/cloud/reference/artifacts/) for the directory layout. ```bash marathon-cloud download --id --output artifacts/ ``` | Flag | Description | Default | | -------------------- | --------------------------------------------------------- | ----------------------------------- | | `--id` | Run ID. Required. | | | `-o`, `--output` | Directory to download into. Required. | | | `--wait` | Wait for the run to finish before downloading. | `true` | | `--glob` | Only download paths matching the glob, e.g. `'tests/**'`. | everything | | `--api-key` | Marathon Cloud API token. Required. | | | `--base-url` | API base URL. | `https://cloud.marathonlabs.io/api` | | `--no-progress-bars` | Disable interactive progress output. | `false` | ## marathon-cloud completions [Section titled “marathon-cloud completions”](#marathon-cloud-completions) ```bash marathon-cloud completions bash > /etc/bash_completion.d/marathon-cloud ``` Supported shells: `bash`, `zsh`, `fish`, `powershell`, `elvish`. ## Global options [Section titled “Global options”](#global-options) | Flag | Description | | ----------------- | ------------------------------------------------------- | | `-v`, `--verbose` | Increase log verbosity. Repeat for more detail (`-vv`). | | `-q`, `--quiet` | Decrease log verbosity. Repeat to silence more. | | `-h`, `--help` | Show help. | | `-V`, `--version` | Show the CLI version. | Note For task-oriented walkthroughs, see the [Android](/cloud/android/setup/), [iOS](/cloud/ios/setup/), and [Maestro](/cloud/maestro/setup/) setup guides. # Batching and parallelism > How Marathon Cloud groups tests into batches, spreads them across devices, and when to constrain it. Marathon Cloud runs your suite on a pool of devices in parallel. Two decisions shape every run: how many devices to use, and how many tests to send to a device at once. Both are made for you from your suite’s history; this page explains the defaults and the two flags that override them. ## Parallelism [Section titled “Parallelism”](#parallelism) When a run starts, the scheduler estimates the total execution time of the suite from previous runs (or from a conservative default the first time) and sizes the device pool so the run finishes in about 15 minutes. A suite that needs two hours of device time gets roughly eight devices; a suite that needs ten minutes gets one or two. Fifteen minutes is a target, not a hard limit. Very long individual tests, a suite with no history yet, or an unusually slow batch can push a run past it. ## Batching [Section titled “Batching”](#batching) A batch is a group of tests sent to one device together. The tests run in sequence and their results, logs, and videos come back as a group. Between batches the device pulls the previous batch’s artifacts, receives the next batch, and starts it; the app is installed once when the device is set up, not per batch. Batching amortizes that per-batch overhead. Sending tests one at a time would spend more time on transfer than on testing. The scheduler picks batch sizes from each test’s historical duration so that no single batch dominates the run. You do not configure this directly. Within a batch, tests run in the same app process in sequence, the same way they do when you run a class locally. State a test leaves behind, such as logged-in sessions, files, or preferences, is visible to the tests that follow it in the batch. ## Isolated execution [Section titled “Isolated execution”](#isolated-execution) `--isolated` sets the batch size to one, so no test shares a batch with another. ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --isolated ``` Use it to confirm whether a failure depends on tests that ran before it. Expect the run to take longer and cost more, since per-batch overhead is now paid once per test. Once you have found the leaking state, fix the test and drop the flag. On iOS, `--batch-isolation uninstall_app` removes and reinstalls the app between batches without going down to one test per batch. ## Limiting concurrency [Section titled “Limiting concurrency”](#limiting-concurrency) `--concurrency-limit N` caps the pool at `N` devices. ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --concurrency-limit 4 ``` This is an escape hatch, not a tuning knob. Use it when the tests, or the systems they talk to, cannot cope with parallel execution: * Tests share a backend account or fixture and interfere with each other when run at the same time. * A staging backend falls over under the load of many devices hitting it at once. * Tests depend on a rate-limited third-party API. A limit slows the run in proportion: a two-hour suite capped at four devices takes about thirty minutes. The better fix is to make tests independent and give the backend headroom, then remove the limit. ## Debugging “passes locally, fails on Cloud” [Section titled “Debugging “passes locally, fails on Cloud””](#debugging-passes-locally-fails-on-cloud) The usual cause is state your local emulator has accumulated that the test silently depends on: a logged-in account, granted permissions, cached data, a snapshot restored at boot. To reproduce the Cloud environment locally: 1. Wipe the emulator’s data and delete its snapshots. 2. Cold boot it. 3. Run the failing test alone, then as part of its class. If it passes alone but fails in the class, it depends on another test’s state; `--isolated` on Cloud will confirm it. If it fails alone on a clean emulator, the test depends on environment that the cloud device does not have, such as a pre-installed app, a system setting, or network access to something internal. Note See [retries](/cloud/configuration/retries/) for how failures inside a batch are re-run. # Test filtering > Use a YAML filter file to select which tests to run on Marathon Cloud. Marathon Cloud filters your test suite using a YAML file passed with `--filter-file`. Use it to run subsets such as smoke tests, a single feature’s tests, or everything except a quarantined package. The file uses the same `filteringConfiguration` schema as the OSS Marathon runner, so existing filter files work unchanged. ## Quick start [Section titled “Quick start”](#quick-start) ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --filter-file filters.yaml ``` filters.yaml ```yaml filteringConfiguration: allowlist: - type: "annotation" values: ["com.example.annotations.SmokeTest"] blocklist: - type: "package" values: ["com.example.legacy"] ``` ## File structure [Section titled “File structure”](#file-structure) The top-level key is `filteringConfiguration`. It contains an optional `allowlist` and an optional `blocklist`; at least one must be present. * **allowlist**: a test runs only if it matches at least one allowlist filter. * **blocklist**: a test is skipped if it matches any blocklist filter. Each filter has a `type` and exactly one selector: `values`, `regex`, or `file`. The exception is `composition`, which takes `op` and a nested `filters` list instead of a selector. | Selector | Description | | -------- | -------------------------------------------------------------------------------------------------------------- | | `values` | List of exact values to match. | | `regex` | A single regular expression. | | `file` | Path to a text file with one value per line, relative to the filter file. Lines starting with `#` are ignored. | ## Filter types [Section titled “Filter types”](#filter-types) | Type | Matches against | | ---------------------------- | ---------------------------------------------------------------------------- | | `fully-qualified-class-name` | `com.example.LoginTest` | | `simple-class-name` | `LoginTest` | | `fully-qualified-test-name` | `com.example.LoginTest#testLogin` | | `simple-test-name` | `LoginTest#testLogin` | | `package` | `com.example` | | `method` | `testLogin` | | `annotation` | Fully qualified annotation name, e.g. `com.example.annotations.SmokeTest` | | `composition` | Combines nested `filters` with an `op` (`UNION`, `INTERSECTION`, `SUBTRACT`) | The `allure`, `fragmentation`, and `annotationData` filter types from OSS Marathon are not supported and cause a validation error. ### Maestro flows [Section titled “Maestro flows”](#maestro-flows) For Maestro runs the directory path becomes the package (`flows/auth/login.yaml` → `auth`), the class is always `MaestroTest`, the method is the file name without extension (`login`), and the flow’s `tags:` list is exposed to the `annotation` filter. A single flow is therefore `auth.MaestroTest#login` for `fully-qualified-test-name`, or `login` for `method`. See [Maestro setup](/cloud/maestro/setup/#filtering-by-tag-or-path) for a worked example. ## Examples [Section titled “Examples”](#examples) ### Run one test class [Section titled “Run one test class”](#run-one-test-class) ```yaml filteringConfiguration: allowlist: - type: "fully-qualified-class-name" values: ["com.example.LoginTest"] ``` ### Run tests matching a regex [Section titled “Run tests matching a regex”](#run-tests-matching-a-regex) ```yaml filteringConfiguration: allowlist: - type: "fully-qualified-test-name" regex: "com\\.example\\.checkout\\..*" ``` ### Load test names from a file [Section titled “Load test names from a file”](#load-test-names-from-a-file) ```yaml filteringConfiguration: allowlist: - type: "fully-qualified-test-name" file: "smoke-tests.txt" ``` smoke-tests.txt ```text com.example.LoginTest#testLogin com.example.CheckoutTest#testHappyPath ``` ### Combine filters [Section titled “Combine filters”](#combine-filters) Run every test in `com.example.ui` that is either a `@Smoke` test or has a method name starting with `testCritical`, excluding anything tagged `@Flaky`. ```yaml filteringConfiguration: allowlist: - type: "package" values: ["com.example.ui"] - type: "composition" op: "UNION" filters: - type: "annotation" values: ["com.example.annotations.Smoke"] - type: "method" regex: "testCritical.*" blocklist: - type: "annotation" values: ["com.example.annotations.Flaky"] ``` ## Apply the filter [Section titled “Apply the filter”](#apply-the-filter) 1. Save the filter definition as a YAML file, for example `smoke-tests.yaml`. 2. Pass it with `--filter-file`. The flag works with `run android`, `run ios`, and `run maestro`. ```bash marathon-cloud run android \ --api-key $MARATHON_CLOUD_API_KEY \ --application app.apk \ --test-application tests.apk \ --filter-file smoke-tests.yaml ``` The CLI validates the file before uploading and reports the failing filter if the schema is wrong. ## Best practices [Section titled “Best practices”](#best-practices) * Store filter files in version control next to your CI configuration. * Name files by intent: `smoke-tests.yaml`, `nightly-regression.yaml`. * Prefer `annotation` filters over hand-maintained class lists; they stay correct as tests move between classes. Note The schema is shared with the OSS runner. See the [Marathon runner filtering docs](https://marathonlabs.github.io/marathon/configuration/filtering/) for the full reference, including composition semantics. # Retries > How Marathon Cloud retries failed and flaky tests, and how to tune or disable it. Marathon Cloud retries individual tests, not whole suites. The goal is to separate tests that are genuinely broken from tests that fail intermittently, without you re-running the pipeline. ## Kinds of retries [Section titled “Kinds of retries”](#kinds-of-retries) ### Reactive retries [Section titled “Reactive retries”](#reactive-retries) When a test fails, it is scheduled again on a different device than the one it failed on. A test that passes on any attempt is reported as **flaky**; a test that fails every attempt is reported as **failed**. The number of reactive retries per test is set by the platform from your suite’s history, and can be overridden with `--retry-quota-test-reactive`. ### Preventive retries [Section titled “Preventive retries”](#preventive-retries) Marathon Cloud keeps a per-test history of outcomes across your runs. When a test has been unstable recently, the scheduler may run it more than once from the start, without waiting for a failure. This costs a little device time but returns a verdict sooner than failing first and retrying later, which matters for the 15-minute target. The preventive quota is set by the platform and can be overridden with `--retry-quota-test-preventive`. ### Uncompleted executions [Section titled “Uncompleted executions”](#uncompleted-executions) A test that never produces a result — the device crashed, the process was killed, the batch timed out — is treated as *uncompleted* rather than failed. Uncompleted executions are re-run under a separate quota, `--retry-quota-test-uncompleted`, so infrastructure problems do not consume the retries reserved for real test failures. ## What the report shows [Section titled “What the report shows”](#what-the-report-shows) Each test in the report is one of: | Verdict | Meaning | | ------- | --------------------------------------------- | | Passed | Passed on the first attempt. | | Flaky | Failed at least once, then passed on a retry. | | Failed | Failed on every attempt. | | Ignored | Skipped by the test framework. | Flaky tests count as passing for the run’s exit code, and are listed separately so you can find them. The flakiness history used by preventive retries is internal to the scheduler and is not currently shown in the console. ## Configuration [Section titled “Configuration”](#configuration) Retries are on by default with quotas chosen by the platform. Override them per run: ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --retry-quota-test-reactive 2 \ --retry-quota-test-preventive 0 \ --retry-quota-test-uncompleted 1 ``` Disable all retries: ```bash marathon-cloud run android \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --no-retries ``` Both examples read the API key from `MARATHON_CLOUD_API_KEY`. `--no-retries` sets every quota to zero and cannot be combined with the `--retry-quota-*` flags. Note Turning retries off is useful when you are measuring raw flakiness or debugging a specific test. For gating pull requests, leave them on. ## Reducing flakiness at the source [Section titled “Reducing flakiness at the source”](#reducing-flakiness-at-the-source) Retries hide flakiness; they do not fix it. Tests that show up as flaky repeatedly are candidates for: * **Isolation**: run with `--isolated` to rule out state leaking between tests in a batch. See [batching](/cloud/configuration/batching/). * **Concurrency**: if flakiness correlates with suite size, your shared backend may be the bottleneck. See [limiting concurrency](/cloud/configuration/batching/#limiting-concurrency). * **Refactoring**: replace sleeps with idling resources or explicit waits, and remove dependencies on test ordering. # API keys > Create, use, and delete personal access tokens for the Marathon Cloud CLI and CI. The CLI and every CI integration authenticate with a personal access token. Tokens belong to your user and act with your team role; create separate tokens for your laptop and for each CI system so one can be deleted without affecting the others. ## Quick start [Section titled “Quick start”](#quick-start) ```bash export MARATHON_CLOUD_API_KEY=YOUR_TOKEN marathon-cloud devices android ``` Every command reads the token from `MARATHON_CLOUD_API_KEY`, or from `--api-key` if you prefer to pass it explicitly. ## Create a token [Section titled “Create a token”](#create-a-token) 1. Sign in to the [Marathon Cloud console](https://cloud.marathonlabs.io/) and open the user menu in the top-right corner. 2. Choose **Tokens**. The page is at `/tokens`. 3. Under **Create token**, enter a **Name** that identifies where the token will be used, for example `github-actions` or `laptop`, and click **Create**. 4. A **Token created** dialog shows the value once. Use the copy button and store it in your secret manager before closing the dialog. Danger The token value is not shown again. If you lose it, delete the token and create a new one. ## Delete a token [Section titled “Delete a token”](#delete-a-token) Active tokens are listed under **Active tokens** with their name and creation date. Click the trash icon next to a token and confirm with **Delete**. Requests using that token are rejected with `401 Unauthorized` from that moment. ## Where to store it [Section titled “Where to store it”](#where-to-store-it) | Environment | Store as | | -------------- | -------------------------------------------------------------------- | | GitHub Actions | Repository or organization secret, passed as `apiKey` | | CircleCI | Context or project environment variable `MARATHON_CLOUD_API_KEY` | | Bitrise | Secret `MARATHON_CLOUD_API_KEY`, passed as `api_key` | | Local shell | `MARATHON_CLOUD_API_KEY` in your shell profile, or a secrets manager | Never commit a token to version control. See [CI/CD integration](/cloud/ci-cd/overview/) for per-platform examples. # Overview > What Marathon Cloud does, how a run works, and what it supports. Marathon Cloud runs your Android and iOS UI test suite on a pool of virtual devices, in parallel, and gives you a verdict fast enough to gate a pull request. You upload the app and test binaries with a CLI; the platform provisions devices, distributes tests, retries the flaky ones, and hands back a report, JUnit XML, video, and logs. It is built on the open-source [Marathon test runner](https://github.com/MarathonLabs/marathon), the execution engine used by teams whose apps reach hundreds of millions of users. ## How a run works [Section titled “How a run works”](#how-a-run-works) 1. **Submit**: `marathon-cloud run android|ios|maestro` uploads the binaries and starts the run. In CI this is one step; see [CI/CD integration](/cloud/ci-cd/overview/). 2. **Plan**: the scheduler parses the test list, looks up how long each test has taken in past runs, and sizes a device pool to finish in about 15 minutes. 3. **Execute**: devices are provisioned fresh, the app is installed, and tests run in [batches](/cloud/configuration/batching/). Failed tests are [retried](/cloud/configuration/retries/) on a different device. 4. **Report**: the CLI exits `0` or `1`, and the run is available in the console with per-test video, logcat or system logs, and Allure and JUnit reports. Artifacts can be pulled into CI with `--output` or `marathon-cloud download`. The 15-minute figure is a target the scheduler optimizes for, not a guarantee. Suites with very long individual tests, or a first run with no history, can take longer. ## What it is good at [Section titled “What it is good at”](#what-it-is-good-at) * **Gating pull requests**: a full UI suite in one CI step, with a clear pass/fail exit code. * **Absorbing flakiness**: retries happen per test, on a different device, and the report separates flaky from failed. * **Scaling without ops**: no device farm to maintain, no shard configuration, no emulator images to keep current. * **Second-level billing**: you pay for device time actually used; see [pricing](/cloud/account/pricing/). ## What it is not [Section titled “What it is not”](#what-it-is-not) * Not a physical device farm. Android runs on emulators and iOS on arm64 simulators. Hardware-specific behaviour is out of scope. * Not a tunnel into your network. Backends under test must be reachable from the internet. * Not a replacement for unit tests. It is built for instrumentation and UI suites that need a device. ## Supported frameworks [Section titled “Supported frameworks”](#supported-frameworks) | Platform | Frameworks | | -------------- | --------------------------------------------------------------------------------------------------------------------- | | Android | Espresso, UIAutomator, Kakao, Kaspresso, Cucumber, Flutter via Patrol; anything that runs as an instrumentation test. | | iOS | XCTest, XCUITest, KIF, Flutter via Patrol. | | Cross-platform | Maestro YAML flows on Android and iOS. | ## Reports and artifacts [Section titled “Reports and artifacts”](#reports-and-artifacts) Every run produces an Allure report, JUnit XML, the Marathon HTML timeline, per-test screen recordings, and per-test device logs. See [artifacts](/cloud/reference/artifacts/) for the layout and retention. ## Support [Section titled “Support”](#support) Email . Include the run ID from the CLI output or console URL when reporting a problem with a run. Teams on an Enterprise plan get a shared Slack Connect channel after onboarding. ## Next steps [Section titled “Next steps”](#next-steps) * [Quickstart](/cloud/getting-started/quickstart/): first run from your terminal in a few minutes. * [CI/CD integration](/cloud/ci-cd/overview/): the GitHub Action, CircleCI orb, Bitrise step, and Docker image. * [Migrating from Firebase Test Lab](/cloud/migration/firebase-test-lab/) or [BrowserStack](/cloud/migration/browserstack/). # Quickstart > Get started with Marathon Cloud in minutes. This guide walks you through installing the Marathon Cloud CLI, obtaining an API key, and running your first test suite. If you do not have test binaries to hand, step 3 has prebuilt samples so you can see a run end to end first. 1. ### Install the CLI [Section titled “Install the CLI”](#install-the-cli) ```bash brew tap malinskiy/tap && brew install malinskiy/tap/marathon-cloud ``` Use Homebrew to install the Marathon Cloud CLI on macOS or Linux. For other platforms, refer to the [CLI installation](/cloud/cli/installation/) guide. 2. ### Obtain an API key [Section titled “Obtain an API key”](#obtain-an-api-key) In the [Marathon Cloud console](https://cloud.marathonlabs.io/), open the user menu and choose **Tokens**, then create a token and export it: ```bash export MARATHON_CLOUD_API_KEY=YOUR_TOKEN ``` See [API keys](/cloud/getting-started/api-key/) for storing it in CI. 3. ### Prepare your application [Section titled “Prepare your application”](#prepare-your-application) * Android ```bash ./gradlew :app:assembleDebug :app:assembleDebugAndroidTest ``` Run the Gradle task to build the debug APK and the test APK. * iOS ```bash xcodebuild build-for-testing \ -workspace YourApp.xcworkspace \ -scheme YourApp \ -destination 'generic/platform=iOS Simulator' \ -derivedDataPath build ``` Build for the **simulator**; Marathon Cloud runs iOS tests on ARM64 simulators, so a device build will not run. * Sample suite Prebuilt binaries from the [samples repository](https://github.com/MarathonLabs/samples/releases), if you want to see a run before wiring up your own project: ```bash curl -LO https://github.com/MarathonLabs/samples/releases/download/samples-2023.06/sample-app.apk curl -LO https://github.com/MarathonLabs/samples/releases/download/samples-2023.06/sample-app-androidTest.apk curl -LO https://github.com/MarathonLabs/samples/releases/download/samples-2023.06/sample-ios.zip curl -LO https://github.com/MarathonLabs/samples/releases/download/samples-2023.06/sample-iosUITests-Runner.zip ``` Use `sample-app.apk` / `sample-app-androidTest.apk` in place of the Android paths below, or `sample-ios.zip` / `sample-iosUITests-Runner.zip` for iOS. 4. ### Run your first test [Section titled “Run your first test”](#run-your-first-test) * Android ```bash marathon-cloud run android \ --api-key YOUR_API_KEY \ --application app/build/outputs/apk/debug/app-debug.apk \ --test-application app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk ``` * iOS ```bash marathon-cloud run ios \ --api-key YOUR_API_KEY \ --application build/Build/Products/Debug-iphonesimulator/YourApp.app \ --test-application build/Build/Products/Debug-iphonesimulator/YourAppUITests-Runner.app ``` The CLI uploads the binaries, waits for the run, and streams progress. 5. ### Read the result [Section titled “Read the result”](#read-the-result) When the run finishes the CLI prints a link to the report and exits `0` if every test passed or `1` if any failed. Open the link for per-test video, logs, and the Allure report. Add `--output ./results` to download JUnit XML and artifacts locally. ## Next steps [Section titled “Next steps”](#next-steps) * Learn more about [Android setup](/cloud/android/setup/) or [iOS setup](/cloud/ios/setup/). * Configure [CI/CD integration](/cloud/ci-cd/overview/) for automated testing. * Explore [CLI parameters](/cloud/cli/parameters/) for advanced configuration. # iOS setup > Build for the simulator and run XCTest, XCUITest, Patrol, and KIF suites on Marathon Cloud. Marathon Cloud runs iOS tests on ARM64 iOS Simulators. XCTest, XCUITest, Flutter via [Patrol](https://patrol.leancode.co/), and KIF are supported. ## Quick start [Section titled “Quick start”](#quick-start) ```bash marathon-cloud run ios \ --api-key $MARATHON_CLOUD_API_KEY \ --application build/Build/Products/Debug-iphonesimulator/YourApp.app \ --test-application build/Build/Products/Debug-iphonesimulator/YourAppUITests-Runner.app ``` ## Build for testing [Section titled “Build for testing”](#build-for-testing) Build with `xcodebuild build-for-testing` against a **simulator** destination. Device builds (`Debug-iphoneos`) will not run. 1. Build from the root of your project. ```bash xcodebuild build-for-testing \ -workspace YourApp.xcworkspace \ -scheme YourApp \ -destination 'generic/platform=iOS Simulator' \ -derivedDataPath build ``` Use `-project YourApp.xcodeproj` instead of `-workspace` if you do not use a workspace. 2. Locate the bundles. ```text build/Build/Products/Debug-iphonesimulator/YourApp.app build/Build/Products/Debug-iphonesimulator/YourAppUITests-Runner.app ``` For unit-test targets that run inside the host app, the test bundle is `YourAppTests.xctest` inside the app’s `PlugIns` directory; pass that as `--test-application`. ## Run tests [Section titled “Run tests”](#run-tests) ```bash marathon-cloud run ios \ --api-key $MARATHON_CLOUD_API_KEY \ --application build/Build/Products/Debug-iphonesimulator/YourApp.app \ --test-application build/Build/Products/Debug-iphonesimulator/YourAppUITests-Runner.app \ --os-version 18.4 \ --device iPhone-16 ``` The CLI accepts `.app` and `.xctest` directories and zips them before upload. Pre-zipped `.zip` files and `.ipa` archives are also accepted. ### Device selection [Section titled “Device selection”](#device-selection) | Parameter | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--os-version` | `18.2`, `18.4`, or `26.1`. Inferred from `--device` when omitted. | | `--device` | `iPhone-11`, `iPhone-16`, `iPhone-16-Plus`, `iPhone-16-Pro`, `iPhone-16-Pro-Max` on iOS 18.x; `iPhone-16-Pro`, `iPhone-17`, `iPhone-17-Pro`, `iPhone-17-Pro-Max` on iOS 26.1. | | `--batch-isolation` | `default`, or `uninstall_app` to remove the app between batches. | Xcode is selected by the platform from the iOS version. `--xcode-version` is deprecated and ignored. ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) The examples below omit `--api-key`; the CLI reads it from `MARATHON_CLOUD_API_KEY` when the flag is absent. ### Environment variables [Section titled “Environment variables”](#environment-variables) ```bash marathon-cloud run ios \ --application YourApp.app \ --test-application YourAppUITests-Runner.app \ --xctestrun-env "API_BASE_URL=https://api.staging.example.com" \ --xctestrun-test-env "ENABLE_LOGGING=YES" ``` `--xctestrun-env` is visible to the test runner process; `--xctestrun-test-env` is visible to the app under test. Both are repeatable. ### Test plans [Section titled “Test plans”](#test-plans) Select tests with an `.xctestplan` file instead of a [filter file](/cloud/configuration/filtering/): ```bash marathon-cloud run ios \ --application YourApp.app \ --test-application YourAppUITests-Runner.app \ --xctestplan-filter-file YourApp.xctestplan \ --xctestplan-target-name "YourAppUITests" ``` ### Permissions [Section titled “Permissions”](#permissions) Pre-grant system permissions so tests do not hit permission dialogs: ```bash marathon-cloud run ios \ --application YourApp.app \ --test-application YourAppUITests-Runner.app \ --granted-permission location \ --granted-permission notifications ``` Values: `calendar`, `contacts-limited`, `contacts`, `location`, `location-always`, `photos-add`, `photos`, `media-library`, `microphone`, `motion`, `notifications`, `reminders`, `siri`. ### Timeouts [Section titled “Timeouts”](#timeouts) Timeouts are in seconds. ```bash marathon-cloud run ios \ --application YourApp.app \ --test-application YourAppUITests-Runner.app \ --test-timeout-default 300 \ --test-timeout-max 600 ``` `--test-timeout-default` applies to every test (default `300`). `--test-timeout-max` caps timeouts that a test plan sets higher. ### Code coverage [Section titled “Code coverage”](#code-coverage) ```bash marathon-cloud run ios \ --application YourApp.app \ --test-application YourAppUITests-Runner.app \ --code-coverage ``` ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Tests never start on the simulator [Section titled “Tests never start on the simulator”](#tests-never-start-on-the-simulator) The bundle was probably built for `iphoneos`. Rebuild with `-destination 'generic/platform=iOS Simulator'` and pick the bundles from `Debug-iphonesimulator`. ### Tests fail to launch with missing symbols [Section titled “Tests fail to launch with missing symbols”](#tests-fail-to-launch-with-missing-symbols) Confirm all frameworks the test runner links are embedded in the runner target, and enable **Allow testing Host Application APIs** if tests import the app module. ### Permission dialogs block tests [Section titled “Permission dialogs block tests”](#permission-dialogs-block-tests) Pass the permissions with `--granted-permission` rather than tapping through dialogs in test code. Note See [CLI parameters](/cloud/cli/parameters/#marathon-cloud-run-ios) for every flag. # Maestro setup > Run Maestro flows on Android and iOS devices in Marathon Cloud. Marathon Cloud supports executing Maestro flows on both Android and iOS. The platform handles device provisioning and parallel execution of your YAML-defined flows. ## Quick start [Section titled “Quick start”](#quick-start) ```bash marathon-cloud run maestro android \ --api-key $MARATHON_CLOUD_API_KEY \ --application app-debug.apk \ --test-application flows/ ``` ## Flow structure [Section titled “Flow structure”](#flow-structure) Each Maestro flow is a separate YAML file and becomes one test that can run in parallel with the others. Marathon Cloud discovers flows the same way `maestro test` does, so an existing workspace works unchanged. ### Example flow [Section titled “Example flow”](#example-flow) ```yaml appId: com.example.app tags: - smoke --- - launchApp - tapOn: "Login" - inputText: "user@example.com" - tapOn: "Password" - inputText: "password123" - tapOn: "Submit" - assertVisible: "Welcome" ``` ### Workspace layout and `config.yaml` [Section titled “Workspace layout and config.yaml”](#workspace-layout-and-configyaml) Flow discovery follows Maestro’s workspace rules: * By default only YAML files **directly inside** `--test-application` are flows. Files in subdirectories are ignored unless a `config.yaml` at the root of the flows directory lists them. * Flows referenced by `runFlow` (subflows) should live outside the matched patterns so they are not run on their own. * `includeTags` / `excludeTags` in `config.yaml` are applied at parse time, before Marathon Cloud’s own filtering. * `executionOrder` is not supported; a workspace that defines it is rejected. Flows always run independently. A typical layout: ```text flows/ ├── config.yaml ├── login.yaml ├── checkout/ │ └── happy_path.yaml └── subflows/ └── sign_in.yaml # used via runFlow, not run directly ``` flows/config.yaml ```yaml flows: - "*.yaml" - "checkout/**" ``` A flow’s `name:` field is not used for test identity; the file name is. Two files with the same name in different directories are distinct tests. ## Run Maestro tests [Section titled “Run Maestro tests”](#run-maestro-tests) Marathon Cloud provides separate subcommands for Android and iOS Maestro execution. * Android **Command** ```bash marathon-cloud run maestro android \ --api-key YOUR_API_KEY \ --application path/to/your-app.apk \ --test-application path/to/flows/ ``` * iOS **Command** ```bash marathon-cloud run maestro ios \ --api-key YOUR_API_KEY \ --application path/to/YourApp.app \ --test-application path/to/flows/ \ --os-version 18.4 ``` The app must be a simulator build (`Debug-iphonesimulator`). A `.app` directory, `.zip`, or `.ipa` is accepted. Point the `--test-application` parameter to the directory containing your YAML flows. ### Common parameters [Section titled “Common parameters”](#common-parameters) | Parameter | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--application` | Application binary: APK on Android; `.app`, `.zip`, or `.ipa` on iOS. | | `--test-application` | Directory containing Maestro YAML flows. | | `--os-version`, `--device` | Device selection, same values as `run android` / `run ios`. | | `--arch` | Android only: `amd64` (default) or `arm64`. `arm64` needs Android `9` or later. | | `--maestro-env` | Environment variable for flows (`KEY=VALUE`). Repeatable. | | `--concurrency-limit` | Cap parallel devices if your backend cannot handle many flows at once. See [limiting concurrency](/cloud/configuration/batching/#limiting-concurrency). | | `FLOW...` | Positional flow files or directories to run; everything runs when omitted. | Maestro runs always use `google_apis` Android images; `--system-image` and `--flavor` are not available. ## Best practices [Section titled “Best practices”](#best-practices) ### Self-contained flows [Section titled “Self-contained flows”](#self-contained-flows) Design each flow to be independent. Avoid relying on the state left by a previous flow, as Marathon Cloud executes flows in parallel across different devices. ### Flow duration [Section titled “Flow duration”](#flow-duration) Aim for flows that take between 40 and 80 seconds to complete. Very short flows increase the relative overhead of device provisioning, while very long flows reduce the benefits of parallelization. ### File organization [Section titled “File organization”](#file-organization) Place one flow per YAML file and keep shared steps in subflows invoked with `runFlow`. Marathon Cloud does not support `executionOrder`; if one flow depends on another, merge them or make each self-contained. ## Advanced configuration [Section titled “Advanced configuration”](#advanced-configuration) ### Environment variables [Section titled “Environment variables”](#environment-variables) Use the `--maestro-env` flag to pass variables to your flows. These can be accessed in your YAML files using the `${VARIABLE_NAME}` syntax. ```bash marathon-cloud run maestro android \ --application app.apk \ --test-application flows/ \ --maestro-env "USER_EMAIL=test@example.com" \ --maestro-env "API_URL=https://staging.api.com" ``` ### Selecting flows [Section titled “Selecting flows”](#selecting-flows) Pass flow files or directories as positional arguments after the flags to run a subset. Paths are relative to the working directory, inside `--test-application`. ```bash marathon-cloud run maestro android \ --application app.apk \ --test-application flows/ \ flows/login.yaml flows/onboarding/ ``` ### Filtering by tag or path [Section titled “Filtering by tag or path”](#filtering-by-tag-or-path) `--filter-file` works on Maestro runs. Each flow file becomes a test: the directory is the package, the class is always `MaestroTest`, the method is the file name without extension, and the flow’s `tags:` become annotations. | Flow file | `package` | `method` | `fully-qualified-test-name` | `annotation` | | ------------------------------ | --------------- | -------- | ------------------------------- | ------------ | | `flows/login.yaml` | (empty) | `login` | `MaestroTest#login` | its `tags:` | | `flows/auth/login.yaml` | `auth` | `login` | `auth.MaestroTest#login` | its `tags:` | | `flows/checkout/cart/add.yaml` | `checkout.cart` | `add` | `checkout.cart.MaestroTest#add` | its `tags:` | Paths are relative to `--test-application`. Flows in subdirectories must be included by `config.yaml` (see [workspace layout](#workspace-layout-and-configyaml)) or they are never parsed, regardless of the filter file. Because every flow shares the class name, the useful filter types are `annotation`, `package`, `method`, and `fully-qualified-test-name`. `simple-class-name` and `fully-qualified-class-name` select whole directories at best. Run only flows tagged `smoke`, excluding the `experimental` directory: flows/config.yaml ```yaml flows: - "**" ``` flows/login.yaml ```yaml appId: com.example.app tags: - smoke --- - launchApp ``` smoke.yaml ```yaml filteringConfiguration: allowlist: - type: "annotation" values: ["smoke"] blocklist: - type: "package" values: ["experimental"] ``` ```bash marathon-cloud run maestro android \ --application app.apk \ --test-application flows/ \ --filter-file smoke.yaml ``` See [filtering](/cloud/configuration/filtering/) for the full schema. For a one-off subset, positional arguments are simpler. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Path syntax [Section titled “Path syntax”](#path-syntax) Always use Linux-style forward slashes (`/`) for file paths in your Maestro flows, even if you are developing on Windows. Marathon Cloud executes tests in a Linux-based environment. ### App ID mismatch [Section titled “App ID mismatch”](#app-id-mismatch) Ensure the `appId` defined in your YAML flows matches the package name (Android) or bundle identifier (iOS) of the application you upload. Mismatching IDs will prevent Maestro from launching the application. # Migrate from BrowserStack > Move mobile instrumentation and XCUITest suites from BrowserStack App Automate to Marathon Cloud. Marathon Cloud replaces BrowserStack App Automate for Espresso, XCUITest, and Maestro suites with a single CLI, automatic parallelization, and per-test retries. ## Concept mapping [Section titled “Concept mapping”](#concept-mapping) | BrowserStack App Automate | Marathon Cloud | | --------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | Upload app via REST API, then reference `bs://` | `--application app.apk`; the CLI uploads | | Upload test suite via REST API | `--test-application tests.apk` | | `devices: ["Google Pixel 7-13.0"]` | `--os-version 13`; the platform picks the emulator profile | | `shards` / `parallelsPerPlatform` | Automatic; cap with `--concurrency-limit` if needed | | `retryOnFailure` / `numFlakyTestAttempts` | Per-test [retries](/cloud/configuration/retries/) with `--retry-quota-*` | | `class` / `package` / `annotation` filters in the build payload | `--filter-file` YAML | | `instrumentationLogs`, `video`, `deviceLogs` options | Always collected; see [artifacts](/cloud/reference/artifacts/) | | `local: true` (Local Testing tunnel) | No equivalent; backends must be reachable from the internet | | Build status via REST API polling | `--wait` (default) blocks and exits `0` / `1`; `--result-file` writes a JSON summary | | Report download via REST API | `--output ./results` or `marathon-cloud download` | ## Key differences [Section titled “Key differences”](#key-differences) ### Virtual, not physical [Section titled “Virtual, not physical”](#virtual-not-physical) BrowserStack runs on real devices. Marathon Cloud runs Android on emulators and iOS on ARM64 simulators. Review tests that depend on OEM-specific behaviour or physical hardware before migrating. ### Parallelism is decided for you [Section titled “Parallelism is decided for you”](#parallelism-is-decided-for-you) BrowserStack requires you to size `parallelsPerPlatform` against your plan. Marathon Cloud sizes the pool from the suite’s historical duration, targeting a 15-minute run. If your shared backend cannot absorb that load, set `--concurrency-limit`; see [limiting concurrency](/cloud/configuration/batching/#limiting-concurrency). ### Pricing [Section titled “Pricing”](#pricing) BrowserStack bills per parallel slot per month. Marathon Cloud bills device time per second at $2/hour (Android) and $3/hour (iOS). See [pricing](/cloud/account/pricing/) for how time is counted. ### No tunnel [Section titled “No tunnel”](#no-tunnel) There is no equivalent of BrowserStack Local. Tests must reach staging or test backends over the public internet. ## Migration steps [Section titled “Migration steps”](#migration-steps) 1. Install the [CLI](/cloud/cli/installation/) and create an [API key](/cloud/getting-started/api-key/). 2. Run your existing binaries once from a terminal to confirm they execute on Marathon Cloud before touching CI: ```bash marathon-cloud run android \ --api-key $MARATHON_CLOUD_API_KEY \ --application app-debug.apk \ --test-application app-debug-androidTest.apk \ --output marathon ``` 3. If tests used BrowserStack Local to reach a private backend, move them to an internet-reachable staging environment first. This is the step most likely to need work. 4. Translate BrowserStack filters into a [filter file](/cloud/configuration/filtering/). 5. Add the Marathon Cloud step to CI next to the BrowserStack step with `--ignore-test-failures` while you compare results, using the [GitHub Action, Bitrise step, or Docker image](/cloud/ci-cd/overview/). 6. Point your JUnit publisher at `/tests/omni/marathon_junit_report.xml`. 7. Once results match, drop `--ignore-test-failures` and remove the BrowserStack step. # Migrate from Firebase Test Lab > Map gcloud firebase test commands and GitHub Actions workflows to Marathon Cloud. Marathon Cloud replaces Firebase Test Lab (FTL) for Android instrumentation and iOS XCTest suites with a single CLI, automatic sharding, and retries that re-run individual tests instead of the whole suite. ## Command mapping [Section titled “Command mapping”](#command-mapping) | Firebase Test Lab | Marathon Cloud | | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `gcloud firebase test android run` | `marathon-cloud run android` | | `gcloud firebase test ios run` | `marathon-cloud run ios` | | `--app app.apk` | `--application app.apk` | | `--test test.apk` | `--test-application test.apk` | | `--device model=Pixel2,version=28` | `--os-version 9 --system-image google_apis` (API 28 is Android 9; versions below 10 need `google_apis`; the emulator profile is chosen by the platform) | | `--device model=...,version=...` for Wear / TV | `--device watch` / `--device tv` | | `--environment-variables KEY=VALUE` | `--instrumentation-arg KEY=VALUE` | | `--test-targets "class com.example.LoginTest"` | `--filter-file` with a `fully-qualified-class-name` filter | | `--num-flaky-test-attempts 3` | `--retry-quota-test-reactive 3` (or leave the platform default) | | `--num-uniform-shards N` | Not needed; sharding is automatic | | `--timeout 30m` | Not needed; per-test timeouts are set with `--test-timeout-default` on iOS | | `--results-bucket BUCKET` | `--output ./results`, or `marathon-cloud download --id RUN_ID` | | `--directories-to-pull /sdcard/screenshots` | `--pull-files EXTERNAL_STORAGE:/sdcard/screenshots` | | `--use-orchestrator` | No direct equivalent. Tests run in batches; `--isolated` makes each batch a single test. See [batching](/cloud/configuration/batching/) | ## Concept mapping [Section titled “Concept mapping”](#concept-mapping) ### Device selection [Section titled “Device selection”](#device-selection) FTL asks for a specific model and API level. Marathon Cloud asks for an OS version and optionally a form factor; the platform selects an emulator profile for it. Run `marathon-cloud devices android` to see the catalog. ### Sharding [Section titled “Sharding”](#sharding) FTL shards only when you configure `--num-uniform-shards` or `--test-targets-for-shard`. Marathon Cloud shards every run automatically, sizing the device pool from your suite’s historical duration to target a 15-minute wall clock. See [batching](/cloud/configuration/batching/). ### Flaky tests [Section titled “Flaky tests”](#flaky-tests) FTL’s `--num-flaky-test-attempts` re-runs the suite. Marathon Cloud retries only the failing test, on a different device, and reports tests that pass on retry as flaky rather than failed. See [retries](/cloud/configuration/retries/). ### Results [Section titled “Results”](#results) Instead of a Cloud Storage bucket, results are attached to the run in the console and downloadable with `--output` or `marathon-cloud download`. JUnit XML, Allure, per-test video, and logs are included. See [artifacts](/cloud/reference/artifacts/). ## GitHub Actions migration [Section titled “GitHub Actions migration”](#github-actions-migration) * Firebase Test Lab ```yaml - uses: google-github-actions/auth@v2 with: credentials_json: ${{ secrets.GCP_SA_KEY }} - name: Test with FTL run: | gcloud firebase test android run \ --type instrumentation \ --app app-debug.apk \ --test app-debug-androidTest.apk \ --device model=Pixel2,version=28 \ --num-flaky-test-attempts 2 ``` * Marathon Cloud ```yaml - name: Test with Marathon Cloud uses: MarathonLabs/action-test@1.0.21 with: version: 1.0.68 apiKey: ${{ secrets.MARATHON_CLOUD_API_KEY }} platform: Android application: app-debug.apk testApplication: app-debug-androidTest.apk osVersion: 9 systemImage: google_apis output: marathon ``` No service account or project setup is needed; an [API key](/cloud/getting-started/api-key/) is the only credential. ## First migrated pull request [Section titled “First migrated pull request”](#first-migrated-pull-request) 1. Add `MARATHON_CLOUD_API_KEY` to your CI secrets. 2. Keep the FTL step in place and add the Marathon Cloud step next to it with `ignoreTestFailures: true`, so the first runs do not block merges while you compare results. 3. Set `output` and point your JUnit publisher at `/tests/omni/marathon_junit_report.xml`. 4. Check that anything the tests call is reachable from the internet; FTL and Marathon Cloud both run outside your network, so this is usually already true. 5. Once results match, drop `ignoreTestFailures` and remove the FTL step. ## Platform differences [Section titled “Platform differences”](#platform-differences) ### iOS execution [Section titled “iOS execution”](#ios-execution) FTL runs iOS tests on a limited pool of physical devices. Marathon Cloud runs iOS on ARM64 simulators, which provision on demand. If your suite depends on physical-device-only behaviour (camera hardware, real push notifications, device-specific performance), verify it on a simulator before migrating. ### Android devices [Section titled “Android devices”](#android-devices) Marathon Cloud provisions Android emulators, not physical devices. Tests that rely on OEM-specific behaviour should be reviewed. ### Concurrency [Section titled “Concurrency”](#concurrency) FTL applies per-project device quotas. Marathon Cloud scales the pool to the suite. If your backend cannot take that load, cap it with `--concurrency-limit`; see [limiting concurrency](/cloud/configuration/batching/#limiting-concurrency). ## What you gain [Section titled “What you gain”](#what-you-gain) * **Per-test retries** instead of whole-suite reruns, with flaky tests labelled in the report. * **Automatic sharding** targeting a 15-minute run, without shard configuration. * **One credential and one binary** for Android, iOS, and Maestro. * **Second-level billing** of device time; see [pricing](/cloud/account/pricing/). # Test artifacts and downloads Marathon Cloud stores all test results, logs, and recordings generated during a run. ## Retention [Section titled “Retention”](#retention) Marathon Cloud applies the following retention policies: * **Application binaries**: Stored for 7 days. * **Test results and artifacts**: Stored for 30 days. Data is permanently deleted after these periods. ## Downloading artifacts [Section titled “Downloading artifacts”](#downloading-artifacts) Use the `marathon-cloud download` command to retrieve artifacts after a run completes. ```bash marathon-cloud download --id RUN_ID --output ./results ``` ### Filtering artifacts [Section titled “Filtering artifacts”](#filtering-artifacts) Use the `--glob` flag to download specific files or directories. ```bash # Download JUnit XML only for CI reporting marathon-cloud download --id $RUN_ID --output ./results --glob 'tests/**' # Download videos only for debugging failures marathon-cloud download --id $RUN_ID --output ./results --glob 'video/**' # Download Allure report only marathon-cloud download --id $RUN_ID --output ./results --glob 'report/**' ``` ### Wait behavior [Section titled “Wait behavior”](#wait-behavior) By default, the `download` command blocks until the run completes (`--wait true`). To download artifacts from a previously completed run without waiting, pass the run ID and set `--wait false`. ## Directory structure [Section titled “Directory structure”](#directory-structure) The downloaded artifacts are organized into the following structure: ```text / ├── tests/omni/ │ └── marathon_junit_report.xml # JUnit XML report ├── video/omni/ │ └── / │ └── #-.mp4 # Per-test screen recording ├── logs/omni/ │ └── / │ └── #-.log # Per-test device logs ├── report/ │ ├── index.html # Allure report │ └── allure-results/ # Raw Allure data ├── html/ │ ├── index.html # Marathon HTML report │ ├── pools/ │ └── timeline/ ├── devices/omni/ │ └── .json # Per-device configuration ├── bill/omni/ │ └── .json # Per-device billing ├── device-files.tar # Files pulled from devices (--pull-files) ├── parsed_tests.yml # Discovered test list with metadata └── parsed_tests_names.yml # Test names only ``` # Marathon Cloud vs OSS Marathon Marathon Cloud is a managed test execution platform built on the core technology of the open-source [Marathon test runner](https://marathonlabs.github.io/marathon/). This page is for teams that run OSS Marathon today and want to know what changes. Note Marathon Cloud does not use the OSS `Marathonfile`. Pass all configuration via CLI flags. The only shared format is the test filter file (`--filter-file`). ## Configuration differences [Section titled “Configuration differences”](#configuration-differences) OSS Marathon uses a `Marathonfile` (YAML) for configuration. Marathon Cloud uses CLI flags for all execution parameters. A run is fully described by its command line, so it can be triggered from any CI environment without a separate configuration file. The test filter file used with the `--filter-file` flag follows the same schema as the OSS Marathon `filteringConfiguration`. You can reuse existing filter files between OSS and Cloud. ## Feature comparison [Section titled “Feature comparison”](#feature-comparison) ### Infrastructure and management [Section titled “Infrastructure and management”](#infrastructure-and-management) | Feature | OSS Marathon | Marathon Cloud | | ------------------- | ------------------------------------ | ----------------------------------------- | | Infrastructure | Self-hosted (Local, SSH, Kubernetes) | Managed (Global device fleet) | | Device provisioning | Manual | Automatic | | Scaling | Manual | Unlimited auto-scaling | | Artifact hosting | Manual | Included (30-day retention) | | Live dashboard | No | Yes | | Run-time target | None | 15 minutes; pool sized from suite history | ### Execution logic [Section titled “Execution logic”](#execution-logic) | Feature | OSS Marathon | Marathon Cloud | | -------- | ------------- | ---------------------------------------------------------------------------------------------------------------------- | | Batching | Configurable | Chosen from suite history; `--isolated` forces batch size 1 | | Sharding | Manual config | Automatic; cap with `--concurrency-limit` | | Retries | Manual config | Reactive, preventive, and uncompleted quotas with platform defaults; override with `--retry-quota-*` or `--no-retries` | | Sorting | Manual config | Optimized by historical data | | Pooling | Custom rules | Global fleet | ## Mapping OSS to Cloud [Section titled “Mapping OSS to Cloud”](#mapping-oss-to-cloud) Use this table to find the Marathon Cloud equivalent for OSS `Marathonfile` keys. | OSS Marathonfile key | Marathon Cloud CLI flag | | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | [`filteringConfiguration`](https://marathonlabs.github.io/marathon/configuration/filtering/) | `--filter-file` (same schema; `allure`, `fragmentation`, and `annotationData` filters are not supported) | | [`sortingStrategy`](https://marathonlabs.github.io/marathon/configuration/sorting/) | Handled automatically | | [`batchingStrategy`](https://marathonlabs.github.io/marathon/configuration/batching/) | Handled automatically; `--isolated` for batch size 1 | | [`shardingStrategy`](https://marathonlabs.github.io/marathon/configuration/sharding/) | Handled automatically; `--concurrency-limit` caps the pool | | [`retryStrategy`](https://marathonlabs.github.io/marathon/configuration/retries/) | `--retry-quota-test-uncompleted`, `--no-retries` | | [`flakyTestStrategy`](https://marathonlabs.github.io/marathon/configuration/flakiness/) | `--retry-quota-test-preventive`, `--retry-quota-test-reactive` | | [`poolingStrategy`](https://marathonlabs.github.io/marathon/configuration/pooling/) | Handled automatically | | `testParserConfiguration` / `instrumentationArgs` | `--instrumentation-arg` | | `outputDir` | `--output`, or `marathon-cloud download` | | `screenRecordingPolicy` | Always on; per-test video is collected | ## OSS-only features [Section titled “OSS-only features”](#oss-only-features) The following features are specific to the self-hosted OSS version and are not available in Marathon Cloud: * `ALL_SUCCESS` execution mode * Custom device pooling rules * Count-based sharding * Random-order sorting * SSH remote workers * ADB server multiplicity * File push before batch * Custom screen recorder configuration * Local test parser # Security and data Marathon Cloud is designed with security and data isolation as core principles. ## Data retention [Section titled “Data retention”](#data-retention) Marathon Cloud stores data only for the duration required for test execution and result retrieval: * **Application binaries**: Stored for 7 days. * **Test results and artifacts**: Stored for 30 days. After these retention periods, all data is permanently deleted from our systems. ## Network and authentication [Section titled “Network and authentication”](#network-and-authentication) ### API authentication [Section titled “API authentication”](#api-authentication) All interactions with the Marathon Cloud API require an API key. Pass the key using the `--api-key` flag or the `MARATHON_CLOUD_API_KEY` environment variable. API keys are scoped to your organization. ### Encryption [Section titled “Encryption”](#encryption) The CLI communicates with the Marathon Cloud API (`cloud.marathonlabs.io`) exclusively over HTTPS. All data in transit is encrypted. ### IP allowlisting [Section titled “IP allowlisting”](#ip-allowlisting) Organizations with strict security requirements can configure IP allowlisting to restrict API access to specific network ranges. Contact support to enable this feature. ## Device isolation [Section titled “Device isolation”](#device-isolation) Marathon Cloud ensures complete isolation between test runs: * **Fresh provisioning**: Every test run uses freshly provisioned virtual devices. * **No persistence**: Devices are destroyed immediately after the run completes. * **Zero state leakage**: No data or state persists between runs from different users or organizations. ## Compliance and SSO [Section titled “Compliance and SSO”](#compliance-and-sso) ### Certifications [Section titled “Certifications”](#certifications) Marathon Cloud does not hold SOC2 or similar compliance certifications at this time. If your organization requires specific certifications, contact . ### Single Sign-On (SSO) [Section titled “Single Sign-On (SSO)”](#single-sign-on-sso) SAML-based SSO is available for enterprise organizations. Refer to the [SSO setup guide](/cloud/account/sso/) for configuration details. # Supported devices and OS versions > Android emulator and iOS simulator configurations available on Marathon Cloud. Marathon Cloud runs Android tests on emulators and iOS tests on ARM64 simulators. Physical devices are not offered. ## Android [Section titled “Android”](#android) Note Device profiles change as new ones are added. Run `marathon-cloud devices android --api-key $MARATHON_CLOUD_API_KEY` for the current catalog and the device type IDs accepted by `--device`. ### OS versions [Section titled “OS versions”](#os-versions) Pass the Android version to `--os-version`: | `--os-version` | API level | | -------------- | --------- | | `17` | 37 | | `16` | 36 | | `15` | 35 | | `14` | 34 | | `13` | 33 | | `12` | 31 | | `11` | 30 | | `10` | 29 | | `9` | 28 | | `8.1` | 27 | | `8` | 26 | ### System images [Section titled “System images”](#system-images) | `--system-image` | Description | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `default` | AOSP image without Google services. Android `10` to `14`, `arm64` only. | | `google_apis` | Google APIs (Play Services) included. Available on every version; required for Android `8` to `9`, Wear OS, TV, and Maestro. | | `google_apis_playstore` | Google APIs plus the Play Store. Android `10` and later. | Valid combinations by version: | `--os-version` | Allowed `--system-image` | | ---------------- | ------------------------------------------------- | | `8`, `8.1`, `9` | `google_apis` | | `10` to `14` | `default`, `google_apis`, `google_apis_playstore` | | `15`, `16`, `17` | `google_apis`, `google_apis_playstore` | The CLI rejects other combinations before uploading, for example `--os-version 15 --system-image default`. ### Architecture [Section titled “Architecture”](#architecture) Emulators run on `amd64` (x86\_64) unless you pass `--arch arm64`. Use `arm64` when the app bundles native libraries without an `x86_64` ABI, or to test on the same architecture as production devices. | `--arch` | Requirements | | -------- | ----------------------------------------------------------------------- | | `amd64` | `google_apis` or `google_apis_playstore`. Any Android version. Default. | | `arm64` | Android `9` or later. Every system image, including `default`. | `default` images exist only for `arm64`, so `--system-image default --arch amd64` is rejected. `arm64` is available on `run android` and `run maestro android` from CLI 1.0.65; iOS simulators are always `arm64`. ### Form factors [Section titled “Form factors”](#form-factors) | `--device` | OS versions | System image | | ----------------------------------------- | ---------------------------- | ------------- | | phone (default; any phone device type ID) | All | Any | | `watch` | `11`, `13`, `15` | `google_apis` | | `tv` | `10`, `11`, `12`, `13`, `14` | `google_apis` | The CLI rejects combinations outside this table before uploading. ## iOS [Section titled “iOS”](#ios) The simulator list is fixed. Xcode is chosen by the platform from the iOS version; `--xcode-version` is deprecated and ignored. ### Devices by iOS version [Section titled “Devices by iOS version”](#devices-by-ios-version) | `--os-version` | `--device` | | -------------- | -------------------------------------------------------------------------------- | | `18.2`, `18.4` | `iPhone-11`, `iPhone-16`, `iPhone-16-Plus`, `iPhone-16-Pro`, `iPhone-16-Pro-Max` | | `26.1` | `iPhone-16-Pro`, `iPhone-17`, `iPhone-17-Pro`, `iPhone-17-Pro-Max` | If you pass only `--device`, the iOS version is inferred when unambiguous. `iPhone-16-Pro` is available on both 18.x and 26.1, so pass `--os-version` with it. # Changelog > Recent updates and improvements to Emu. ## Preview [Section titled “Preview”](#preview) Nothing new on the canary channel ## 2026-09-03 (Build 442) [Section titled “2026-09-03 (Build 442)”](#2026-09-03-build-442) ### Features [Section titled “Features”](#features) * New AI tools: open URLs and deep links, uninstall apps, read and change screen orientation, and set per-app locale (Android 13+) ### Fixes [Section titled “Fixes”](#fixes) * Fixed popout mirror window oscillating position and size when the main window is in the background * Fixed emulator version store reliability — stale temp directories are cleaned up, partial failures are recovered ### Other [Section titled “Other”](#other) * Lower-latency mirror frames ## 2026-08-26 (Build 437) [Section titled “2026-08-26 (Build 437)”](#2026-08-26-build-437) ### Features [Section titled “Features”](#features-1) * Emulator startup diagnostics — clear error messages with actionable suggestions when an emulator fails to start * Shared-memory mirroring on Windows for emulators ### Fixes [Section titled “Fixes”](#fixes-1) * Fixed memory leak on macOS when the window is on a background desktop ## 2026-08-21 (Build 416) [Section titled “2026-08-21 (Build 416)”](#2026-08-21-build-416) ### Features [Section titled “Features”](#features-2) * Anonymous usage analytics with PostHog (conversion funnel tracking) * Privacy settings card with telemetry opt-out toggle and installation UUID display * First-run notice informing users about anonymous data collection * Debug mode for inspecting telemetry payloads (`EMU_TELEMETRY_DEBUG=1`) ### Other [Section titled “Other”](#other-1) * [Privacy policy](https://emu.marathonlabs.io/privacy) published * [Telemetry reference](/emu/reference/telemetry/) documenting all collected events * Replaced Umami with PostHog for analytics ## 2026-08-20 (Build 410) [Section titled “2026-08-20 (Build 410)”](#2026-08-20-build-410) ### Features [Section titled “Features”](#features-3) * Connect and mirror multiple devices at the same time * Each device gets its own mirror, recording, and logcat * Faster screen mirroring on macOS and Linux using the native GPU pipeline (Direct+, requires custom emulator) * Foldable device support with hinge position indicator * Edit emulator feature flags from the settings panel * Set custom display refresh rates for emulators * Install emulator versions from local ZIP files * Copy request and response bodies to clipboard in the network inspector * Clipboard sync between host and emulator via companion service * Auto-detect Android SDK when environment variables are not set * FPS indicator shows idle state and recovers instantly on activity * Animated switch component ### Fixes [Section titled “Fixes”](#fixes-2) * Desktop keyboard and mouse capture improvements * Better stability when switching between multiple devices * Recording and Android Auto now handle device switching correctly * Improved foldable posture tracking during boot * Daemon shuts down cleanly * Popout mirror window preserves position and size across macOS desktop switches * Fixed clipboard sync on real (non-emulator) devices * Fixed crashes on device disconnect and VRAM leak * Windows stability: eliminated console window flashes, fixed D3D11 crashes, fixed Media Foundation encoder cleanup * Step snapping on numeric inputs prevents glitchy values ### Other [Section titled “Other”](#other-2) * Video frames upload directly to GPU — smoother rendering, lower CPU use * Smoother text rendering with subpixel kerning ## 2026-07-17 (Build 303) [Section titled “2026-07-17 (Build 303)”](#2026-07-17-build-303) ### Features [Section titled “Features”](#features-4) * Android Auto projection — see your car head unit right on your desktop * Media playback and navigation status shown in the sidebar * Mouse input mode for desktop Android emulators * Right-click, middle-click, and scroll wheel support * Higher-quality screen recording with H.264 passthrough * Battery saver toggle in emulator controls * Copy logcat rows to clipboard * AI tools for Android Auto control ### Fixes [Section titled “Fixes”](#fixes-3) * Fixed input mode detection and state reset when switching devices * Trackpad now uses inotify instead of per-frame polling * Correct color range for physical phone scrcpy streams ## 2026-07-04 (Build 284) [Section titled “2026-07-04 (Build 284)”](#2026-07-04-build-284) ### Features [Section titled “Features”](#features-5) * Wear OS mirror and ambient mode toggle * Clone an emulator with copy-on-write — fast and saves disk space ### Fixes [Section titled “Fixes”](#fixes-4) * x86 system images now show in AVD creation on x86\_64 hosts * Logcat stays connected across mirror reconnects * FPS counter now uses a rolling average instead of jumping between values ## 2026-06-13 (Build 246) [Section titled “2026-06-13 (Build 246)”](#2026-06-13-build-246) ### Features [Section titled “Features”](#features-6) * Wireless ADB pairing — scan a QR code to connect your phone * Type non-Latin characters (CJK, emoji, etc.) on any device * Change locale without rebooting * Operations queue — see downloads and installs in progress * Port forwarding setup dialog * Boot status tracking from emulator logs * Linux multitouch passthrough for laptop trackpads ### Fixes [Section titled “Fixes”](#fixes-5) * Fixed mDNS device detection for wirelessly paired devices * Stale frames no longer flash when switching devices * Mirror switch delay reduced from \~18s to near-instant ### Other [Section titled “Other”](#other-3) * SQLite viewer improvements — table pills, pagination, WAL databases ## 2026-06-06 (Build 230) [Section titled “2026-06-06 (Build 230)”](#2026-06-06-build-230) ### Features [Section titled “Features”](#features-7) * macOS app bundle with DMG installer * Linux multitouch via evdev ### Fixes [Section titled “Fixes”](#fixes-6) * Fixed rotation display and input for physical devices ## 2026-05-31 (Build 214) [Section titled “2026-05-31 (Build 214)”](#2026-05-31-build-214) ### Features [Section titled “Features”](#features-8) * GPU-accelerated screen mirroring with hardware video decode * Touch and keyboard input forwarding * Emulator controls: GPS, battery, sensors, network throttling * Device management with emulator start and stop * AI integration — connect coding assistants to control your devices via MCP * 30+ tools for tapping, typing, inspecting, taking screenshots, and more * Command palette (Ctrl+K) for quick access to any action * Design comparison page — overlay design exports on the device screen * 5 compare modes: overlay, diff, split, slide, toggle * Pixel ruler, grid overlay, color picker with swatch history * Bug replay — screen recording, touch actions, and logs captured automatically * Export bugs as a single .emubug file anyone can open * Accessibility audit with 15 detectors and 46 checks * SDK setup wizard — installs everything you need on first launch * Float the device mirror on top of your IDE * Multi-touch input with Ctrl+click * Clipboard sync, screen recording toggle * Locale, timezone, and demo mode controls * Drag-and-drop APK install with split APK support * Persistent logcat — logs saved to disk across restarts * Network proxy with automatic HTTPS interception * Export network captures as HAR files * Windows support * Licensing and free trial * Webcam passthrough for emulator camera * Theme follows your OS dark/light preference ### Other [Section titled “Other”](#other-4) * Visual snapshot testing in CI * Platform-aware mirroring backend selection # Accessibility audit > Find accessibility issues in your app before your users do. ## Quick start [Section titled “Quick start”](#quick-start) 1. Open the **Accessibility** tab in the workspace. 2. Click **Run Audit**. 3. Review the report for issues and recommendations. ![Accessibility audit results with highlighted issues](/_astro/a11y.BfjIileG_Z1MHWAd.avif) ## What’s checked [Section titled “What’s checked”](#whats-checked) Emu runs a set of detectors against the accessibility tree and the device screen. Examples of what gets flagged: * **Text contrast**: Pixel-level analysis of foreground/background contrast ratios, tested against WCAG A, AA, and AAA thresholds. * **Touch targets**: Elements that are too small to tap reliably. * **Missing content descriptions**: Images and interactive elements without labels for screen readers. * **Redundant descriptions**: Labels that duplicate the visible text and add noise for assistive technology users. * **Clickable but not focusable**: Interactive elements unreachable via keyboard or switch access. * **Duplicate speakable text**: Multiple elements in the same region announcing the same text. * **Spacing**: Interactive elements placed too close together, increasing the risk of accidental taps. * **Nested focus**: Focusable elements inside other focusable elements, which confuses navigation order. ## Bidirectional highlighting [Section titled “Bidirectional highlighting”](#bidirectional-highlighting) * **From report**: Click an issue in the audit report to highlight the corresponding element on the device. * **From device**: Click an element on the device to jump to the relevant issue in the report. ## Live inspection [Section titled “Live inspection”](#live-inspection) Emu maintains a persistent on-device accessibility server for real-time element inspection. As you navigate your app, accessibility properties of the focused element update in the inspector panel. The mirror in the Accessibility tab is not interactive — it shows the overlay but does not forward touch input. Use the **Pop Out** button to open a floating mirror window alongside the audit view. Interact with your app in the popout while watching the accessibility properties update in real time. ## Inspection tiers [Section titled “Inspection tiers”](#inspection-tiers) Emu supports three tiers for retrieving the accessibility tree, selectable in **Settings > Accessibility**. * **Tier 1 (app\_process)**: Runs an accessibility service directly on the device via `app_process`. No additional installation required. * **Tier 2 (companion)**: Uses a companion app on the device for deeper inspection of custom views and complex layouts. * **Tier 3 (shell)**: Fallback that retrieves the accessibility tree via ADB shell commands. Less detailed but works on any device. Emu defaults to auto-selecting the best available tier for the connected device. # Android Auto > Project your car's head unit display onto your desktop. ## Quick start [Section titled “Quick start”](#quick-start) 1. Connect the device in Emu. 2. Open the **Android Auto** section in the inspector panel and click **Connect**. 3. The head unit display appears in the workspace. ![Android Auto head unit projected in Emu](/_astro/android-auto.DhmRB7Do_24CCvw.avif) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) Android Auto projection requires some one-time setup on the emulator. 1. ### Install Android Auto [Section titled “Install Android Auto”](#install-android-auto) The stub that ships with most emulator system images does not support head unit projection. You need to sideload a full Android Auto APK sourced externally — it is not available on the Play Store for emulators. 2. ### Sign in with a Google account [Section titled “Sign in with a Google account”](#sign-in-with-a-google-account) Android Auto requires an active Google account on the device. Open **Settings > Accounts** and sign in if you haven’t already. 3. ### Accept permissions [Section titled “Accept permissions”](#accept-permissions) On first connection, Android Auto will prompt you to grant permissions and accept terms. Follow the on-screen prompts on the device. Tip These steps only need to be done once per device. After initial setup, Emu connects on subsequent launches without repeating them. ## Media and navigation [Section titled “Media and navigation”](#media-and-navigation) Emu surfaces Android Auto metadata in the inspector panel. * **Media playback**: Track title, artist, and playback status. * **Navigation**: Turn-by-turn navigation status when active. ## Vehicle controls [Section titled “Vehicle controls”](#vehicle-controls) Emu exposes Android Auto protocol controls in the inspector panel. * **Day/Night mode**: Toggle between day and night display modes to test how your app adapts. * **Driving status**: Switch between Parked, Idle, and Moving to test how your app responds to driving restrictions. ## MCP tools [Section titled “MCP tools”](#mcp-tools) AI agents can interact with the Android Auto head unit using dedicated MCP tools (`aa_tap`, `aa_swipe`, `aa_screenshot`, `aa_start_recording`/`aa_stop_recording`, etc.). See the [tool reference](/emu/mcp/tools/) for the full list. Caution The Android Auto display is rendered via its own projection protocol, not through the standard Android view hierarchy. The accessibility tree (`ui_dump`) and element-based tools (`tap_element`, `fill_element`) do not work on the head unit. Agents need to use `aa_screenshot` with computer vision to understand the screen content, and `aa_tap`/`aa_swipe` with pixel coordinates to interact. # Bug replay > Capture screen recordings, inputs, and logs into a shareable bug report. ## Quick start [Section titled “Quick start”](#quick-start) 1. Enable the **replay buffer** recording mode in the toolbar (the default mode is quality recording, which captures continuous video instead of a rolling buffer). 2. Reproduce the bug on the connected device. 3. Click the **Capture Bug** button in the toolbar. ![Bug replay detail view with video timeline](/_astro/bugs.CTJqNluB_Z1KHeeJ.avif) Emu saves the captured artifacts (video, inputs, logs) into a local bug report. You can then export it as a single `.emubug` file from the bug detail view. Tip The replay buffer continuously records the last N seconds of activity in memory. When you capture a bug, Emu saves what’s already in the buffer — no need to have started recording beforehand. ## What’s captured [Section titled “What’s captured”](#whats-captured) * **Screen recording**: Video of the device display from the replay buffer. * **Touch actions**: A timeline of user interactions with accessibility labels. * **Logcat**: System logs for the duration of the capture. * **Crash stack traces**: Included if the app crashed during the recording. ## Configurable buffer [Section titled “Configurable buffer”](#configurable-buffer) Adjust the replay buffer settings in **Settings**. * **Duration**: 15 seconds to 5 minutes. * **Resolution**: 480p, 720p, or 1080p. Lower resolutions use less memory and produce smaller exports. ## Export and sharing [Section titled “Export and sharing”](#export-and-sharing) From the bug detail view, export the report as a single `.emubug` file. Share it with a teammate — when they open it in Emu, they see the video, touch timeline, and logs without needing to reproduce the bug. ## Video playback [Section titled “Video playback”](#video-playback) Open a bug report in Emu to scrub through the recording. The interaction timeline is synced to the video so you can see which touch action corresponds to each frame. # Design comparison > Overlay design exports on the device screen to spot pixel-level deviations. ## Quick start [Section titled “Quick start”](#quick-start) 1. Open the **Design** tab in the workspace. 2. Drag a PNG export from Figma, Sketch, or your file manager onto the tab. 3. The design overlay appears on the device mirror. ![Design comparison overlay mode](/_astro/design.B5CmeV8v_Z1hzjs9.avif) ## 5 compare modes [Section titled “5 compare modes”](#5-compare-modes) Choose the best way to visualize differences between the design and the live app. * **Overlay**: Render the design with adjustable transparency over the live screen. * **Diff**: Highlight only the pixels that differ between design and implementation. * **Side by side**: Show the design and live app next to each other with a vertical divider. * **Slider**: Drag a divider across the screen to reveal the design underneath. * **Hover**: Switch between design and live app on mouse hover to spot differences. ## Measurement tools [Section titled “Measurement tools”](#measurement-tools) Use built-in tools to quantify deviations. * **Pixel ruler**: Measure distances between elements in pixels. * **Dimension lines**: Draw lines to check alignment and spacing against the design. ## Grid overlay [Section titled “Grid overlay”](#grid-overlay) Enable a configurable grid to check alignment across the entire screen. You can adjust the grid size and color in the design settings panel. ## Color picker [Section titled “Color picker”](#color-picker) Pick colors directly from the device screen to verify implementation accuracy. Emu keeps a history of picked colors in a swatch grid. # Emulator controls > Control GPS, battery, sensors, network, and foldable posture from one panel. ## Quick start [Section titled “Quick start”](#quick-start) 1. Connect an Android emulator. 2. Open the **Controls** tab in the workspace. 3. Adjust settings in the responsive grid of control cards. ![Emulator controls panel showing location and battery settings](/_astro/controls.dvAQK2sx_Z57Igj.avif) ## Location [Section titled “Location”](#location) Simulate device movement and position without leaving your desk. * **Coordinates**: Set latitude, longitude, and altitude. * **Movement**: Configure speed and bearing for navigation testing. * **Presets**: Choose from a list of built-in locations or save your current coordinates as a new preset. ## Battery [Section titled “Battery”](#battery) Test how your app behaves under different power conditions. * **Level**: Adjust the battery percentage slider. * **Status**: Toggle between charging, discharging, full, and not charging. * **Health**: Simulate battery health states like Good, Overheated, or Dead. * **Battery saver**: Toggle the system battery saver mode. Android emulators do not automatically enable battery saver when the level drops low, unlike real devices. Emu lets you trigger it manually so you can test how your app adapts to battery saver restrictions (background limits, reduced animations, network throttling). ## Network [Section titled “Network”](#network) Apply network throttling to test app performance on slow connections. * **Profiles**: Choose from standard profiles like 2G, 3G, or LTE. * **Custom**: Set specific bandwidth limits and latency delays. * **Airplane mode**: Toggle all cellular and wireless radios. ## Sensors [Section titled “Sensors”](#sensors) Manipulate 7 different hardware sensors to test motion and environment-aware features. * **Motion**: Accelerometer and Gyroscope. * **Environment**: Light, Pressure, and Temperature. * **Position**: Magnetometer and Proximity. ## Foldable [Section titled “Foldable”](#foldable) Control the physical posture of foldable devices. * **Posture**: Switch between Open, Half-open, and Closed states. * **Hinge angle**: Use the slider to set a specific hinge angle for fine-grained UI testing. ## Snapshots [Section titled “Snapshots”](#snapshots) Save and restore the exact state of your emulator. * **Save**: Capture the current disk and memory state with a custom name. * **Load**: Restore a previously saved snapshot to return to a known state instantly. * **Delete**: Remove old snapshots to free up disk space. ## Wear OS [Section titled “Wear OS”](#wear-os) When a Wear OS emulator is connected, the Controls tab shows watch-specific controls. * **Wrist tilt**: Toggle between gaze (screen on) and ungaze (ambient mode) to test always-on display behavior. * **Heart rate**: Set a simulated heart rate with a slider (40–200 bpm) or use presets — Rest, Active, Cardio, Max. * **Ambient light**: Adjust RGBC ambient light sensor values to test watch face brightness adaptation. The mirror view also renders round watch faces correctly with a circular mask. ## Advanced features [Section titled “Advanced features”](#advanced-features) Edit `advancedFeatures.ini` flags directly from the settings panel to enable experimental emulator capabilities without manually editing configuration files. # Live mirror > GPU-accelerated screen mirroring at native resolution. ## Quick start [Section titled “Quick start”](#quick-start) 1. Connect a physical device via USB or start an emulator. 2. Click on the device in the sidebar list. 3. The live mirror appears in the main workspace. ![Live mirror showing a device screen mirrored on the desktop](/_astro/mirror.BKUDMqYN_1kgtYt.avif) ## Capabilities [Section titled “Capabilities”](#capabilities) Emu provides a high-performance mirroring engine designed for professional development. * **Native resolution**: View your app exactly as it appears on the device hardware. * **Hardware decode**: Uses Vulkan Video, VideoToolbox, or VA-API for low-latency H.264 and H.265 decoding. * **Multi-device support**: Mirror and control multiple devices simultaneously. ## Floating window [Section titled “Floating window”](#floating-window) You can pop out the mirror into a dedicated floating window to keep it on top of your IDE or other tools. Click the **Pop Out** button in the mirror toolbar to detach the view. Tip On Linux tiling window managers like Hyprland, you can set window rules for the `emu-live` class to ensure the popout always floats. ## Multi-touch [Section titled “Multi-touch”](#multi-touch) These gestures work out of the box with no configuration: * **Second finger**: Hold `Ctrl` (or `Cmd`) and click to place a second touch point, mirrored around the screen center. * **Pinch zoom**: Hold `Ctrl` and scroll with your mouse wheel or trackpad. * **Trackpad pinch**: Native two-finger pinch on your trackpad maps to a pinch gesture on the device. ### Multitouch passthrough [Section titled “Multitouch passthrough”](#multitouch-passthrough) For full multi-finger control (3-finger swipe, 4-finger tap, or any arbitrary Android gesture), enable **Multitouch passthrough** in **Settings > Mirroring**. This is off by default and available on macOS and Linux. When enabled, each finger on your trackpad maps 1:1 to a touch point on the device — up to 10 simultaneous fingers. While your cursor is over the mirror panel, the trackpad is captured by Emu and system gestures (swipe between desktops, Mission Control, etc.) will not work until you move the cursor away. ## Replay buffer [Section titled “Replay buffer”](#replay-buffer) Emu supports two recording modes, selectable from the toolbar: * **Quality**: Continuous screen recording for long-form capture. Uses hardware-accelerated encoding where available. * **Replay buffer**: Keeps the last 30 seconds of activity in a rolling memory buffer. When you encounter a bug, click **Capture** to save what already happened — no need to have started recording beforehand. See [Bug replay](/emu/features/bug-replay/) for details. ## Mirror backends [Section titled “Mirror backends”](#mirror-backends) Emu automatically selects the best mirroring backend for your device. Emulators use an optimized path with lower latency, while physical devices and generic connections use the scrcpy protocol. You can manually override the backend in **Settings > Mirroring** if you encounter compatibility issues. # Logcat > Real-time log viewer with automatic PID-following and crash detection. ## Quick start [Section titled “Quick start”](#quick-start) 1. Open the **Logcat** tab in the workspace. 2. Select your app package from the dropdown menu. 3. Logs will begin streaming, automatically filtered to your process. ![Logcat viewer with filtered log entries](/_astro/logcat.BnrgCzdI_2hWb8u.avif) ## PID following [Section titled “PID following”](#pid-following) Emu tracks your application’s Process ID (PID) in real time. If your app restarts or crashes, Emu automatically detects the new PID and re-attaches the log stream without losing context or requiring manual intervention. ## Crash detection [Section titled “Crash detection”](#crash-detection) When an application crash occurs (`FATAL EXCEPTION`), Emu captures the stack trace and flags it. Crashes are also surfaced in [bug replay](/emu/features/bug-replay/) reports when capture is active. ## Filtering [Section titled “Filtering”](#filtering) Narrow down logs using powerful built-in filters. * **Priority level**: Filter by Verbose, Debug, Info, Warning, Error, or Fatal. * **Tag**: Show only logs with specific tags. * **Regex search**: Use regular expressions to find complex patterns in log messages. * **Package name**: Isolate logs from a specific application package. ## Presets [Section titled “Presets”](#presets) Save your current filter configuration as a named preset to switch contexts quickly. A preset captures the priority level, search query, regex mode, suppressed tags, and followed package — everything that defines what you’re looking at. Use presets to jump between debugging scenarios without reconfiguring filters each time. For example, keep a “Network” preset filtered to OkHttp tags and a “UI” preset following your app package at Warning level. ## Disk-backed history [Section titled “Disk-backed history”](#disk-backed-history) Logs are written to disk in compressed segments so they persist across mirror reconnects, app restarts, and Emu restarts. You do not lose log history when switching devices or closing the app. ## Row selection [Section titled “Row selection”](#row-selection) Interact with log entries using standard shortcuts. * **Select**: Click a row to select it. * **Copy**: Press `Ctrl+C` (or `Cmd+C`) to copy the selected rows to your clipboard. # Network inspector > Inspect HTTP/HTTPS traffic with a built-in proxy. ## Quick start [Section titled “Quick start”](#quick-start) 1. Open the **Network** tab in the workspace. 2. Click **Enable** to start the built-in proxy. 3. Traffic from your device begins appearing in the request list. ![Network inspector showing captured HTTP requests](/_astro/network.CEBGt2wT_1UeEms.avif) Caution The proxy requires a rooted emulator (`userdebug` or `eng` system image) to install its TLS certificate. Play Store images and non-rooted physical devices are not supported. ## How it works [Section titled “How it works”](#how-it-works) When you start the proxy, Emu injects a TLS certificate into the device’s trust store via `adb root` and routes traffic through a local MITM proxy. This allows Emu to intercept and decrypt HTTPS traffic without external tools or manual certificate setup. ## Request details [Section titled “Request details”](#request-details) Click any request in the list to inspect it. * **Headers**: Request and response headers. * **Body**: Raw or formatted request and response bodies. * **Timing**: Connection, request, and response latency. * **Payload decoding**: Protobuf and gRPC payloads are decoded automatically. ## Copy as cURL [Section titled “Copy as cURL”](#copy-as-curl) Right-click any request and select **Copy as cURL** to generate a command you can run in your terminal. ## HAR export [Section titled “HAR export”](#har-export) Export captured traffic as HTTP Archive (HAR) files for sharing or analysis in external tools. ## Network throttling [Section titled “Network throttling”](#network-throttling) Apply network profiles to simulate different connectivity conditions. Available for emulators only. * **Profiles**: 2G, 3G, LTE. * **Custom**: Define specific bandwidth and latency parameters. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### No traffic appearing [Section titled “No traffic appearing”](#no-traffic-appearing) * Verify the proxy is started (the **Enable** button in the Network tab). * The device must be a rooted emulator (`userdebug`/`eng`). Play Store system images and non-rooted physical devices cannot have the proxy certificate injected. # Installation > Install Emu on macOS, Linux, or Windows. ## Quick start [Section titled “Quick start”](#quick-start) ```bash curl -fsSL https://dl.marathonlabs.io/emu.sh | bash ``` Run the install script to download the latest beta build and add the `emu` binary to your path. ## Channels [Section titled “Channels”](#channels) Emu is distributed through three release channels. The beta channel is the default right now. The canary channel contains the latest experimental work. Once emu gets more adoption we will have first stable release. To install the canary channel, pass the `--channel` flag to the install script: ```bash curl -fsSL https://dl.marathonlabs.io/emu.sh | bash -s -- --channel=canary ``` ## Direct downloads [Section titled “Direct downloads”](#direct-downloads) Standalone binaries for macOS, Linux, and Windows are available on the [download page](https://emu.marathonlabs.io/#download). ## Platform requirements [Section titled “Platform requirements”](#platform-requirements) * macOS * **Android SDK**: Emu can install the Android SDK and ADB for you on first launch. If you already have the SDK installed, Emu may still show the setup screen — see the note below. * **Security**: If you install via DMG, you may need to allow the application in **System Settings > Privacy & Security**. Tip macOS GUI apps do not inherit environment variables from your shell profile (`~/.zshrc`, `~/.bashrc`). Even if you have `ANDROID_HOME` configured in your terminal, Emu cannot see it when launched from Finder, Spotlight, or the Dock. When the SDK setup screen appears, point it to your existing SDK directory (typically `~/Library/Android/sdk`) and Emu will remember the location. * Linux * **Android SDK**: Emu can install the Android SDK and ADB for you on first launch. If you already have ADB in your `PATH`, Emu will use it automatically. * **Dependencies**: Standard `libc`, `libstdc++`, `libgcc`, `liblzma`, and `libbz2` libraries. These come pre-installed on most distributions. * Windows * **Android SDK**: Emu can install the Android SDK and ADB for you on first launch. If you already have ADB in your `PATH`, Emu will use it automatically. * **Security**: You may need to allow the binary in Windows Defender or your antivirus software. # Quickstart > Get from install to your first device mirror in under 2 minutes. This walkthrough covers launching the app, connecting a device, and seeing your first mirror. 1. ### Launch [Section titled “Launch”](#launch) * macOS Open **Emu** from your Applications folder, Spotlight, or run `emu` from a terminal. * Linux Run `emu` from your terminal. * Windows Double-click `emu.exe`, or run `emu` from a terminal. You will see the device list showing all connected physical devices and available Android Virtual Devices (AVDs). Tip If you don’t have the Android SDK installed, Emu will walk you through a setup wizard on first launch to install the SDK, accept licenses, and download a system image. 2. ### Connect a device [Section titled “Connect a device”](#connect-a-device) Connect a physical device via USB or start an emulator. * **Physical device**: Plug in via USB or pair wirelessly — open **Wireless pairing** from the toolbar and scan the QR code shown in your device’s Developer Options. * **Emulator**: Click the play icon next to an AVD in the device list to start it. If you don’t have any AVDs yet, click **Create AVD** to set one up — pick a device profile, system image, and Emu handles the rest. 3. ### Mirror [Section titled “Mirror”](#mirror) Click on your device in the list to open the live mirror. Emu establishes a high-performance connection to show a GPU-accelerated view of the device screen at native resolution. 4. ### Interact [Section titled “Interact”](#interact) Use your mouse and keyboard to control the device. * **Touch**: Click and drag to simulate taps and swipes. * **Keyboard**: Type directly into focused text fields. * **Command palette**: Press `Ctrl+K` (or `Cmd+K` on macOS) to search for apps and actions. ## What’s next [Section titled “What’s next”](#whats-next) * [Live mirror](/emu/features/live-mirror) — Learn about multi-touch and recording. * [Network inspector](/emu/features/network-inspector) — Inspect HTTP/HTTPS traffic. * [MCP setup](/emu/mcp/setup) — Connect AI agents to your device. # MCP setup > Connect AI coding assistants to your Android devices via the MCP server. ## Quick start [Section titled “Quick start”](#quick-start) 1. Open **Settings > MCP** in the Emu GUI. 2. Click **Add to config** next to your AI tool (Cursor, Claude Code, OpenCode, etc.). 3. Restart your AI tool — it now has access to all connected Android devices. Emu’s MCP server can run in two modes: embedded inside the GUI, or as a standalone headless daemon. ## Headless daemon [Section titled “Headless daemon”](#headless-daemon) Use this when you want the MCP server running without the GUI — for example, in a CI environment or when you only need AI agent access. ```bash emu mcp ``` This starts a background daemon (if one isn’t already running) and prints the connection URL. The daemon runs independently and persists until you stop it with `emu stop`. ## GUI-embedded server [Section titled “GUI-embedded server”](#gui-embedded-server) When you run the full Emu GUI, you can start the MCP server from **Settings > MCP**. The server runs inside the GUI process — no separate daemon needed. This is the recommended mode for day-to-day development because you get both the visual interface and AI agent access at the same time. ## Auto-configuration [Section titled “Auto-configuration”](#auto-configuration) From **Settings > MCP**, Emu detects installed MCP clients and can inject the server configuration with one click. 1. Emu shows the status of each detected client — **Configured**, **Not Configured**, or **Not Installed**. 2. Click **Add to config** next to any client to write the Emu server entry into its configuration file. Supported clients: **Claude Code**, **Cursor**, **Windsurf**, **Cline**, **OpenCode**, **Zed**. Caution **Claude Desktop** does not support HTTP-based MCP servers directly. It requires the `npx mcp-remote` bridge. Emu shows the correct configuration snippet in the settings screen, but you need to paste it manually. ## Manual configuration [Section titled “Manual configuration”](#manual-configuration) If you prefer to configure manually, add the Emu server to your client’s MCP config: ```json { "mcpServers": { "emu": { "url": "http://127.0.0.1:8565/mcp" } } } ``` The exact format varies by client — Windsurf uses `serverUrl`, Cline uses `type: "streamableHttp"`, Zed uses `context_servers`. The auto-configuration handles these differences for you. ## Daemon lifecycle [Section titled “Daemon lifecycle”](#daemon-lifecycle) These commands apply to the headless daemon mode. ```bash emu mcp # Start daemon (if needed) and print URL emu mcp # Reuses existing daemon — idempotent emu stop # Graceful shutdown emu mcp --port 9000 # Custom port ``` The daemon follows the `adb` pattern: probe the port, spawn a detached process if nothing is listening, wait for readiness, then print the URL. ## Security [Section titled “Security”](#security) The MCP server binds to `127.0.0.1` (localhost) only. DNS rebinding protection rejects requests with non-localhost `Host` headers. No bearer token is required — same trust model as `adb`, the Docker socket, and Chrome DevTools. # Tool reference > Complete list of MCP tools in the Emu server. Tip Tools tagged with **\[Emulator only]** require a running Android Virtual Device (AVD) and will not work on physical devices. ## Device [Section titled “Device”](#device) | Tool | Description | | -------------- | -------------------------------------------------- | | `list_devices` | Serial, state, transport (Emulator/USB/Wi-Fi) | | `device_info` | Model, API level, ABI, battery, RAM, storage | | `shell` | Run an ADB shell command with configurable timeout | ## Input [Section titled “Input”](#input) | Tool | Description | | --------------- | ---------------------------------------------- | | `tap` | Tap at pixel coordinates | | `swipe` | Swipe between two points | | `long_press` | Long press at coordinates | | `double_tap` | Double tap at coordinates | | `drag` | Drag from one point to another | | `pinch` | Pinch gesture (zoom in/out) | | `input_text` | Type text into the focused field | | `press_key` | Key event (KEYCODE\_HOME, KEYCODE\_BACK, etc.) | | `get_clipboard` | Read device clipboard | | `set_clipboard` | Write to device clipboard | ## Touchpad [Section titled “Touchpad”](#touchpad) | Tool | Description | | ----------------- | ------------------------------------ | | `touchpad_tap` | Tap via the touchpad input device | | `touchpad_scroll` | Scroll via the touchpad input device | ## Element interaction [Section titled “Element interaction”](#element-interaction) | Tool | Description | | ------------------- | -------------------------------------------------------- | | `tap_element` | Tap an element found by accessibility selector | | `fill_element` | Fill a text field found by accessibility selector | | `fill_form` | Fill multiple form fields at once | | `scroll_to_element` | Scroll until an element matching the selector is visible | | `wait_for_element` | Wait for an element to appear on screen | ## Screen [Section titled “Screen”](#screen) | Tool | Description | | ---------------------- | ------------------------------------------------------------------- | | `screenshot` | Capture the device screen as PNG or JPEG | | `ui_dump` | Accessibility tree (compressed BFS or raw XML) | | `annotated_screenshot` | Screenshot with structured element index overlay | | `get_orientation` | Get the current screen orientation | | `set_orientation` | Lock to portrait, landscape, reverse portrait, or reverse landscape | ## Apps [Section titled “Apps”](#apps) | Tool | Description | | --------------- | --------------------------------------------------------------------- | | `list_apps` | List installed packages | | `launch_app` | Launch by package name, optionally set a per-app locale (Android 13+) | | `stop_app` | Force-stop by package name | | `uninstall_app` | Uninstall by package name | | `install_apk` | Install APK(s) with split-APK support | | `open_url` | Open a URL or deep link on the device | ## Files [Section titled “Files”](#files) | Tool | Description | | --------------- | ------------------------------------------ | | `file_list_dir` | List files and directories (symlink-aware) | | `file_pull` | Pull file content (text or base64) | | `file_push` | Push a host file to the device | ## Database [Section titled “Database”](#database) | Tool | Description | | ---------------- | ------------------------------------------ | | `list_databases` | List SQLite databases for a debuggable app | | `sqlite_query` | Run a SQL query against an app’s database | ## Logcat [Section titled “Logcat”](#logcat) | Tool | Description | | -------- | ------------------------------------------------------------------ | | `logcat` | Logcat snapshot with level, tag, query, PID, and package filtering | ## Emulator controls [Section titled “Emulator controls”](#emulator-controls) | Tool | Description | | ----------------------- | ------------------------------------------------------------------------ | | `set_location` | **\[Emulator only]** Set GPS coordinates, altitude, speed, bearing | | `set_battery` | **\[Emulator only]** Set level, charging status, health | | `list_network_profiles` | List available network throttling profiles | | `set_network` | **\[Emulator only]** Apply a network profile or custom bandwidth/latency | ## AVD management [Section titled “AVD management”](#avd-management) | Tool | Description | | ------------------- | ----------------------------------------------------- | | `list_avds` | List available AVDs | | `start_emulator` | Start an emulator by AVD name (waits for boot) | | `stop_emulator` | Stop a running emulator by serial | | `create_avd` | Create a new AVD with device profile and system image | | `delete_avd` | Delete an AVD | | `clone_avd` | Clone an AVD with copy-on-write | | `update_avd_config` | Update AVD hardware configuration | | `get_avd_config` | Read AVD hardware configuration | ## SDK and licensing [Section titled “SDK and licensing”](#sdk-and-licensing) | Tool | Description | | ---------------------- | ---------------------------------------- | | `check_licenses` | Check which SDK licenses need acceptance | | `accept_licenses` | Accept pending SDK licenses | | `install_system_image` | Download and install a system image | ## Snapshots [Section titled “Snapshots”](#snapshots) | Tool | Description | | ----------------- | --------------------------------------------- | | `list_snapshots` | **\[Emulator only]** List saved snapshots | | `save_snapshot` | **\[Emulator only]** Save current state | | `load_snapshot` | **\[Emulator only]** Restore a saved snapshot | | `delete_snapshot` | **\[Emulator only]** Delete a snapshot | ## Recording and bug reports [Section titled “Recording and bug reports”](#recording-and-bug-reports) | Tool | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `start_recording` | Start screen recording. `mode`: `auto` (default; quality when mirroring is available, otherwise screenrecord), `quality`, or `screenrecord`. Returns the mode used | | `stop_recording` | Stop the active recording (either mode) and return the MP4 path | | `collect_bug_report` | Capture a bug report with video, inputs, and logs | | `list_bug_reports` | List saved bug reports | | `read_bug_report` | Read bug report metadata and content | | `export_bug_report` | Export as a single .emubug file | | `update_bug_report` | Update bug report metadata | | `import_bug_report` | Import a .emubug file | ### Recording modes [Section titled “Recording modes”](#recording-modes) | Mode | How it records | Limits | | -------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `quality` | Host-side encode of the live mirror stream at native resolution, 30 FPS, 8 Mbps | Does not support display-size changes: rotation or fold/unfold ends the recording or corrupts the video after that point. Record each posture separately and stitch the files | | `screenrecord` | Android’s built-in `screenrecord` on the device | 180-second limit and lower quality, but follows rotation and fold/unfold correctly | | `auto` | `quality` if device mirroring is running or can be started, otherwise `screenrecord` | Same as the mode it picks, including on foldables | ## Network proxy [Section titled “Network proxy”](#network-proxy) | Tool | Description | | ------------------------ | ---------------------------------- | | `network_proxy_status` | Check if the proxy is running | | `start_network_proxy` | Start the HTTPS interception proxy | | `stop_network_proxy` | Stop the proxy | | `list_network_requests` | List captured HTTP requests | | `get_network_request` | Get details of a specific request | | `clear_network_requests` | Clear captured requests | ## Android Auto [Section titled “Android Auto”](#android-auto) | Tool | Description | | ----------------------- | ------------------------------------------------------- | | `start_android_auto` | Start Android Auto projection | | `stop_android_auto` | Stop Android Auto projection | | `aa_tap` | Tap on the head unit display | | `aa_swipe` | Swipe on the head unit display | | `aa_screenshot` | Capture the head unit screen | | `aa_start_recording` | Start recording the head unit screen (quality recorder) | | `aa_stop_recording` | Stop the head unit recording and return the MP4 path | | `aa_send_location` | Send GPS location to Android Auto | | `aa_set_night_mode` | Toggle day/night mode | | `aa_set_driving_status` | Set driving status (Parked, Idle, Moving) | ## Resources [Section titled “Resources”](#resources) | Resource URI | Description | | --------------- | ------------------------------------------------- | | `device://list` | Subscribable device list (auto-notify on changes) | # Configuration > Settings and configuration options for Emu. ## Settings storage [Section titled “Settings storage”](#settings-storage) Emu persists all settings in a local SQLite database. You can access settings from the **Settings** tab in the GUI. | Platform | Database path | | -------- | ------------------------------------------ | | macOS | `~/Library/Application Support/emu/emu.db` | | Linux | `~/.local/share/emu/emu.db` | | Windows | `%APPDATA%\emu\emu.db` | ## GPU mode [Section titled “GPU mode”](#gpu-mode) Controls how the emulator renders graphics. Configurable per-AVD in the device wizard and settings. * **Host**: Uses your system’s GPU for hardware-accelerated rendering. Default and recommended. * **Software**: CPU-based rendering. Use if you encounter graphical glitches or lack a compatible GPU. ## Mirror backend [Section titled “Mirror backend”](#mirror-backend) Emu automatically selects the mirroring backend based on device type and host capabilities — emulators use an optimized shared-memory path when available, physical devices use the scrcpy protocol, and the system falls back through hardware decode options (H.265/H.264 Vulkan, platform hardware decode, software decode) as needed. You can manually override the backend in **Settings > Mirroring**. ## MCP port [Section titled “MCP port”](#mcp-port) The MCP server listens on port `8565` by default. Change it in the settings panel or via the CLI: ```bash emu mcp --port 9000 ``` ## Tiling window managers [Section titled “Tiling window managers”](#tiling-window-managers) The mirror popout window works best when configured to float. Emu sets the `app_id` to `emu-live` on Wayland. ### Hyprland [Section titled “Hyprland”](#hyprland) Add to `~/.config/hypr/hyprland.conf` and run `hyprctl reload`: ```text windowrule = match:class emu-live, float on windowrule = match:class emu-screenshot, float on ``` # Keyboard shortcuts > Keyboard shortcuts for Emu. * macOS ### Global Open command palette`⌘``K` ### Mirror view Multi-touch second finger`⌘``Click` Pinch zoom`⌘``Scroll` Scroll on device`Scroll` * Linux/Windows ### Global Open command palette`Ctrl``K` ### Mirror view Multi-touch second finger`Ctrl``Click` Pinch zoom`Ctrl``Scroll` Scroll on device`Scroll` # Telemetry > What usage data Emu collects, why, and how to opt out. ## What Emu collects [Section titled “What Emu collects”](#what-emu-collects) Emu collects anonymous usage data to understand how the product is used and to improve the experience. | Event | When it fires | Properties | | ------------------- | ---------------------------------- | ----------------------------------------------------------------------- | | `app-first-launch` | First time the app starts | `platform`, `arch` | | `app-session-start` | Every app launch | `trial_day`, `license_status`, `version`, `platform`, `arch` | | `checkout-clicked` | “Buy Early Bird” button clicked | `trial_day`, `license_status`, `source` (settings\_card or nag\_dialog) | | `trial-expired` | Trial period ends (sent once) | `trial_day_at_last_use` | | `license-activated` | License key successfully validated | `trial_day` | ## Never collected [Section titled “Never collected”](#never-collected) The following data is never sent, under any circumstances: * Personal information (name, email, IP address, location) * Environment variables or shell configuration * File paths, file contents, or source code * Connected Android device data, app data, or databases * Screenshots, screen recordings, or video buffers * Network traffic, intercepted HTTP requests, or HAR exports * Keystroke or input recording * Crash reports, error logs, or stack traces * ADB shell command output Each installation gets a random anonymous UUID that is not tied to any account or personal identifier. The UUID exists solely to count unique installations. Approximate location (country/city) is derived from your IP address when events are received. The raw IP address is discarded after processing and is not stored. ## Where data is sent [Section titled “Where data is sent”](#where-data-is-sent) Events are sent to [PostHog](https://posthog.com), a product analytics platform. Data is stored in the US region (`us.i.posthog.com`). No data is shared with third parties. Events are retained for up to **7 years** per PostHog’s data retention policy. ## First-run notice [Section titled “First-run notice”](#first-run-notice) On first launch, Emu displays a one-time notice informing you that anonymous usage data is collected. Telemetry is enabled by default (legitimate interest under GDPR Art. 6(1)(f)). You can opt out at any time. ## How to opt out [Section titled “How to opt out”](#how-to-opt-out) You can disable telemetry at any time using an environment variable or the in-app settings. * Environment variable Set this variable before launching Emu: ```bash export EMU_TELEMETRY_DISABLED=1 ``` * In-app toggle Navigate to **Settings** → **Privacy** and toggle off **Share anonymous usage data**. When you opt out, no analytics client is initialized and zero network requests are made to PostHog. ## Inspecting telemetry [Section titled “Inspecting telemetry”](#inspecting-telemetry) To see exactly what Emu sends, enable debug mode: ```bash EMU_TELEMETRY_DEBUG=1 emu ``` This logs every event payload to stderr as JSON. Events are still sent normally — combine with `EMU_TELEMETRY_DISABLED=1` to log without sending. ## Privacy policy [Section titled “Privacy policy”](#privacy-policy) Read the full privacy policy here: [Privacy Policy](https://emu.marathonlabs.io/privacy) # Troubleshooting > Common issues and how to fix them. ## Emulator won’t start [Section titled “Emulator won’t start”](#emulator-wont-start) ### “Hardware acceleration unavailable” [Section titled ““Hardware acceleration unavailable””](#hardware-acceleration-unavailable) Emu falls back to CPU-based emulation when hardware acceleration is not available. This is significantly slower. * **Linux**: Verify KVM is enabled: `ls /dev/kvm`. If missing, enable virtualization in your BIOS/UEFI settings. * **Windows**: Enable Hyper-V or WHPX in Windows Features. * **macOS**: Hardware acceleration is available by default on Apple Silicon. ### Emulator exits immediately [Section titled “Emulator exits immediately”](#emulator-exits-immediately) If an emulator crashes on startup, try cold booting it — this discards any saved state that may be corrupted. In the device list, use the emulator’s context menu to select **Cold Boot**. If that doesn’t help, check the emulator log. Emu writes emulator output to a log file that you can view from the device context menu. ## Android SDK setup appears when SDK is already installed [Section titled “Android SDK setup appears when SDK is already installed”](#android-sdk-setup-appears-when-sdk-is-already-installed) On macOS, Emu may show the SDK setup screen on launch even though you already have the Android SDK installed. This is expected — macOS does not pass environment variables like `ANDROID_HOME` from your shell profile (`~/.zshrc`, `~/.bashrc`) to GUI apps launched from Finder, Spotlight, or the Dock. When the setup screen appears, select your existing SDK directory (typically `~/Library/Android/sdk`). Emu saves this location and won’t ask again. ## Mirror not connecting [Section titled “Mirror not connecting”](#mirror-not-connecting) ### All mirror backends failed [Section titled “All mirror backends failed”](#all-mirror-backends-failed) Emu tries multiple streaming backends in order and falls back through each one. If all fail: 1. Verify the device is online in the device list. 2. For emulators, verify gRPC is enabled (it is by default in modern Android Emulator versions). 3. Try switching the mirror backend manually in **Settings > Mirroring**. 4. Restart the device connection by clicking on a different device and back. ### Brief frame gap on rotation [Section titled “Brief frame gap on rotation”](#brief-frame-gap-on-rotation) When the device rotates, the video encoder restarts. You may see a brief blank frame (\~100ms). This is a limitation of the current scrcpy protocol version and may be eliminated in a future update. ## Text input issues [Section titled “Text input issues”](#text-input-issues) ### Non-Latin characters not appearing [Section titled “Non-Latin characters not appearing”](#non-latin-characters-not-appearing) Android’s default input pipeline cannot inject non-Latin characters (CJK, emoji, accented characters) via key events. Enable the **Companion IME** in **Settings** to use a direct text injection path that supports all Unicode characters. If the companion IME is not available, Emu falls back to clipboard paste — it copies the text to the device clipboard and triggers a paste action. ## Network inspector [Section titled “Network inspector”](#network-inspector) ### No traffic appearing [Section titled “No traffic appearing”](#no-traffic-appearing) * Verify the proxy is started — click **Enable** in the Network tab. * The device must be a rooted emulator (`userdebug` or `eng` system image). Play Store images and non-rooted physical devices cannot have the proxy certificate injected. ### Certificate errors in the app [Section titled “Certificate errors in the app”](#certificate-errors-in-the-app) Some apps use certificate pinning, which rejects the injected proxy certificate. Emu cannot intercept traffic from these apps without modifying the app itself. ## Snapshots [Section titled “Snapshots”](#snapshots) ### “Snapshots are not supported with this emulator’s GPU mode” [Section titled ““Snapshots are not supported with this emulator’s GPU mode””](#snapshots-are-not-supported-with-this-emulators-gpu-mode) Emulator snapshots are incompatible with certain GPU modes. Switch the GPU mode to **Host** or **Auto** in the device wizard or settings to enable snapshot support. ## File browser [Section titled “File browser”](#file-browser) ### Directory listing shows empty for /sdcard [Section titled “Directory listing shows empty for /sdcard”](#directory-listing-shows-empty-for-sdcard) The `/sdcard` path is a double symlink on Android (`/sdcard` → `/storage/self/primary` → `/storage/emulated/0`). The first listing attempt may return empty. Emu automatically resolves the symlink and retries — if you see an empty listing, navigate into the path again and it will populate.