# Welcome to DeviceCloud

DeviceCloud is a third-party Maestro Cloud alternative.

Reasons to switch:

* Cheap unit-based pricing
* Up to 200 parallel runs
* Twice as fast as Maestro Cloud for half the cost
* Google Play APIs
* Tablets and iPads
* Fast support response times (via Discord or Slack Connect)

## Try it

The CLI is intentionally designed to mimic the Maestro Cloud API, so in most cases the migration is one line:

```bash
# before
maestro cloud --apiKey <apiKey> <appFile> <flowFile>

# after
dcd cloud <appFile> <flowFile>
```

Start with the [Quick Start](/getting-started/quickstart) to install the CLI and run your first flow.

## Quick links

### Get started

* [Quick Start](/getting-started/quickstart) - install the CLI and run your first flow
* [Authentication](/getting-started/api-keys) - `dcd login` locally, API key in CI
* [Flows & Workspaces](/getting-started/flows-and-workspaces) - run a single file, a directory, or a whole workspace
* [Devices & OS Versions](/getting-started/devices-configuration) - every supported device, Android API level and iOS version

### Run in CI

* [CI/CD Integration](/ci-cd-integration/overview) - supported providers, and the features that matter in a pipeline
* [GitHub Actions](/ci-cd-integration/github-actions) - drop-in replacement for the Maestro Cloud action
* [GitHub Checks](/ci-cd-integration/github-checks) - report pass/fail straight onto the pull request
* [Artifacts & Downloads](/test-artifacts-and-reports/artifacts) - pull logs, screenshots, videos and reports

### Configure your runs

* [Device Matrix](/configuration/device-matrix) - run your suite across several devices in one upload
* [Maestro Versions](/configuration/maestro-versions) - pin a Maestro version, or track `latest`
* [Environment Variables](/configuration/environment-variables) - inject config and secrets into your flows

### Automate with AI & code

* [MCP Server](/ai-agents-and-mcp/overview) - let Claude, Cursor and other MCP clients run your tests
* [CLI Reference](/cli-reference/overview) - every `dcd` command and flag
* [REST API](/rest-api/overview) - uploads, results and flows over HTTP

### Pricing & plans

* [Test Run Billing](/billing/test-run-billing) - what a single test run costs
* [Subscriptions](/billing/subscriptions) - the Pro, Max and Enterprise plans
* [Concurrency & Parallel Runs](/getting-started/concurrency-and-parallel-runs) - how many of your tests run at once

## FAQ

### Is DeviceCloud really a drop-in replacement for Maestro Cloud?

For most projects, yes. The CLI mimics the Maestro Cloud API, so you change `maestro cloud` to `dcd cloud` and drop the `--apiKey` flag. On GitHub Actions you swap a single `uses:` line and all other inputs are compatible. See [Quick Start](/getting-started/quickstart) and [Migrating from Maestro Cloud](/ci-cd-integration/github-actions#migrating-from-maestro-cloud).

### How much does it cost?

DeviceCloud bills per test run i.e. a single top-level flow on a single device and are charged at $0.11 for standard iOS and $0.09 for standard Android. iPad, Google Play, tablet and non-default runner flows are charged at the advanced rate of $0.15. New accounts get $20 of free credits. See [Test Run Billing](/billing/test-run-billing).

### How many tests can I run in parallel?

It depends on your plan: Pro runs up to 5 iOS and 5 Android tests at once, and Max up to 20 of each. Enterprise concurrency is arranged to fit your requirements. Once you hit your maximum, further tests queue and start automatically as capacity frees up. See [Concurrency & Parallel Runs](/getting-started/concurrency-and-parallel-runs) and [Subscriptions](/billing/subscriptions).

### Which devices and OS versions can I run on?

Android covers Pixel 6, 6 Pro, 7 and 7 Pro plus a generic tablet, on API levels 29–36. iOS covers the iPhone 14, 15 and 16 families and the iPad Pro (6th generation) on iOS 17, 18 and 26. Not every device supports every OS version, so check the availability tables in [Devices & OS Versions](/getting-started/devices-configuration).

### Which Maestro versions are supported?

We always support the latest version of Maestro with backwards compatibility up to v2.0.4. Runs default to v2.2.0, and you can use `--maestro-version latest` to always use the most up to date version. We periodically remove support for older versions, and these will be announced with plenty of warning. See [Maestro Versions](/configuration/maestro-versions).

### Do I need an API key, or can I just log in?

Both work. Run `dcd login` once for local use and our CLI will store your session, so you never pass a key on a command. For CI or any headless environment, you can generate and set `DEVICE_CLOUD_API_KEY` or pass `--api-key`. When more than one credential is present, precedence is the `--api-key` flag, then the environment variable, then the stored session. See [Authentication](/getting-started/api-keys).

### How do I run DeviceCloud in my CI?

There are first-class integrations for GitHub Actions, Bitrise, Bitbucket Pipelines and EAS Workflows. Any other provider can call the CLI directly with `npx --yes @devicecloud.dev/dcd@latest cloud <app-file> <flows-dir>` and gate the build on the [exit code](/advanced/exit-codes). See [CI/CD Integration](/ci-cd-integration/overview).

### How do I get videos, logs and test reports?

Pass `--download-artifacts ALL` (or `FAILED`) to a `dcd cloud` run, or fetch them after the fact with `dcd artifacts --upload-id <uuid> --download-artifacts ALL`. Everything is also downloadable from the console. Reports are available as `junit`, `html`, `html-detailed` and `allure` via `--report`. See [Artifacts & Downloads](/test-artifacts-and-reports/artifacts) and [Report Formats](/test-artifacts-and-reports/report-formats).

### Is there a time limit on a test?

Yes. Every flow has a 10-minute execution limit, after which it is automatically cancelled. Failed tests can be retried up to twice, giving a maximum of three total runs, free of charge. See [Quick Start](/getting-started/quickstart#limits) and [Retry Strategies](/advanced/retry-strategies).

### Can I allow-list DeviceCloud in our firewall?

Yes. Test runner egress comes from a published set of IP ranges, and the same list is served from an unauthenticated endpoint — `GET https://api.devicecloud.dev/ip-addresses` so your tooling can poll it rather than watching a docs page. See [IP Addresses](/security-and-compliance/ip-addresses).

### Can AI agents drive DeviceCloud?

Yes. The `@devicecloud.dev/dcd` package ships `dcd-mcp`, a Model Context Protocol server that lets Claude, Cursor, VS Code and other MCP clients list devices, submit runs, check status and download artifacts. It's in beta, and the one billable tool can be hidden entirely with `--read-only`. See [MCP Server](/ai-agents-and-mcp/overview).

### How do I get support?

Every account gets community and team support via [Discord](https://discord.gg/gm3mJwcNw8), and you can also email <support@devicecloud.dev>. We aim to respond within four hours for urgent queries and three days for non-urgent. Max and Enterprise customers both receive priority email support and Enterprise customers have direct access to the team via Slack Connect. Support hours are 8am–8pm UK time, Monday to Friday. See [Service Level Agreements](/legal/service-level-agreements).

{% hint style="info" %}
Question not answered here? Ask in our [Discord](https://discord.gg/gm3mJwcNw8) or email <support@devicecloud.dev> and we'll be happy to help.
{% endhint %}


# Quick Start

The dcd command line is intentionally designed to mimic the Maestro Cloud API. In most cases, you can just change `maestro cloud` to `dcd cloud`:

```bash
# maestro cloud --apiKey <apiKey> <appFile> <flowFile>

dcd cloud <appFile> <flowFile>
```

### 1. Install the CLI

The recommended install is the standalone binary — no Node required.

{% tabs %}
{% tab title="macOS / Linux" %}

```bash
curl -fsSL https://get.devicecloud.dev/install.sh | sh
```

{% endtab %}

{% tab title="Windows" %}

```powershell
irm https://get.devicecloud.dev/install.ps1 | iex
```

{% endtab %}

{% tab title="npm" %}
Install [Node 22+](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), then run:

```bash
npm install -g @devicecloud.dev/dcd
```

{% endtab %}
{% endtabs %}

### 2. Authenticate

Log in once in your browser — the CLI remembers your session, so you don't need to pass a key on every command:

```bash
dcd login
```

Running in CI or a headless environment? Use an API key instead. Find it in the [console settings](https://console.devicecloud.dev/settings) page and set it as an environment variable:

```bash
export DEVICE_CLOUD_API_KEY=your-api-key
```

See [Authentication](/getting-started/api-keys) for more detail.

### 3. Run your first flow

**iOS**

```bash
dcd cloud <appFile>.app <flowFile>
```

**Android**

```bash
dcd cloud <appFile>.apk <flowFile>
```

That's it! Questions? Issues? Head to our [Discord](https://discord.gg/gm3mJwcNw8).

***

### Upgrading the CLI

If you installed the standalone binary:

```bash
dcd upgrade
```

If you installed via npm:

```bash
npm install -g @devicecloud.dev/dcd@latest
```

**Prerelease versions** (not recommended): Occasionally, prerelease versions are available under the `alpha`, `beta`, and `rc` tags, in increasing order of stability. See [available versions on NPM](https://www.npmjs.com/package/@devicecloud.dev/dcd?activeTab=versions).

```bash
npm install -g @devicecloud.dev/dcd@alpha
npm install -g @devicecloud.dev/dcd@beta
npm install -g @devicecloud.dev/dcd@rc
```

***

### Limits

Every flow has a 10-minute execution limit after which it will be automatically cancelled.

***

### Next steps

* [Flows & Workspaces](/getting-started/flows-and-workspaces) — organise and run multiple flows
* [Devices & OS Versions](/getting-started/devices-configuration) — choose Android API level or iOS version
* [App Management](/configuration/app-management) — upload and reuse binaries
* [CI/CD Integration](/ci-cd-integration/overview) — run tests in your pipeline

```
```


# Authentication

DeviceCloud supports two ways to authenticate the CLI: **browser login** (best for local, interactive use) and an **API key** (best for CI and headless environments). The REST API uses the API key.

## `dcd login` (recommended for local use)

Run `dcd login` once and authenticate in your browser. The CLI stores the resulting session locally, so you don't need to pass a key on every command — and it unlocks live test updates and a smoother experience.

```bash
dcd login
```

This opens your browser, where you sign in (email OTP or Enterprise SSO) and authorise the CLI. If you belong to more than one organisation, you'll be prompted to pick one. The session is stored at:

* `$XDG_CONFIG_HOME/dcd/config.json`, or
* `~/.dcd/config.json` if `XDG_CONFIG_HOME` is not set

The file is written with `0600` permissions (owner read/write only). Expiring sessions are refreshed automatically — you generally only log in again if you log out or switch machines.

{% hint style="info" %}
No browser on the box (e.g. over SSH)? Run `dcd login --no-browser` and the CLI prints a URL to open elsewhere.
{% endhint %}

See [Login & Accounts](/cli-reference/dcd-login) for `logout`, `whoami`, and `switch-org`.

## API key (recommended for CI / headless)

In CI and other non-interactive environments, use an API key.

### Getting your API key

1. Log in to the [console](https://console.devicecloud.dev/settings)
2. Open **Settings → API Key**
3. Copy the key

### Using your API key

Set it as an environment variable so you don't have to pass it on every command:

```bash
export DEVICE_CLOUD_API_KEY=your-api-key-here
dcd cloud <appFile> <flowFile>
```

Or pass it explicitly with `--api-key` on any command:

```bash
dcd cloud --api-key your-api-key-here <appFile> <flowFile>
```

{% hint style="info" %}
Store your API key as a secret in your CI provider (e.g. a GitHub Actions secret) — never commit it to your repository.
{% endhint %}

## Precedence

When more than one credential is available, the CLI resolves authentication in this order:

1. `--api-key` flag
2. `DEVICE_CLOUD_API_KEY` environment variable
3. Stored session from `dcd login`

## REST API

The [REST API](/rest-api/overview) authenticates with the API key, passed in the `x-app-api-key` header:

```bash
curl https://api.devicecloud.dev/... \
  -H "x-app-api-key: your-api-key-here"
```


# Devices & OS Versions

### Default Devices

If you don't specify a device or OS version, then you will be allocated the default device image:

**Android:** Pixel 7 (API level 34)

**iOS:** iPhone 14 (iOS 17.5)

### Android API Levels

DeviceCloud will default to API level 34 unless you pass the `--android-api-level` flag:

```bash
dcd cloud app.apk test.yaml --android-api-level 35
```

| Android Version | API Level |
| --------------- | --------- |
| Android 16      | 36        |
| Android 15      | 35        |
| Android 14      | 34        |
| Android 13      | 33        |
| Android 12L     | 32        |
| Android 12      | 31        |
| Android 11      | 30        |
| Android 10      | 29        |

Need Google Play? See [google-play-apis.md](/configuration/google-play-apis)

### Android Devices

DeviceCloud will default to Pixel 7 unless you pass the `--android-device` flag:

```bash
dcd cloud app.apk test.yaml --android-device pixel-6
```

| id               | Name                                                                                   | Dimensions  | Valid Android API Levels       |
| ---------------- | -------------------------------------------------------------------------------------- | ----------- | ------------------------------ |
| `pixel-6`        | Pixel 6                                                                                | 1080 x 2400 | 29, 30, 31, 32, 33, 34, 35, 36 |
| `pixel-6-pro`    | Pixel 6 Pro                                                                            | 1440 x 3120 | 33, 35                         |
| `pixel-7`        | Pixel 7                                                                                | 1080 x 2340 | 33, 34, 35, 36                 |
| `pixel-7-pro`    | Pixel 7 Pro                                                                            | 1440 x 3120 | 33, 34, 35, 36                 |
| `generic-tablet` | Generic Tablet (Note: starts in landscape by default, use orientation=90 for portrait) | 1440 x 3120 | 33                             |

### iOS Versions

DeviceCloud will default to iOS 17 unless you pass the `--ios-version` flag:

```bash
dcd cloud app.zip test.yaml --ios-version 18
```

{% hint style="warning" %}
iOS 16 was removed on 24th August 2026 and tests targeting it will now fail. Switch to iOS 17 or newer — iPhone 14 also supports 17 and 18.
{% endhint %}

| id   | Version |
| ---- | ------- |
| `26` | 26.4    |
| `18` | 18.6    |
| `17` | 17.5    |

### iOS Devices

DeviceCloud will default to iPhone 14 unless you pass the `--ios-device` flag:

```bash
dcd cloud app.zip test.yaml --ios-device ipad-pro-6th-gen
```

| id                  | Name                      | Dimensions  | Valid iOS versions |
| ------------------- | ------------------------- | ----------- | ------------------ |
| `iphone-16-pro-max` | iPhone 16 Pro Max         | 1290 x 2796 | 18, 26             |
| `iphone-16-pro`     | iPhone 16 Pro             | 1179 x 2556 | 18, 26             |
| `iphone-16-plus`    | iPhone 16 Plus            | 1290 x 2796 | 26                 |
| `iphone-16`         | iPhone 16                 | 1170 x 2532 | 18, 26             |
| `iphone-15`         | iPhone 15                 | 1170 x 2532 | 17                 |
| `iphone-14`         | iPhone 14                 | 1170 x 2532 | 17, 18             |
| `ipad-pro-6th-gen`  | iPad Pro (6th Generation) | 2732 x 2048 | 18, 26             |

### Targeting a single flow

The flags above set the device for the whole upload. When only one flow needs a particular device then that flow can name its own device in its YAML instead. See [per-flow-devices.md](/configuration/per-flow-devices).

### Running your suite across several devices

To run every flow against several devices from a single `dcd cloud` invocation use a [device matrix](/configuration/device-matrix).


# Flows & Workspaces

There are multiple ways to execute flows from the CLI:

1. a single YAML flow
2. a directory of flows
3. flows that match a glob
4. flows specified in a workspace config file

### 1. Executing a single flow

The most straightforward way to execute a single flow is to pass the flow file path directly to the CLI using a `<flowFile>`.

```
dcd cloud <appFile> <flowFile>
```

Where:

* `<appFile>` is one of:
  * `.app`
  * `.zip`
  * `.apk`
* `<flowFile>` is one of:
  * `.yaml`
  * `.yml`

### 2. Executing flows by passing a directory

```
dcd cloud <appFile> <directoryPath>
```

Where `<directoryPath>` is either:

* an absolute path, i.e. `/path/from/root`
* a relative path, i.e. `./` or `path/from/currentDir`

The CLI will inspect all YAML files in the directory (but not sub-directories) and create a test for each.

### 3. Executing flows by passing a glob

```
dcd cloud <appFile> <glob>
```

Where `<glob>` is a path matching string such as `./**/*.yaml`

The CLI uses the [NPM glob](https://www.npmjs.com/package/glob) module. This package provides its own CLI which you can use for debugging globs.

### 4. Executing flows using a Workspace Config file

For complex setups, a `config.yaml` file is recommended. Place it in the top-level directory you pass to the CLI and it will be detected automatically.

```
dcd cloud <appFile> <directoryPathIncludingConfigYaml>
```

See [Workspace Configuration](/configuration/workspace-config) for more information.

### Referencing flows

As of version 2.0.0, the CLI will search for all nested dependencies referenced by your YAML flows using Maestro keywords (`addMedia`, `runFlow`, `runScript`).


# Concurrency & Parallel Runs

DeviceCloud supports parallel runs, also known as concurrency. This means multiple tests run simultaneously, greatly reducing the overall test suite execution time for a given CI run.

In your DeviceCloud settings page, you'll see two values for each platform: minimum guaranteed concurrency (known as **Concurrency Protection**) and maximum concurrency.

Cheaper plans typically have no Concurrency Protection, which means that during busy periods when runner capacity is stretched, you may need to wait for runner availability or experience less concurrency than your maximum.

Once you reach your maximum concurrency for iOS or Android, additional tests enter a queued state and sit in a backlog until you're under your concurrency limits. At that point, they automatically move to pending and are processed by the runners.

If you have a plan with Concurrency Protection, DeviceCloud will prioritise your jobs to ensure your minimum concurrency is met.

Note that concurrency guarantees only apply to standard runner types. Experimental runner types, such as Android's M1 runner, are not covered by these concurrency rules.

#### **Enterprise Plans**

If you need larger guaranteed concurrency or a bespoke concurrency setup and availability, please contact us at <sales@devicecloud.dev> and we'd be happy to arrange an enterprise plan that meets your requirements.

#### **Why limit concurrency at all?**

DeviceCloud owns its own hardware, which is how we're able to offer such competitive prices. However, we have limited capacity in our runner pool. As such, we sometimes need to limit concurrency to ensure fair throughput for everyone. As more users subscribe, we gradually invest in and expand this pool to increase overall capacity.


# Overview

DeviceCloud supports a wide range of CI/CD options. If you don't see your provider here, please let us know.

### Integration Guides

* [GitHub Actions](/ci-cd-integration/github-actions) - Full GitHub Actions integration with all options
* [Bitrise](/ci-cd-integration/bitrise-steps) - Bitrise integration
* [Bitbucket Pipelines](/ci-cd-integration/bitbucket-pipelines) - Bitbucket Pipelines integration
* [EAS Workflows](/ci-cd-integration/eas-workflows) - EAS integration
* [Any CI](/ci-cd-integration/any-ci) - Generic integration using the CLI directly

### Useful Features For CI/CD

* [Async Execution](/advanced/async-execution) - Fire-and-forget tests without blocking your pipeline
* [dcd status](/cli-reference/dcd-status) - Poll for results after an async run
* [Report Formats](/test-artifacts-and-reports/report-formats) - Generate JUnit/HTML reports for your CI system
* [Artifacts & Downloads](/test-artifacts-and-reports/artifacts) - Access logs, screenshots, and videos

### Configuration

* [Environment Variables](/configuration/environment-variables) - Pass secrets and config to your flows
* [API Keys](/getting-started/api-keys) - Manage authentication
* [Exit Codes](/advanced/exit-codes) - Handle pass/fail in your pipeline


# GitHub Actions

The DeviceCloud GitHub Action is a drop-in replacement for the [Maestro Cloud Action](https://github.com/mobile-dev-inc/action-maestro-cloud). The inputs are identical where practical, so switching is straightforward.

Additionally, you can set up our GitHub App to use GitHub Checks to manage pull requests and blocking. Read more [here](/ci-cd-integration/github-checks).

## Quick Start

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: <path_to_your_app_file>
```

Store your API key as a [GitHub Actions secret](https://docs.github.com/en/actions/security-guides/encrypted-secrets) named `DCD_API_KEY`. You can find your API key in the [console settings](https://console.devicecloud.dev/settings).

## Trigger Recommendations

Trigger tests on pushes to your main branch and on pull requests:

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

{% hint style="info" %}
If you use `pull_request_target` to run tests from forked PRs, explicitly check out the PR HEAD to ensure you're testing the changed code.
{% endhint %}

```yaml
on:
  push:
    branches: [main]
  pull_request_target:
    branches: [main]
jobs:
  run-maestro-on-dcd:
    name: Run Flows on DeviceCloud
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}
```

## Platform Examples

### Android

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app/build/outputs/apk/debug/app-debug.apk
```

`app-file` accepts a path or a glob pattern. When using a glob, the first matched file is used.

### iOS

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: <app_name>.zip
```

`app-file` must point to an Apple silicon compatible Simulator `.app` build, or a zipped `.zip` bundle. Glob patterns are supported; the first match is used.

### iOS with Expo

If you build with EAS, download the build artifact in an earlier step and pass the resulting file to `app-file`. For a first-class Expo experience that wires this up for you, use the dedicated [EAS Workflows integration](/ci-cd-integration/eas-workflows) instead.

***

## Inputs Reference

### Authentication

| Input     | Required | Description                                                                                    |
| --------- | -------- | ---------------------------------------------------------------------------------------------- |
| `api-key` | Yes      | Your DeviceCloud API key. Can also be set via the `DEVICE_CLOUD_API_KEY` environment variable. |

### App Configuration

| Input              | Required | Default | Description                                                                                            |
| ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `app-file`         | No\*     | —       | Path or glob to the app binary (APK or .app/.zip). \*Either `app-file` or `app-binary-id` is required. |
| `app-binary-id`    | No\*     | —       | ID of a previously uploaded app binary. Skips the upload step.                                         |
| `ignore-sha-check` | No       | `false` | Skip the SHA hash check that prevents duplicate uploads. Not recommended.                              |

### Flow Selection

| Input           | Required | Default                    | Description                                                                                                   |
| --------------- | -------- | -------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `flows`         | No       | `./.maestro/`              | Path to a flow file or folder containing flows. Alias: `workspace`. `flows` takes precedence if both are set. |
| `workspace`     | No       | `./.maestro/`              | Alias for `flows`.                                                                                            |
| `exclude-flows` | No       | —                          | Subdirectories to exclude when building the flow list.                                                        |
| `include-tags`  | No       | —                          | Only run flows with these Maestro tags (comma-separated).                                                     |
| `exclude-tags`  | No       | —                          | Exclude flows with these Maestro tags (comma-separated).                                                      |
| `config`        | No       | `config.yaml` in workspace | Path to a custom Maestro config file.                                                                         |

### Device Configuration

| Input               | Required | Default   | Description                                                                                                                                   |
| ------------------- | -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `android-device`    | No       | —         | Android device model. Options: `pixel-6`, `pixel-6-pro`, `pixel-7`, `pixel-7-pro`, `generic-tablet`.                                          |
| `android-api-level` | No       | `34`      | Android API level. Options: `29`, `30`, `31`, `32`, `33`, `34`, `35`, `36`.                                                                   |
| `ios-device`        | No       | —         | iOS device model. Options: `iphone-14`, `iphone-15`, `iphone-16`, `iphone-16-plus`, `iphone-16-pro`, `iphone-16-pro-max`, `ipad-pro-6th-gen`. |
| `ios-version`       | No       | `17`      | Major iOS version. Options: `17`, `18`, `26`.                                                                                                 |
| `device-locale`     | No       | —         | Device locale in `ISO-639-1_ISO-3166-1` format (e.g. `de_DE`). See [Device Locale](/configuration/device-locale).                             |
| `orientation`       | No       | `0`       | Android only. Device orientation in degrees. Options: `0`, `90`.                                                                              |
| `google-play`       | No       | `false`   | Android only. Run flows against Google Play devices.                                                                                          |
| `runner-type`       | No       | `default` | Runner type. Options: `default`, `m1`, `m4`. Non-default runners incur premium pricing. See [Runner Types](/configuration/runner-type).       |

See the [Devices & OS Versions](/getting-started/devices-configuration) page for the full availability matrix.

### Test Configuration

| Input             | Required | Default        | Description                                                                                                |
| ----------------- | -------- | -------------- | ---------------------------------------------------------------------------------------------------------- |
| `maestro-version` | No       | —              | Maestro CLI version to run flows with. See [Maestro Versions](/configuration/maestro-versions).            |
| `env`             | No       | —              | Multiline list of environment variables (`KEY=value`) to inject into flows.                                |
| `name`            | No       | Commit message | Custom name for this test run, visible in the console.                                                     |
| `retry`           | No       | `0`            | Number of retries on failure (max `2`). Retries are free — same as pressing retry in the UI.               |
| `report`          | No       | —              | Report format. Options: `junit`, `html`. See [Report Formats](/test-artifacts-and-reports/report-formats). |

### Android-Specific Options

| Input                       | Required | Default | Description                                                                                                                                 |
| --------------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `maestro-chrome-onboarding` | No       | `false` | Force Maestro-based Chrome onboarding. Fixes browser-related crashes but slows tests. See [Chrome Onboarding](/advanced/chrome-onboarding). |
| `android-no-snapshot`       | No       | `false` | Force cold boot instead of snapshot boot. Automatically enabled for API level 35+.                                                          |

### Performance Options

| Input                | Required | Default | Description                                                                                                                                                                      |
| -------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `disable-animations` | No       | `false` | Disable device animations during test execution. On Android, disables system animation scales. On iOS, enables Reduce Motion. Reduces CPU load and may improve test reliability. |

### GitHub / PR Context

The action automatically attaches Git and pull request metadata to each run, read from the GitHub Actions environment. These values are displayed in the DeviceCloud console alongside the results, so you can trace a run back to the commit or PR that triggered it — you don't need to pass branch, commit, or PR values yourself.

| Input                    | Required | Default | Description                                                                                                                                           |
| ------------------------ | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `include-github-context` | No       | `true`  | Automatically attach GitHub/PR context (branch, commit SHA, PR number, PR URL, run ID, repository) to the run as metadata. Set to `false` to opt out. |

### Execution Options

| Input                | Required | Default | Description                                                                                                                                       |
| -------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `async`              | No       | `false` | Exit immediately without waiting for results. Returns exit code `0` regardless of test outcome. See [Async Execution](/advanced/async-execution). |
| `quiet`              | No       | `false` | Reduce console output. Useful in verbose CI environments.                                                                                         |
| `download-artifacts` | No       | —       | Download logs, screenshots, and videos after the run. Options: `ALL`, `FAILED`.                                                                   |
| `json-file`          | No       | `false` | Write test results to a JSON file (`<run_name>_dcd.json` or `<upload_id>_dcd.json`).                                                              |
| `debug`              | No       | `false` | Enable verbose debug output.                                                                                                                      |
| `use-beta`           | No       | `false` | Use the beta version of the DCD CLI.                                                                                                              |

***

## Outputs Reference

Add an `id` to the step to reference its outputs in later steps:

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  id: devicecloud
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app.apk
```

| Output                       | Description                                                                               |
| ---------------------------- | ----------------------------------------------------------------------------------------- |
| `DEVICE_CLOUD_CONSOLE_URL`   | URL to view the test results in the DeviceCloud console.                                  |
| `DEVICE_CLOUD_FLOW_RESULTS`  | JSON array with results for each flow: `[{ "name": "...", "status": "PASSED" }]`.         |
| `DEVICE_CLOUD_UPLOAD_STATUS` | Overall status of the test run: `PENDING`, `RUNNING`, `PASSED`, `FAILED`, or `CANCELLED`. |
| `DEVICE_CLOUD_APP_BINARY_ID` | ID of the uploaded app binary. Reuse this in subsequent jobs to skip re-uploading.        |

***

## Using Outputs

### Post results to Slack

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  id: devicecloud
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app.apk

- name: Post Test Results to Slack
  if: always()
  uses: slackapi/slack-github-action@v1.24.0
  with:
    channel-id: 'test-results'
    slack-message: |
      Test Run Status: ${{ steps.devicecloud.outputs.DEVICE_CLOUD_UPLOAD_STATUS }}
      View Results: ${{ steps.devicecloud.outputs.DEVICE_CLOUD_CONSOLE_URL }}
  env:
    SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
```

### Save results as a workflow artifact

```yaml
- name: Save Test Results
  if: always()
  run: |
    echo '${{ steps.devicecloud.outputs.DEVICE_CLOUD_FLOW_RESULTS }}' > test-results.json

- name: Upload Results
  uses: actions/upload-artifact@v4
  with:
    name: test-results
    path: test-results.json
```

### Reuse the uploaded binary in a later job

```yaml
jobs:
  upload-and-test:
    runs-on: ubuntu-latest
    outputs:
      binary-id: ${{ steps.devicecloud.outputs.DEVICE_CLOUD_APP_BINARY_ID }}
    steps:
      - uses: devicecloud-dev/device-cloud-for-maestro@v2
        id: devicecloud
        with:
          api-key: ${{ secrets.DCD_API_KEY }}
          app-file: app.apk

  test-another-suite:
    needs: upload-and-test
    runs-on: ubuntu-latest
    steps:
      - uses: devicecloud-dev/device-cloud-for-maestro@v2
        with:
          api-key: ${{ secrets.DCD_API_KEY }}
          app-binary-id: ${{ needs.upload-and-test.outputs.binary-id }}
          flows: other-tests/
```

***

## Common Patterns

### Opt out of automatic PR context

Git and PR metadata (branch, commit SHA, PR number/URL, repository, run ID) is attached automatically — see [GitHub / PR Context](#github--pr-context). To turn it off, set `include-github-context: false`:

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app.apk
    include-github-context: false
```

### Run async tests (non-blocking)

Use `async: true` to start tests without blocking your pipeline. Then use [`dcd status`](/cli-reference/dcd-status) to check results separately.

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app.apk
    async: true
    name: ${{ github.sha }}
```

{% hint style="info" %}
With the [DeviceCloud GitHub App](/ci-cd-integration/github-checks) installed, an async run reports back as a pass/fail check on the pull request, so you can gate merges without keeping a runner alive to wait for results.
{% endhint %}

### Filter tests by tag

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app.apk
    include-tags: smoke, critical
    exclude-tags: wip
```

### Pass secrets to flows

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app.apk
    env: |
      USERNAME=${{ secrets.TEST_USERNAME }}
      PASSWORD=${{ secrets.TEST_PASSWORD }}
```

### Save JSON results file

```yaml
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app.apk
    json-file: true
    name: my-run
```

This creates `my-run_dcd.json`:

```json
{
  "uploadId": "abcd1234-5678-efgh-9012-ijklmnopqrst",
  "consoleUrl": "https://console.devicecloud.dev/results?upload=abcd1234-...",
  "appBinaryId": "app-binary-5678",
  "status": "PASSED",
  "flowResults": [
    { "name": "login_test", "status": "PASSED" },
    { "name": "checkout_flow", "status": "PASSED" }
  ]
}
```

***

## Migrating from Maestro Cloud

Switch in one line — replace the `uses` value:

```yaml
# Before
- uses: mobile-dev-inc/action-maestro-cloud@v2
  with:
    api-key: ${{ secrets.MCLOUD_API_KEY }}
    app-file: app.apk

# After
- uses: devicecloud-dev/device-cloud-for-maestro@v2
  with:
    api-key: ${{ secrets.DCD_API_KEY }}
    app-file: app.apk
```

All other inputs are compatible. The only change needed (beyond the `uses` line) is updating your secret name from `MCLOUD_API_KEY` to `DCD_API_KEY` (or whatever name you choose when storing your DeviceCloud API key).


# GitHub Checks

Install the DeviceCloud GitHub App and every test run posts a pass/fail check on the pull request that started it. It works best with the Action's [`async`](/ci-cd-integration/github-actions#execution-options) mode where your workflow submits the run and exits after a few seconds, we run the tests on our own devices, and the result will land on the PR when it's ready. Nothing sits waiting on a runner, so you're not burning CI minutes while the tests execute.

{% hint style="info" %}
You do not need to install our Checks app to use the GitHub Action, but you do need the GitHub Action to use Checks.
{% endhint %}

## How it works

The Action submits your tests and either polls for results or if you set `async: true`, returns straight away. DeviceCloud posts an in-progress check against the PR's commit and updates it to passed or failed once every flow has finished. The check links back to the full run in the console, and because it's an ordinary GitHub check you can require it in branch protection to keep failing builds out of your main branch.

## Connect the App

Open **Settings → Integrations** in the [console](https://console.devicecloud.dev/settings) and click **Connect GitHub**. GitHub asks you to authorize DeviceCloud, and what happens next depends on whether you already have the App:

* **First time.** GitHub sends you on to install the App and choose which repositories it can see. Once you're done you land back on the Integrations page, connected to your current team.
* **Already installed** (on another team, or you removed the connection earlier). We reconnect to your existing installation and return you straight to the console — no reinstall, and no repository step. Your existing repository selection carries over; change it any time with **Manage repositories on GitHub** on the Integrations card.

If you have the App installed on more than one GitHub account or organization, we'll ask which one to connect.

A few things worth knowing:

* Only owners and admins can connect GitHub for a team.
* Your GitHub account needs to be linked to DeviceCloud first, under **Settings → Account**. This is separate from installing the App — it's how we know which DeviceCloud account an installation belongs to.
* If you're an admin on more than one team, you may be asked which one to connect if we can't figure it out automatically using account context.

## Choosing which repositories

Checks only post on repositories the App can access. **Settings → Integrations** shows the current scope — either *All repositories* or a count of selected ones — with a **Manage repositories on GitHub** link that opens the installation's settings, where you can add or remove repositories.

Changes take effect immediately; we track GitHub's repository events, so the console reflects the new scope without you reconnecting.

## Run your tests

Run the [Action](/ci-cd-integration/github-actions) on your pull requests with `async: true`. It attaches the branch, commit, and PR number for you, which is how DeviceCloud knows where to post the check, so ensure you do not disable [`include-github-context`](/ci-cd-integration/github-actions#github--pr-context) (it's enabled by default).

```yaml
on:
  pull_request:
    branches: [main]

jobs:
  mobile-e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # build your app, or download a prebuilt artifact from an earlier job
      - uses: devicecloud-dev/device-cloud-for-maestro@v2
        with:
          api-key: ${{ secrets.DCD_API_KEY }}
          app-file: app/build/outputs/apk/release/app-release.apk
          async: true
```

## Require it before merging

To make the check a merge gate, add it to a branch protection rule: your repo's **Settings → Branches → Require status checks to pass**, then pick **DeviceCloud**. GitHub only lists a check here after it's run at least once, so open a pull request before you set up the rule.

## Re-running failures

A failed check has a **Re-run failed tests** button. It re-runs only the flows that failed and is completely free, just like retries from the CI. It'll move the check back to in-progress until they finish.

## What the check shows

The pass/fail count and total runtime. Below that, a table of every flow with its result, the reason for anything that failed, and a link into the console for the logs, video, and screenshots.

## Managing the connection

**Settings → Integrations** shows the connected account and the repositories the App can reach. Owners and admins can disconnect from there, which always removes the connection and stops the checks. We also try to uninstall the App from GitHub as part of that — if we can't, the console says so and links you to remove it yourself. You can uninstall from your GitHub settings at any time regardless.

If you reconnect later, DeviceCloud picks up your existing installation instead of making you install again.

## When a check doesn't show up

Usually it's one of these:

* **Nothing posts on the PR.** The App isn't installed on that repo, isn't connected to your team, or the run used a different team's API key. Check **Settings → Integrations**, and confirm the repo is in scope under **Manage repositories on GitHub**.
* **Connect GitHub lands on a GitHub 404.** That's a configuration problem on our side, not yours — nothing you can change will fix it. Get in touch and we'll sort it.
* **The check is stuck in progress.** The run probably hasn't finished yet. Look it up in the console; the check only resolves once every flow reaches a final state.
* **"That GitHub account isn't linked."** Link your GitHub identity under **Settings → Account**, then connect.
* **The check isn't in the branch-protection list.** It has to run once on the repo before GitHub will offer it. Open a pull request first.

***

See also [GitHub Actions](/ci-cd-integration/github-actions) and [Async Execution](/advanced/async-execution).


# Bitrise Steps

DeviceCloud includes a BitRise step to allow automatic triggering of tests via your BitRise CI process.

**How to configure the integration:**

1. Edit your workflow in BitRise to add a new step.
2. Search for 'Device Cloud for Maestro' and select the step.

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

3. The step will now appear in your workflow

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

4. Populate the API key using a BitRise secret and set any variables. The step inputs mirror the [`dcd cloud`](/cli-reference/dcd-cloud) flags — see that reference for the full list and accepted values.

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


# EAS Workflows

The DeviceCloud EAS Workflow wrapper is a drop-in alternative to Expo's built-in [`maestro-cloud`](https://docs.expo.dev/eas/workflows/syntax/#maestro-cloud) job type. Run your Maestro flows on DeviceCloud directly from your [EAS Workflow](https://docs.expo.dev/eas/workflows/get-started/) — no need to leave your Expo pipeline.

## Quick Start

```yaml
jobs:
  build_android:
    type: build
    params:
      platform: android
      profile: preview

  e2e:
    needs: [build_android]
    runs_on: linux-medium
    steps:
      - uses: eas/checkout
      - id: download
        uses: eas/download_build
        with:
          build_id: ${{ needs.build_android.outputs.build_id }}
      - id: dcd
        run: |
          npx --yes @devicecloud.dev/eas-workflow@v1 \
            --app-file ${{ steps.download.outputs.artifact_path }} \
            --flows ./.maestro
```

Before running, store your DeviceCloud API key as an EAS project secret:

```bash
eas env:create --scope project --environment production --environment preview --environment development \
  --name DEVICE_CLOUD_API_KEY --visibility secret --type string --value <your-api-key>
```

You can find your API key in the [DeviceCloud console settings](https://console.devicecloud.dev/settings).

{% hint style="info" %}
EAS Workflow custom jobs run on EAS-hosted runners. `linux-medium` is sufficient — the wrapper just hands off to DeviceCloud, which runs your tests on its own device fleet.
{% endhint %}

## Platform Examples

### Android

```yaml
jobs:
  build_android:
    type: build
    params:
      platform: android
      profile: preview

  e2e_android:
    needs: [build_android]
    runs_on: linux-medium
    env:
      DCD_EAS_BUILD_ID: ${{ needs.build_android.outputs.build_id }}
      DCD_EAS_PLATFORM: ${{ needs.build_android.outputs.platform }}
      DCD_EAS_APP_VERSION: ${{ needs.build_android.outputs.app_version }}
      DCD_GH_SHA: ${{ github.sha }}
      DCD_GH_BRANCH: ${{ github.ref_name }}
    outputs:
      console_url: ${{ steps.dcd.outputs.console_url }}
      status: ${{ steps.dcd.outputs.upload_status }}
      flow_results: ${{ steps.dcd.outputs.flow_results }}
    steps:
      - uses: eas/checkout
      - id: download
        uses: eas/download_build
        with:
          build_id: ${{ needs.build_android.outputs.build_id }}
      - id: dcd
        run: |
          npx --yes @devicecloud.dev/eas-workflow@v1 \
            --app-file ${{ steps.download.outputs.artifact_path }} \
            --flows ./.maestro \
            --android-device pixel-7 \
            --android-api-level 34
```

### iOS

```yaml
jobs:
  build_ios:
    type: build
    params:
      platform: ios
      profile: preview

  e2e_ios:
    needs: [build_ios]
    runs_on: linux-medium
    env:
      DCD_EAS_BUILD_ID: ${{ needs.build_ios.outputs.build_id }}
      DCD_EAS_PLATFORM: ${{ needs.build_ios.outputs.platform }}
      DCD_EAS_APP_VERSION: ${{ needs.build_ios.outputs.app_version }}
      DCD_GH_SHA: ${{ github.sha }}
      DCD_GH_BRANCH: ${{ github.ref_name }}
    outputs:
      console_url: ${{ steps.dcd.outputs.console_url }}
      status: ${{ steps.dcd.outputs.upload_status }}
      flow_results: ${{ steps.dcd.outputs.flow_results }}
    steps:
      - uses: eas/checkout
      - id: download
        uses: eas/download_build
        with:
          build_id: ${{ needs.build_ios.outputs.build_id }}
      - id: dcd
        run: |
          npx --yes @devicecloud.dev/eas-workflow@v1 \
            --app-file ${{ steps.download.outputs.artifact_path }} \
            --flows ./.maestro \
            --ios-device iphone-16 \
            --ios-version 18
```

{% hint style="info" %}
You don't need a macOS runner for iOS — DeviceCloud runs iOS simulators on its own Mac fleet.
{% endhint %}

***

## Environment Variables

EAS context that doesn't change per-flow goes through the **job-level** `env:` block. Step-level `env:` may be silently ignored — keep `env:` on the job.

The wrapper turns these into either DCD flags or `--metadata` tags (so each run is searchable in the console).

### Provided by EAS automatically

| Variable               | Source                               | Purpose                                               |
| ---------------------- | ------------------------------------ | ----------------------------------------------------- |
| `DEVICE_CLOUD_API_KEY` | `eas env:create --visibility secret` | DCD `--api-key` (required). Auto-injected at runtime. |

### Build context (recommended)

| Variable              | YAML source                                    | Tagged as                  |
| --------------------- | ---------------------------------------------- | -------------------------- |
| `DCD_EAS_BUILD_ID`    | `${{ needs.<build_job>.outputs.build_id }}`    | `eas_build_id` metadata    |
| `DCD_EAS_PLATFORM`    | `${{ needs.<build_job>.outputs.platform }}`    | `eas_platform` metadata    |
| `DCD_EAS_APP_VERSION` | `${{ needs.<build_job>.outputs.app_version }}` | `eas_app_version` metadata |
| `DCD_EAS_PROFILE`     | `${{ needs.<build_job>.outputs.profile }}`     | `eas_profile` metadata     |

The build artifact itself is downloaded by `eas/download_build` and passed via `--app-file` — there is no `build_url` output on EAS Workflow build jobs.

### Git context (optional)

| Variable           | YAML source                                 | Tagged as               |
| ------------------ | ------------------------------------------- | ----------------------- |
| `DCD_GH_SHA`       | `${{ github.sha }}`                         | `gh_sha` metadata       |
| `DCD_GH_BRANCH`    | `${{ github.ref_name }}`                    | `gh_branch` metadata    |
| `DCD_GH_RUN_ID`    | `${{ github.run_id }}`                      | `gh_run_id` metadata    |
| `DCD_GH_PR_NUMBER` | `${{ github.event.pull_request.number }}`   | `gh_pr_number` metadata |
| `DCD_GH_PR_URL`    | `${{ github.event.pull_request.html_url }}` | `gh_pr_url` metadata    |
| `DCD_GH_REPO`      | `${{ github.repository }}`                  | `gh_repo` metadata      |

{% hint style="warning" %}
`${{ github.event.pull_request.number }}`, `${{ github.event.pull_request.html_url }}` and `${{ github.repository }}` resolve to `null` on manual triggers and EAS rejects them as invalid env values. Only set the PR and repo variables inside an `if:` guard that limits the job to PR events, or omit them. `github.sha`, `github.ref_name` and `github.run_id` coerce to empty strings safely.
{% endhint %}

### Advanced

| Variable               | Default                       | Description                                                                                                                                                           |
| ---------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DEVICE_CLOUD_API_URL` | `https://api.devicecloud.dev` | Override the API URL (staging/dev environments).                                                                                                                      |
| `DCD_USE_BETA`         | `false`                       | Set to the string `true` to use the beta DCD CLI (any other value is treated as `false`).                                                                             |
| `DCD_EAS_BUILD_URL`    | —                             | Supply a build URL directly (mapped to `dcd cloud --app-url`) instead of downloading the artifact and passing `--app-file`. Useful when you host the binary yourself. |

***

## CLI Flags

Anything you pass on the command line after `npx @devicecloud.dev/eas-workflow@v1 ...` is forwarded verbatim to [`dcd cloud`](/cli-reference/dcd-cloud).

### App source

| Flag                   | Description                                                                             |
| ---------------------- | --------------------------------------------------------------------------------------- |
| `--app-file <path>`    | Path to the downloaded build (typically `${{ steps.download.outputs.artifact_path }}`). |
| `--app-binary-id <id>` | Reuse a previously uploaded binary instead of re-uploading.                             |
| `--ignore-sha-check`   | Skip the duplicate-upload SHA check. Not recommended.                                   |

### Flow Selection

| Flag                     | Description                                               |
| ------------------------ | --------------------------------------------------------- |
| `--flows <path>`         | Path to a flow file or directory. Default: `./.maestro/`. |
| `--include-tags <tags>`  | Only run flows with these Maestro tags (comma-separated). |
| `--exclude-tags <tags>`  | Exclude flows with these tags.                            |
| `--exclude-flows <path>` | Subdirectories to exclude.                                |
| `--config <path>`        | Path to a custom Maestro config file.                     |

### Device Configuration

| Flag                       | Description                                                                                                |
| -------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `--android-device <model>` | `pixel-6`, `pixel-6-pro`, `pixel-7`, `pixel-7-pro`.                                                        |
| `--android-api-level <n>`  | `29` – `36`. Default `34`.                                                                                 |
| `--ios-device <model>`     | `iphone-14`, `iphone-15`, `iphone-16`, `iphone-16-pro`, `iphone-16-pro-max`, `ipad-pro-6th-gen`.           |
| `--ios-version <n>`        | `17`, `18`, `26`. Default `17`.                                                                            |
| `--device-locale <code>`   | E.g. `de_DE`. See [Device Locale](/configuration/device-locale).                                           |
| `--orientation <deg>`      | Android only. `0`, `90`, `180`, `270`.                                                                     |
| `--google-play`            | Android only. Run on Google Play devices.                                                                  |
| `--runner-type <type>`     | `default`, `m1`, `m4`. Non-default incurs premium pricing. See [Runner Types](/configuration/runner-type). |

See the [Devices & OS Versions](/getting-started/devices-configuration) page for the full availability matrix.

### Test Configuration

| Flag                         | Description                                                                                                   |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `--maestro-version <semver>` | Maestro CLI version. See [Maestro Versions](/configuration/maestro-versions).                                 |
| `--env KEY=value`            | Inject environment variables into your flows. Repeatable.                                                     |
| `--name <name>`              | Custom name for this test run.                                                                                |
| `--retry <n>`                | Number of automatic retries on failure (max `2`). Retries are free.                                           |
| `--report <format>`          | `junit`, `html`, `html-detailed`, `allure`. See [Report Formats](/test-artifacts-and-reports/report-formats). |

### Execution Options

| Flag                          | Description                                                                                                                |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `--async`                     | Exit immediately without waiting for results (exit code `0` regardless). See [Async Execution](/advanced/async-execution). |
| `--download-artifacts <mode>` | Download logs/screenshots/videos. Options: `ALL`, `FAILED`.                                                                |
| `--disable-animations`        | Disable device animations. See [Animations](/configuration/disable-animations).                                            |
| `--maestro-chrome-onboarding` | Android only. See [Chrome Onboarding](/advanced/chrome-onboarding).                                                        |
| `--android-no-snapshot`       | Force cold boot. Auto-enabled for API 35+.                                                                                 |
| `--debug`                     | Verbose debug output.                                                                                                      |

Full CLI reference: [CLI: Cloud](/cli-reference/dcd-cloud).

***

## Outputs

The wrapper emits these as EAS step outputs via `set-output` after the run completes.

| Output          | Description                                                               |
| --------------- | ------------------------------------------------------------------------- |
| `console_url`   | URL to view the test results in the DeviceCloud console.                  |
| `upload_status` | Final status: `PENDING`, `RUNNING`, `PASSED`, `FAILED`, or `CANCELLED`.   |
| `flow_results`  | JSON array: `[{ "name": "...", "status": "PASSED" }]`.                    |
| `app_binary_id` | ID of the uploaded binary. Reuse via `--app-binary-id` to skip re-upload. |

### Using outputs in a downstream job

```yaml
jobs:
  e2e:
    needs: [build_android]
    runs_on: linux-medium
    outputs:
      console_url: ${{ steps.dcd.outputs.console_url }}
      status: ${{ steps.dcd.outputs.upload_status }}
    steps:
      - uses: eas/checkout
      - id: download
        uses: eas/download_build
        with:
          build_id: ${{ needs.build_android.outputs.build_id }}
      - id: dcd
        run: |
          npx --yes @devicecloud.dev/eas-workflow@v1 \
            --app-file ${{ steps.download.outputs.artifact_path }} \
            --flows ./.maestro

  notify:
    needs: [e2e]
    steps:
      - run: |
          echo "Status: ${{ needs.e2e.outputs.status }}"
          echo "Results: ${{ needs.e2e.outputs.console_url }}"
```

***

## Exit Codes

| Code | Meaning                                                                                |
| ---- | -------------------------------------------------------------------------------------- |
| `0`  | All flows passed, or run started successfully in async mode.                           |
| `1`  | At least one flow failed, the run was cancelled, or the wrapper hit an internal error. |

See [Exit Codes](/advanced/exit-codes) for the full list.

***

## Common Patterns

### Run on every PR

```yaml
on:
  pull_request: {}

jobs:
  build_android:
    type: build
    params:
      platform: android
      profile: preview

  e2e:
    needs: [build_android]
    runs_on: linux-medium
    env:
      DCD_EAS_BUILD_ID: ${{ needs.build_android.outputs.build_id }}
      DCD_GH_SHA: ${{ github.sha }}
    steps:
      - uses: eas/checkout
      - id: download
        uses: eas/download_build
        with:
          build_id: ${{ needs.build_android.outputs.build_id }}
      - run: |
          npx --yes @devicecloud.dev/eas-workflow@v1 \
            --app-file ${{ steps.download.outputs.artifact_path }} \
            --flows ./.maestro
```

### Run async (non-blocking)

```yaml
- run: |
    npx --yes @devicecloud.dev/eas-workflow@v1 \
      --app-file ${{ steps.download.outputs.artifact_path }} \
      --flows ./.maestro \
      --async \
      --name ${{ github.sha }}
```

### Pass secrets into flows

Set the secret as an EAS env var first:

```bash
eas env:create --scope project --environment production \
  --name TEST_USERNAME --visibility secret --type string --value <value>
```

Then reference it as a regular env var in the wrapper invocation:

```yaml
- run: |
    npx --yes @devicecloud.dev/eas-workflow@v1 \
      --app-file ${{ steps.download.outputs.artifact_path }} \
      --flows ./.maestro \
      --env "USERNAME=$TEST_USERNAME" \
      --env "PASSWORD=$TEST_PASSWORD"
```

### Filter by tag

```yaml
- run: |
    npx --yes @devicecloud.dev/eas-workflow@v1 \
      --flows ./.maestro \
      --app-file ${{ steps.download.outputs.artifact_path }} \
      --include-tags smoke,critical \
      --exclude-tags wip
```

***

## Migrating from `maestro-cloud`

EAS's built-in `maestro-cloud` job is closed-source and hardcoded to Maestro Cloud. To switch to DeviceCloud, replace the whole job with a custom job that calls the wrapper:

```yaml
# Before
e2e:
  type: maestro-cloud
  params:
    build_id: ${{ needs.build.outputs.build_id }}
    maestro_project_id: proj_xxx
    flows: ./.maestro
    maestro_api_key: ${{ secrets.MAESTRO_CLOUD_API_KEY }}

# After
e2e:
  needs: [build]
  runs_on: linux-medium
  steps:
    - uses: eas/checkout
    - id: download
      uses: eas/download_build
      with:
        build_id: ${{ needs.build.outputs.build_id }}
    - run: |
        npx --yes @devicecloud.dev/eas-workflow@v1 \
          --app-file ${{ steps.download.outputs.artifact_path }} \
          --flows ./.maestro
```

Then store your DeviceCloud API key as a project secret (as shown in [Quick Start](#quick-start)).

## Source

The wrapper is open source and can be found here: [github.com/devicecloud-dev/device-cloud-for-eas](https://github.com/devicecloud-dev/device-cloud-for-eas).


# Bitbucket Pipelines

DeviceCloud ships a [Bitbucket Pipe](https://support.atlassian.com/bitbucket-cloud/docs/pipes/) that runs your Maestro flows on devicecloud.dev directly from your Bitbucket Pipelines.

## Quick Start

Add the pipe to a step in your `bitbucket-pipelines.yml`:

```yaml
image: atlassian/default-image:4

pipelines:
  default:
    - step:
        name: E2E tests
        script:
          - pipe: docker://moropo/device-cloud-for-bitbucket:1.2.0
            variables:
              API_KEY: $DEVICE_CLOUD_API_KEY
              APP_FILE: 'build/app-release.apk'
              WORKSPACE: '.maestro'
              ANDROID_DEVICE: 'pixel-6'
              ANDROID_API_LEVEL: '34'
```

Store your DeviceCloud API key as a **Secured Repository variable** in *Repository settings → Repository variables*. Reference it as `$DEVICE_CLOUD_API_KEY` and pass it to the pipe as `API_KEY`.

Find your API key at [console.devicecloud.dev/settings](https://console.devicecloud.dev/settings).

## Platform Examples

### Android

```yaml
- pipe: docker://moropo/device-cloud-for-bitbucket:1.2.0
  variables:
    API_KEY: $DEVICE_CLOUD_API_KEY
    APP_FILE: 'build/app-release.apk'
    WORKSPACE: '.maestro'
    ANDROID_DEVICE: 'pixel-7'
    ANDROID_API_LEVEL: '34'
    REPORT: 'junit'
```

### iOS

```yaml
- pipe: docker://moropo/device-cloud-for-bitbucket:1.2.0
  variables:
    API_KEY: $DEVICE_CLOUD_API_KEY
    APP_FILE: 'build/MyApp.app.zip'
    WORKSPACE: '.maestro'
    IOS_DEVICE: 'iphone-16-pro'
    IOS_VERSION: '18'
```

## Variables

The pipe variables map 1:1 to the [`dcd cloud`](/cli-reference/dcd-cloud) CLI flags. The full list lives in the pipe's [README](https://bitbucket.org/devicecloud-dev/device-cloud-for-bitbucket/src/main/README.md). Required: `API_KEY`. Common ones:

| Variable                              | Description                                                 |
| ------------------------------------- | ----------------------------------------------------------- |
| `API_KEY`                             | DeviceCloud API key (required, secured).                    |
| `APP_FILE`                            | Path to the app binary to upload.                           |
| `APP_BINARY_ID`                       | Reuse a previously uploaded binary.                         |
| `WORKSPACE` / `FLOWS`                 | Path to your Maestro flows folder/file.                     |
| `ANDROID_DEVICE`, `ANDROID_API_LEVEL` | Android device + API level.                                 |
| `IOS_DEVICE`, `IOS_VERSION`           | iOS device + version.                                       |
| `ENV_LIST`                            | Newline-separated `KEY=VALUE` env vars injected into flows. |
| `INCLUDE_TAGS` / `EXCLUDE_TAGS`       | Filter flows by Maestro tag.                                |
| `REPORT`                              | `junit`, `html`, `html-detailed`, `allure`.                 |
| `DOWNLOAD_ARTIFACTS`                  | `ALL` or `FAILED` — downloads logs/screenshots/videos.      |
| `ASYNC`                               | `"true"` to fire-and-forget.                                |
| `RUNNER_TYPE`                         | `m1` or `m4` for premium runners.                           |

## Bitbucket context auto-attached

The pipe auto-attaches Bitbucket context (commit SHA, repo, branch, PR number) as metadata on every run and are included on the Console UI so you know what triggered each run.

If you would prefer not to include this metadata, set `INCLUDE_BITBUCKET_CONTEXT: 'false'`.

## Outputs

The pipe writes a `dcd-result.env` file into the repo's working directory. Subsequent script lines in the same step can `source` it:

```yaml
- pipe: docker://moropo/device-cloud-for-bitbucket:1.2.0
  variables:
    API_KEY: $DEVICE_CLOUD_API_KEY
    APP_FILE: 'app.apk'
    WORKSPACE: '.maestro'

- source ./dcd-result.env
- echo "Console: $DEVICE_CLOUD_CONSOLE_URL"
- echo "Status:  $DEVICE_CLOUD_UPLOAD_STATUS"
```

Exported: `DEVICE_CLOUD_CONSOLE_URL`, `DEVICE_CLOUD_UPLOAD_STATUS`, `DEVICE_CLOUD_FLOW_RESULTS`, `DEVICE_CLOUD_APP_BINARY_ID`, `DEVICE_CLOUD_UPLOAD_ID`.

The pipe exits non-zero on test failures so the build fails by default.

## Passing env vars into flows

```yaml
- pipe: docker://moropo/device-cloud-for-bitbucket:1.2.0
  variables:
    API_KEY: $DEVICE_CLOUD_API_KEY
    APP_FILE: 'app.apk'
    WORKSPACE: '.maestro'
    ENV_LIST: |
      USERNAME=test@example.com
      PASSWORD=$TEST_PASSWORD
      ENVIRONMENT=staging
```

`$TEST_PASSWORD` is interpolated by Bitbucket from your repo variables before the pipe runs.

## Reports as build artifacts

To expose a report as a Bitbucket artifact (and have Bitbucket pick up the test results UI), declare it on the step. For example, if you would like a JUnit report, set up your pipe like this:

```yaml
- step:
    name: E2E tests
    script:
      - pipe: docker://moropo/device-cloud-for-bitbucket:1.2.0
        variables:
          API_KEY: $DEVICE_CLOUD_API_KEY
          APP_FILE: 'app.apk'
          WORKSPACE: '.maestro'
          REPORT: 'junit'
    artifacts:
      - report.xml
```

## Source

[bitbucket.org/devicecloud-dev/device-cloud-for-bitbucket](https://bitbucket.org/devicecloud-dev/device-cloud-for-bitbucket).


# Any CI

As DeviceCloud uses a simple CLI, you can run it in any CI service using a basic script in bash (or any lang).

We recommend using npx so you don't need to install the npm package. In CI, authenticate with an [API key](/getting-started/api-keys) — set `DEVICE_CLOUD_API_KEY` as a secret, or pass `--api-key`. For example:

```bash
npx --yes @devicecloud.dev/dcd@latest cloud sample.zip ios-flow.yaml --quiet --api-key ${{ secrets.DEV_DCD_API_KEY }}
```

If you need advice - please ask in [Discord](https://discord.gg/gm3mJwcNw8), we'd be happy to provide pointers.

Want a specific CI integration build? Please request it in [GitHub Discussions](https://github.com/orgs/devicecloud-dev/discussions)


# App Management

Device Cloud provides several ways to manage your application binaries during test execution.

## App Binary Options

You have two ways to specify your application binary:

1. **Direct File Upload**

```bash
dcd cloud --app-file ./path/to/your/app.apk flow.yaml
```

2. **Using Previously Uploaded Binary**

```bash
dcd cloud --app-binary-id abc123 flow.yaml
```

## Binary Management

### SHA Hash Checking

By default, Device Cloud checks the SHA hash of your binary to avoid redundant uploads (it will automatically work out the app-binary-id for you). To override:

```bash
dcd cloud --app-file ./app.apk --ignore-sha-check
```

Use this when:

* You need to force a re-upload of the same binary
* You're experiencing hash verification issues


# Environment Variables

Device Cloud allows you to inject environment variables into your test flows, enabling dynamic configuration and secure credential management.

## Setting Environment Variables

### Command Line

```bash
dcd cloud -e API_URL=https://api.example.com -e API_KEY=secret
```

### Multiple Variables

```bash
dcd cloud -e DB_HOST=localhost \
          -e DB_USER=test \
          -e DB_PASS=password \
          -e ENVIRONMENT=staging
```

## Use Cases

### Configuration Management

```bash
# Development
dcd cloud -e API_URL=https://dev-api.example.com

# Staging
dcd cloud -e API_URL=https://staging-api.example.com

# Production
dcd cloud -e API_URL=https://api.example.com
```

### Secure Credentials

```bash
dcd cloud -e USERNAME=${CI_USERNAME} \
          -e PASSWORD=${CI_PASSWORD}
```

## iOS: SIMCTL Variable Passthrough

On iOS, variables prefixed with `SIMCTL_CHILD_` are passed through by the simulator directly to the app under test, with the prefix stripped. This allows you to inject values that are readable from inside the app via `ProcessInfo.processInfo.environment`.

```bash
dcd cloud app.zip flow.yaml -e SIMCTL_CHILD_API_URL=https://staging-api.example.com
```

Inside the iOS app, this is accessible as `API_URL`:

```swift
let apiUrl = ProcessInfo.processInfo.environment["API_URL"]
```

This is useful for feature flags, environment switching, or any value your app reads at launch without needing to rebuild the binary.

## Best Practices

* Never commit sensitive values
* Use CI/CD secrets
* Use UPPERCASE for variable names
* Use descriptive names
* Name secret-bearing variables with a keyword like `PASSWORD`, `TOKEN`, `SECRET`, or `API_KEY` so their values are automatically masked in logs and artifacts — see [Inspecting Variables](/test-artifacts-and-reports/inspecting-variables)


# Workspace Configuration

A `config.yaml` (or `config.yml`) file lets you configure how the CLI discovers, filters, and runs your flows without needing to pass every option on the command line. It is recommended for any project with more than a handful of flows.

## Auto-detection

Place your `config.yaml` file at the root of the directory you pass to `dcd cloud` and the CLI will pick it up automatically.

```bash
dcd cloud app.apk flows/        # flows/config.yaml is loaded automatically
```

## Custom path

Use `--config` to load a config file from a non-standard location:

```bash
dcd cloud app.apk flows/ --config ci/workspace.yaml
```

## Fields

### `flows`

Glob patterns that select which flow files to run. Accepts a list of patterns using the [NPM glob](https://www.npmjs.com/package/glob) syntax.

```yaml
flows:
  - ./**/*.yaml        # all YAML files recursively
  - ./smoke/*.yaml     # only files inside smoke/
```

Files named `config.yaml` / `config.yml` and paths containing `.app` path segments are always excluded regardless of the pattern.

If `flows` is omitted, all `.yaml` / `.yml` files in the directory (except config files) are included.

### `includeTags` / `excludeTags`

Filter flows by their Maestro `tags`. Values here are **merged** with any tags set using the CLI flags.

```yaml
includeTags:
  - smoke
excludeTags:
  - slow
  - wip
```

### `executionOrder`

Run a subset of flows sequentially (in order) before the remaining flows run in parallel.

```yaml
executionOrder:
  continueOnFailure: false
  flowsOrder:
    - login         # matches flow with name: "login" or file login.yaml
    - checkout
    - payment
```

* `flowsOrder` — list of flow names to run in sequence. A name matches either the `name:` field inside the flow YAML or the filename without extension.
* `continueOnFailure` — if `true`, subsequent flows in the sequence run even if an earlier one fails. Defaults to `false`.

{% hint style="info" %}
If a flow name in `flowsOrder` doesn't match any discovered flow (e.g. after tag filtering), the CLI will emit a warning and lists the available names to help diagnose the mismatch.
{% endhint %}

### `notifications`

Send email notifications when a run completes. See [Email Notifications](/notifications/email-notifications) for full context.

```yaml
notifications:
  email:
    enabled: true
    onSuccess: false   # set to true to also notify on passing runs
    recipients:
      - team@example.com
      - ci-alerts@example.com
```

### `platform`

Per-platform settings. Currently supports disabling animations, which is equivalent to passing `--disable-animations` on the CLI but lets you control each platform independently. The CLI flag takes precedence if both are set.

```yaml
platform:
  android:
    disableAnimations: true   # disables system animation scales
  ios:
    disableAnimations: true   # enables Reduce Motion on the simulator
```

## Full example

```yaml
flows:
  - ./**/*.yaml

includeTags:
  - smoke

excludeTags:
  - wip

executionOrder:
  continueOnFailure: false
  flowsOrder:
    - login
    - onboarding

notifications:
  email:
    enabled: true
    onSuccess: false
    recipients:
      - team@example.com

platform:
  android:
    disableAnimations: true
  ios:
    disableAnimations: true
```


# Per-flow Devices

By default every flow in an upload runs on the same device, set by the `--ios-device`, `--ios-version`, `--android-device` and `--android-api-level` flags on `dcd cloud`.

Sometimes a single flow only matters on one device for example a flow you must verify on a tablet, or a flow that has to work on your oldest supported OS. Rather than running a separate upload per flow, a flow can declare the device it needs directly in its YAML.

A flow that declares a device runs as normal on that device. Flows that declare nothing inherit the upload's device, exactly as before. Your run count does not change.

## Usage

Set the device in the flow's `env:` block. You can find a list of supported devices and operating systems [here](/getting-started/devices-configuration).

### iOS

```yaml
# checkout-tablet.yaml
appId: my.app
env:
    DEVICECLOUD_OVERRIDE_IOS_DEVICE: ipad-pro-6th-gen
    DEVICECLOUD_OVERRIDE_IOS_VERSION: "26"
---
# test steps
```

### Android

```yaml
# onboarding-oldest-os.yaml
appId: my.app
env:
    DEVICECLOUD_OVERRIDE_ANDROID_DEVICE: pixel-6
    DEVICECLOUD_OVERRIDE_ANDROID_API_LEVEL: "30"
---
# test steps
```

Either key may be set on its own. A flow that sets only `DEVICECLOUD_OVERRIDE_IOS_DEVICE` keeps the upload's iOS version, and vice versa.

## Precedence

1. `DEVICECLOUD_OVERRIDE_*` in the flow's YAML (per test)
2. The upload-wide `--ios-device` / `--ios-version` / `--android-device` / `--android-api-level` flags
3. Default: iPhone 14 on iOS 17, or Pixel 7 on API level 34

## Billing

Each flow still runs once, so your total run count is unchanged. What can change is a flow's **rate**: iPad and Google Play flows are charged at our advanced rate as per our [pricing](/billing/test-run-billing). Targeting an iPad from one flow in an otherwise iPhone upload charges that one flow at the advanced rate, and the rest at the standard rate.

## Rules

* Device and OS must be a supported combination, including Google Play if applicable.
* You cannot target across platforms in the same upload.
* Currently not supported on `m1` runners due to device limitations.

## Running a suite across several devices

Per-flow targeting picks a device for *one* flow. To run your **entire** suite against several devices, use a [device matrix](/configuration/device-matrix) — every flow runs once per device, under a single upload.


# Device Matrix

By default every flow in an upload runs on one device, set by the `--ios-device`, `--ios-version`, `--android-device` and `--android-api-level` flags on `dcd cloud`.

A **device matrix** runs your whole suite against several devices from a single `dcd cloud` invocation. Every flow runs once per device, all under one upload with one console entry.

## Usage

Pass `--ios-device-matrix <device>:<version>` (or `--android-device-matrix <device>:<apiLevel>`) once per device you want. Supported devices and operating systems are listed [here](/getting-started/devices-configuration).

### iOS

```bash
dcd cloud --app-binary-id <id> ./flows \
  --ios-device-matrix iphone-16-pro:18 \
  --ios-device-matrix iphone-16-pro:26 \
  --ios-device-matrix iphone-16-pro-max:26
# one upload, 3 devices x N flows
```

### Android

```bash
dcd cloud --app-binary-id <id> ./flows \
  --android-device-matrix pixel-7:34 \
  --android-device-matrix pixel-6:33
```

Append `:play` to run a cell against a Google Play device:

```bash
dcd cloud --app-binary-id <id> ./flows \
  --android-device-matrix pixel-7:34 \
  --android-device-matrix pixel-7:34:play
```

Every `--ios-device-matrix` / `--android-device-matrix` names **exactly one device**. There is no cross-product: the flags are not combined with each other, or with `--ios-device` / `--ios-version`.

```bash
# runs exactly these two, and nothing else
dcd cloud [...] --ios-device-matrix iphone-15:17 --ios-device-matrix iphone-16-plus:26
```

This is deliberate as not every device supports every OS. Naming each device explicitly means you always get exactly what you asked for.

## Cost

A matrix runs one test per (flow x device), so a 4-device matrix over 10 flows is **40 flows**, not 10. Before submitting, the CLI prints the number of flows and what they will cost:

```
Device matrix
  cells       4
  est. cost   $0.32
  Pixel 7 - API 34   2 flows - $0.16
  Pixel 6 - API 33   2 flows - $0.16
```

Rates are per device — iPad and Google Play flows are charged at the advanced rate, as per our [pricing](/billing/test-run-billing).

## Results

Each device gets its own result, grouped together in the console so you can compare a flow across devices at a glance.

Under `--json`, every entry in `tests[]` carries a `device` object, so the same flow run on two devices is unambiguous:

```json
{
  "uploadId": "...",
  "tests": [
    { "name": "login.yaml", "status": "PASSED", "device": { "name": "Pixel 7", "osVersion": "34" } },
    { "name": "login.yaml", "status": "FAILED", "device": { "name": "Pixel 6", "osVersion": "33" } }
  ]
}
```

Without `--async` the run waits for **every** device and exits `0` only if all of them passed. Retrying a single failed result re-runs just that one flow on that one device and not the entire matrix.

## Rules

* Every device and OS must be a supported combination, including Google Play if applicable. An unsupported one is rejected before anything runs so you never get a partial submission.
* One platform per upload. An upload runs one app binary, so `--ios-device-matrix` and `--android-device-matrix` cannot be combined in the same run.
* A flow that names its own device wins. If a flow targets a device in its YAML via [per-flow devices](/configuration/per-flow-devices), it runs once on that device and is excluded from the matrix. Ten flows across four devices with one targeted flow is 37 runs, not 40.


# Device Locale

Set device language and region for localization testing.

{% hint style="info" %}
The default locale for Android is `en_US` . The default locale for iOS is currently `en_GB` but will be changing to `en_US` later in 2026.
{% endhint %}

## Format

`{language_code}_{COUNTRY_CODE}`

* Language: ISO-639-1 (lowercase)
* Country: ISO-3166-1 (uppercase)

## Usage

To set locale for the whole upload:

```bash
# German
dcd cloud ... --device-locale de_DE

# With device selection
dcd cloud ... --android-device pixel-6 --device-locale ja_JP
```

To override locale for a specific test, pass in the DeviceCloud env variable. This will override the upload setting above, but just for a single test:

```yaml
# in your test.yaml
appId: my.app
env:
    DEVICECLOUD_OVERRIDE_DEVICE_LOCALE: de_DE
---
# test steps
```

## Common Locales

### European

* `en_GB`: English (UK)
* `de_DE`: German
* `fr_FR`: French
* `it_IT`: Italian

### Asian

* `zh_CN`: Chinese
* `ja_JP`: Japanese
* `ko_KR`: Korean

### RTL

* `ar_SA`: Arabic
* `he_IL`: Hebrew


# Device Date & Time

Run a flow with the device clock set to a chosen date and time, useful for testing expiry logic, subscription renewals, time‑gated features, free‑trial countdowns, or other date‑specific UI (birthdays, holidays, "new this week" etc).

By default our runners take the current time when booting. Our Android runners run UTC and our iOS runners match GMT/BST in accordance with Daylight Savings Time.

## Usage

To set the time for a flow, set a DeviceCloud env variable in the flow's YAML. The value is an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date‑time string.

```yaml
# in your test.yaml
appId: my.app
env:
    DEVICECLOUD_OVERRIDE_DEVICE_DATETIME: "2030-01-01T09:00:00Z"
    DEVICECLOUD_OVERRIDE_DEVICE_TIMEZONE: "America/New_York"   # optional
---
# test steps
```

* `DEVICECLOUD_OVERRIDE_DEVICE_DATETIME` — target date/time, ISO 8601 (e.g. `2030-01-01T09:00:00Z`).
* `DEVICECLOUD_OVERRIDE_DEVICE_TIMEZONE` — optional [IANA timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g. `Europe/London`). Android only.

If the value can't be parsed, it is ignored and the flow runs with the default (real) time.

Please note that due to platform limitations, this behaves very differently on Android and iOS. On Android it sets the real device clock, so the app sees the new time. On iOS it only changes the cosmetic status‑bar clock — the app's own `Date()` is unaffected as iOS simulators share their host kernel. Read the platform notes below before relying on it.

## Semantics

The override sets the device's starting clock. The clock then ticks forward normally from there for the rest of the run as it is not a frozen clock. If you need time to stand still, you'll need to drive it from within your app/test instead.

## Android

Sets the real emulator clock, so `Date.now()`, `System.currentTimeMillis()`, date pickers, and anything else the app reads all see the new time. Auto‑time sync is disabled for the run so the value sticks.

{% hint style="info" %}
Please note that this is not available on Google Play images. [Google Play](/configuration/google-play-apis) emulators are not rootable, so the clock cannot be set. Google Play flows will log a warning and continue on real time. Use the standard image if you need the date/time override.
{% endhint %}

## iOS

iOS simulators read the host machine's clock and there is no per‑simulator API to change it, so:

* The app's `Date()` / `NSDate` is not changed — your app logic will still see the real time. iOS cannot time‑travel app code.
* DeviceCloud applies a cosmetic status‑bar time only via the simulator status‑bar override.
* Apple's tooling only accepts whole `HH:MM` times and rejects a zero minute, so a requested time on the hour (e.g. `09:00`) is shown as `09:01`, and midnight (`00:xx`) is left at the real time. You can workaround this by setting the time one minute before you need, for example if you need `09:00`, you can set `08:58` and in most cases the simulator will have been running for long enough to increment the clock.

If you need real time‑travel on iOS, control the date from inside your app (e.g. a debug/launch argument) rather than relying on this override.

## Precedence

1. `DEVICECLOUD_OVERRIDE_DEVICE_DATETIME` (per test)
2. Default: the real current device time


# Animations

You can turn off device animations during a test run. On Android this works by zeroing the three system animation scales (`window`, `transition`, `animator`) via adb. On iOS this enables the **Reduce Motion** accessibility setting.

## Usage

### Whole upload

Pass the flag on `dcd cloud`:

```bash
dcd cloud ... --disable-animations
```

Or set it once in the workspace `config.yaml`:

```yaml
platform:
  android:
    disableAnimations: true
  ios:
    disableAnimations: true
```

### Per test (both platforms)

Override the upload-wide setting for a single flow via the DeviceCloud env variable:

```yaml
# in your test.yaml
appId: my.app
env:
    DEVICECLOUD_OVERRIDE_DISABLE_ANIMATIONS: true
---
# test steps
```

### Per test, platform-specific

When the same flow YAML runs on both Android and iOS, you can target each platform independently. Use this when animations cause problems on one platform but you want them on for the other (for example, animations off on Android to fix scroll/tap flakiness, but on for iOS where Reduce Motion changes app appearance):

```yaml
# in your test.yaml
appId: my.app
env:
    DEVICECLOUD_OVERRIDE_ANDROID_DISABLE_ANIMATIONS: true
    DEVICECLOUD_OVERRIDE_IOS_DISABLE_ANIMATIONS: false
---
# test steps
```

The other platform's key is ignored — only the one matching the run's platform is consulted.

## Precedence

When more than one is set, the most specific wins:

1. `DEVICECLOUD_OVERRIDE_<PLATFORM>_DISABLE_ANIMATIONS` (per test, this platform)
2. `DEVICECLOUD_OVERRIDE_DISABLE_ANIMATIONS` (per test, both platforms)
3. The upload-wide `--disable-animations` flag or `config.yaml`
4. Default: animations are **on**

A `false` override is honoured the same as `true` — a test can opt out even when the upload-wide flag is set.


# Device Orientation

Control Android device orientation during test execution.

> **Note**: Android only. iOS devices are always portrait.

## Options

* `0`: Portrait (default)
* `90`: Landscape

> **Note**: Only `0` and `90` are supported. The legacy `180` and `270` values are no longer accepted.

## Usage

```bash
# Portrait
dcd cloud ... --orientation 0

# Landscape
dcd cloud ... --orientation 90
```


# Maestro Versions

Device Cloud supports multiple Maestro versions to ensure compatibility with your test flows.

By default, tests currently run using Maestro 2.2.0.

Please note that we periodically remove support for older versions so we always recommend you stay up to date.

## Available Versions

We currently support the following versions of Maestro:

* 2.0.4
* 2.0.9
* 2.1.0
* 2.2.0
* 2.5.0
* 2.5.1
* 2.6.0
* 2.6.1
* 2.7.0
* 2.8.0

## Version Selection

You can specify a version using `--maestro-version <version>`.

We additionally support the use of `--maestro-version latest` which will default to the most up-to-date version we support. This currently resolves to 2.8.0.

Note that Maestro 2.7.0 reorganised the per-flow debug output into a new bundle layout, with screenshots named `step-<number>-<command>.png`. If you download and process test artifacts programmatically, check your tooling against a 2.7.0 or later run before switching.

### Examples

```bash
# Use a specific version of Maestro
dcd cloud ... --maestro-version 2.1.0

# Always use the most recent version of Maestro available
dcd cloud ... --maestro-version latest
```


# Google Play APIs

DeviceCloud allows flow execution using Android emulators with the Google Play APIs. Please note that these are charged at our advanced rate as per our [pricing](/billing/test-run-billing).

## Usage

### Whole upload

Pass the flag on `dcd cloud`:

```bash
dcd cloud ... --google-play
```

Every Android flow in the upload runs on a Google Play emulator.

### Per test

When most of your suite is fine on the standard image but a single flow needs Play Services (for example, a Google Sign-In step or a Play-billing screen), opt that one flow in via a DeviceCloud env variable in its YAML:

```yaml
# in your test.yaml
appId: my.app
env:
    DEVICECLOUD_OVERRIDE_GOOGLE_PLAY: true
---
# test steps
```

Only that flow boots on a Google Play emulator; the rest of the upload uses the standard image. Billing is per flow — Play-image flows are charged at the Google Play rate, the others at the standard rate.

## Precedence

1. `DEVICECLOUD_OVERRIDE_GOOGLE_PLAY: true` (per test, opt-in)
2. The upload-wide `--google-play` flag
3. Default: standard Google APIs image

## Device availability

Google Play APIs are currently only available on a single device:

* `pixel-7` on API level `34`

Please contact support if you would like more device/API availability.


# Runner Types

By default each test run uses the following underlying hardware specifications:

| Runner Type | Platform | Host OS | Architecture | RAM   | CPU Cores |
| ----------- | -------- | ------- | ------------ | ----- | --------- |
| `default`   | iOS      | MacOS   | M1           | 8 GB  | 8         |
| `cpu1`      | Android  | Ubuntu  | x86          | 12 GB | 12        |

As modern simulators and emulators are becoming more resource intensive, we're is gradually rolling out additional runner types to support more advanced/intensive test scenarios.

{% hint style="info" %}
Additional runner types are currently in beta as we work to increase capacity.
{% endhint %}

If your app or test is struggling with the default runner types, you can try the options below. Please note that non-default runner types are charged at the advanced run fee as per our [pricing](/billing/test-run-billing).

## Available runners

| Runner Type | Platform | Host OS | Architecture | RAM   | CPU Cores | Notes                                                                |
| ----------- | -------- | ------- | ------------ | ----- | --------- | -------------------------------------------------------------------- |
| `m4`        | iOS      | MacOS   | M4           | 16 GB | 10        |                                                                      |
| `m1`        | Android  | MacOS   | M1           | 8 GB  | 8         | Very limited capacity. Used for debugging host OS issues on Android. |
| `gpu1`      | Android  | Ubuntu  | x86          | 16 GB | 5         | Limited capacity. Additional GPU acceleration also provided.         |

### Usage

To use a specialised runner, use the CLI flag `--runner-type`.

```bash
dcd cloud ... --runner-type=<type>

# For example, to use the m4 runner:
dcd cloud ... --runner-type=m4
```


# Artifacts & Downloads

DeviceCloud captures various artifacts during test execution that can help debug and analyze your test runs.

## Available Artifacts

Each test run generates:

* Logs
* Screenshots
* Videos
* Test reports

## Download Options

There are two ways to download artifacts: inline during a `dcd cloud` run, or on-demand after the fact using `dcd artifacts`.

### Inline (during `dcd cloud`)

Pass `--download-artifacts` directly to the `cloud` command and artifacts are downloaded automatically when the run completes:

```bash
# Download artifacts for all tests
dcd cloud app.apk flows/ --download-artifacts ALL

# Download artifacts for failed tests only
dcd cloud app.apk flows/ --download-artifacts FAILED

# Save to a custom path
dcd cloud app.apk flows/ --download-artifacts FAILED --artifacts-path ./failed.zip
```

### On-demand (`dcd artifacts`)

Use the [`dcd artifacts`](/cli-reference/dcd-artifacts) command to download artifacts or reports for any completed run by its upload ID. This is useful when tests were submitted with `--async`, or when you need to pull reports after the fact:

```bash
# Download a zip of all artifacts
dcd artifacts --upload-id <uuid> --download-artifacts ALL

# Download a JUnit report
dcd artifacts --upload-id <uuid> --report junit

# Download an Allure report
dcd artifacts --upload-id <uuid> --report allure --allure-path ./allure-report.html
```

See the [`dcd artifacts` reference](/cli-reference/dcd-artifacts) for the full flag list.

### From the Console

Artifacts and workspace downloads are also available directly from the test result page in the [DeviceCloud console](https://console.devicecloud.dev). Use the download menu on any completed result to save videos, logs, screenshots, or the full workspace bundle.

## Artifact Archive Structure

Artifacts are downloaded as a zip file:

```
artifacts/
├── test1/
│   ├── logs/
│   ├── screenshots/
│   └── video/
├── test2/
│   ├── logs/
│   ├── screenshots/
│   └── video/
└── report.xml  # if JUnit report was requested
```


# Report Formats

Device Cloud supports the standard Maestro report formats as well as [Allure](https://allurereport.org/), a widely used open-source reporting framework that provides enhanced visualisation and history tracking.

## Available Reports

We support the following report types:

* `junit`
* `html`
* `html-detailed`
* `allure`

You can enable each report using `--report <type>`.

The CLI will automatically download the report to your working directory for you after the run finishes. To change the download path, use the flag for that report format:

* `--junit-path <path>` (default `./report.xml`)
* `--html-path <path>` (default `./report.html`, applies to `html` and `html-detailed`)
* `--allure-path <path>` (default `./report.html`)

(`--artifacts-path` is a separate flag that sets the download path for the artifacts zip and requires `--download-artifacts`.)

## Types of Report

### JUnit

JUnit reports are XML-based and are generally the standard format for CI/CD reports.

### HTML and HTML-Detailed

HTML reports are human-readable provide a summary overview of a test run including failure screenshots. Detailed reports include test steps alongside everything from standard HTML reports.

### Allure

Allure reports are HTML-based and include rich and comprehensive visualisations of test results. Read more [here](https://allurereport.org/).


# Inspecting Variables

When you view a test result, the Maestro log shows the commands your flow ran — including any variable references such as `${APP_ID}` or `${output.userId}`. DeviceCloud resolves those references for you and you can hover over a variable in the log or use the variables tab to see the value it had during the run.

This works for variables from any source:

| Source                   | Example                          | Shown as           |
| ------------------------ | -------------------------------- | ------------------ |
| Passed with `--env`      | `${API_URL}`                     | `passed via --env` |
| Defined in the flow      | `${APP_ID}`                      | `defined in flow`  |
| Set by a script (static) | `${output.locators.loginButton}` | `script output`    |
| Computed at runtime      | `${output.sessionId}`            | `runtime value`    |

If a value genuinely can't be recovered (for example a value read from the screen at runtime that was never stored), the tooltip shows "Not captured".

## Hiding sensitive values

Some variables hold secrets — passwords, API tokens, auth credentials — that you don't want visible in the result log or in any downloaded artifact.

DeviceCloud masks these automatically, based on the variable's name. If a variable's name contains any of the following words (case-insensitive, anywhere in the name), its value is hidden and:

```
password   secret   token   apikey / api_key / api-key   auth
credential   passwd   pwd   pin   otp   private
```

You don't need any special syntax, just name the variable accordingly:

```bash
# Passed with --env
dcd cloud app.zip flow.yaml \
  -e DB_PASSWORD=... \
  -e API_TOKEN=... \
  -e SESSION_SECRET=...
```

```yaml
# Defined in a flow or set by a script
env:
  LOGIN_CREDENTIAL: ${CI_CREDENTIAL}
---
- evalScript: ${output.authToken = login()}
```

Matching is case-insensitive and matches anywhere in the name, so `apiToken`, `loginPassword`, `userPIN`, and `OTP_CODE` all qualify.

{% hint style="warning" %}
DeviceCloud accepts no liability for any secret that may be unintentionally displayed through this system. **If your secret has been leaked, rotate it immediately** then contact our support team to flag the issue.
{% endhint %}

### What masking covers

For a variable whose name matches, DeviceCloud:

* shows "Hidden value" on hover instead of the value ensuring the value is never sent to your browser nor logged internally;
* replaces the value with `***` in the result-page log;
* scrubs the value from downloadable artifacts (`maestro.log`, stdout/stderr, `commands.json`).

{% hint style="warning" %}
Masking is based on the variable **name**, not the value. A sensitive value stored under an innocent-looking name (e.g. `loginUrl = "https://example.com?key=abc123"`) will **not** be masked. Always name secret-bearing variables with one of the keywords above.
{% endhint %}


# Email Notifications

DeviceCloud can email a pass/fail summary of each test run to your team, so people find out about failures without having to open the console.

Unlike [Slack](/notifications/slack-notifications) and [webhook](/notifications/webhook-notifications) notifications, email is configured **per project** in your `config.yaml` rather than from the console. That means it travels with your repo and can differ per branch or pipeline.

### Enabling email notifications

Add a `notifications.email` block to your `config.yaml` and list the recipients:

```yaml
# config.yaml
notifications:
  email:
    enabled: true
    recipients:
      - sam@example.com
      - devs@example.com
```

By default you'll only be emailed when a suite has at least one failed or cancelled flow.

{% hint style="info" %}
The `config.yaml` is picked up automatically from the directory you pass to `dcd cloud`. See [Workspace Configuration](/configuration/workspace-config) for where the file lives and how it's loaded.
{% endhint %}

#### Also notify on passing runs

To receive a summary after **every** completed run set `onSuccess` to `true`:

```yaml
# config.yaml
notifications:
  email:
    enabled: true
    onSuccess: true
    recipients:
      - sam@example.com
      - devs@example.com
```

### What's in the email

The email is sent from **DeviceCloud <<hello@devicecloud.dev>>** and includes:

* An at-a-glance summary in the subject line.
* The test suite name and overall counts of passed, failed and cancelled flows.
* A grouped list of all flows by status.
* Links to view the suite or individual flows on the console.

{% hint style="info" %}
If you are not receiving emails for your test runs, try adding **<hello@devicecloud.dev>** to your safe senders list.
{% endhint %}

### Disabling emails

Simply set `enabled: false` (or remove the `notifications.email` block) from your `config.yaml`.

```yaml
# config.yaml
notifications:
  email:
    enabled: false
    onSuccess: true
    recipients:
      - sam@example.com
      - devs@example.com
```


# Slack Notifications

DeviceCloud can post a pass/fail summary to a Slack channel every time a test run completes, so your team finds out about failures without checking the console.

Slack is configured once per organization from the DeviceCloud console — there is nothing to add to your `config.yaml` or CI pipeline.

### Connecting Slack

1. Head to the [console settings](https://console.devicecloud.dev/settings?section=notifications) page and open the **Notifications** section.
2. Click **Connect Slack**. You'll be redirected to Slack to authorize DeviceCloud for your workspace.
3. After you approve, you'll be returned to the console.
4. Pick the **channel** you want notifications posted to and click **Save**.

{% hint style="info" %}
DeviceCloud requests only the permissions it needs to list your channels and post messages. It cannot read your messages.
{% endhint %}

#### Sending a test message

Once a channel is selected, use **Send test** to post a sample message to that channel and confirm everything is wired up. The test uses data from your most recent run when available.

#### Only notify on failures

By default DeviceCloud posts after **every** completed run. To cut down on noise, turn on **Only notify on failures** in the Slack section — passing runs will then be skipped and you'll only get a message when a run has at least one failed flow.

### What's in the message

Each notification includes:

* An overall **passed / failed** status.
* The test suite (upload) name, pass/fail counts, and total duration.
* **Run environment**: app ID, device & OS (e.g. *Android · Pixel 7 · API 34*), Maestro version, and runner type.
* A list of failed flows (with their failure reason, where available).
* A **View results in DeviceCloud** link to the run in the console.

#### Git / CI context

When a run carries git metadata, the message also shows a line with the **repository**, **branch**, a linked **commit**, and a linked **pull request**.

This is populated automatically when you run in CI, or when you pass the git flags to the CLI:

```bash
dcd cloud app.apk flows/ \
  --branch "$GIT_BRANCH" \
  --commit-sha "$GIT_SHA" \
  --repo-name "owner/repo" \
  --pr-number "123" \
  --pr-url "https://github.com/owner/repo/pull/123"
```

The line only appears when at least one of these values is present, and the commit/PR become clickable GitHub links when a `--repo-name` is supplied.

### Disconnecting

Click **Disconnect** in the Slack section to stop notifications and revoke DeviceCloud's access to your workspace. You can disconnect and reconnect at any time.


# Webhook Notifications

DeviceCloud can send a POST request to a URL of your choice when a set of tests completes.

### Enabling Webhooks

Head to the [console settings](https://console.devicecloud.dev/settings) page and scroll to the Webhooks section. Enter your webhook URL there.

{% hint style="info" %}
Only HTTPS URLs are supported.
{% endhint %}

You can also send a test request from the console to verify that your endpoint processes webhooks correctly. The test payload will contain information from your most recent job.

### Webhook Structure

**Request headers:**

```
Content-Type: application/json
X-DeviceCloud-Secret: DeviceCloud_<webhook_secret>
X-DeviceCloud-Webhook-ID: wh_test_<random_hex>
X-DeviceCloud-Timestamp: <unix_timestamp>
User-Agent: DeviceCloud-Webhooks/1.0
```

**Request body:**

```json
{
  "event": "upload.completed",
  "timestamp": "2025-09-19T15:07:15.357Z",
  "upload_id": "test_upload_synthetic_123",
  "console_url": "https://console.devicecloud.dev/results?upload=test_upload_synthetic_123",
  "status": "PASSED",
  "device": {
    "name": "iPhone 15",
    "osVersion": "17.5",
    "runnerType": "standard",
    "maestroVersion": "1.39.0"
  },
  "summary": {
    "totalTests": 1,
    "passed": 1,
    "failed": 0,
    "durationSeconds": 45
  },
  "results": [
    {
      "name": "test_flow.yaml",
      "status": "PASSED",
      "durationSeconds": 45,
      "tags": ["smoke"],
      "properties": {
        "jira_ticket": "ENG-402",
        "deployment_env": "staging"
      }
    }
  ],
  "metadata": {
    "branch": "main",
    "commit": "abc1234"
  },
  "test": true
}
```

**Fields:**

| Field      | Description                                                                                                                                                                      |
| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`    | Always `upload.completed`.                                                                                                                                                       |
| `status`   | Overall run outcome: `PASSED` only when every test passed, otherwise `FAILED`. Use this to gate a deploy or set a commit status.                                                 |
| `device`   | Device and runner context for the run: `name`, `osVersion`, `runnerType`, and `maestroVersion`. Individual fields are omitted when unavailable.                                  |
| `summary`  | Aggregate counts (`totalTests`, `passed`, `failed`, and optional `cancelled`/`queued`/`pending`/`running`) plus `durationSeconds`, `wallClockDurationSeconds`, and `retryCount`. |
| `results`  | One entry per test: `name`, `status`, `durationSeconds`, `failReason` (failures only), plus `tags` and `properties` (see below).                                                 |
| `metadata` | The key/value pairs you supplied via the CLI `--metadata` flag. Omitted when none were provided.                                                                                 |
| `test`     | Present and `true` only for test requests sent from the console.                                                                                                                 |

### Tags and Custom Properties

Each entry in `results` carries the `tags` and `properties` declared in that flow's YAML front matter, so you can route or filter events without a second API call:

```yaml
appId: com.example.app
name: Login Flow
tags:
  - smoke
properties:
  jira_ticket: "ENG-402"
  deployment_env: "staging"
---
- launchApp
```

`tags` and `properties` are each omitted from a result when the flow declares none.

{% hint style="info" %}
Property values are always delivered as strings — numbers and booleans are converted (`42` becomes `"42"`, `true` becomes `"true"`).
{% endhint %}

{% hint style="warning" %}
Property values must be scalars. Maestro rejects a flow whose `properties:` contains a nested object or a list, failing it before it runs with `Incorrect Format: <key>`.
{% endhint %}

### Webhook Secrets

All webhook requests are signed with the secret shown on your DeviceCloud console. Use this to verify the request came from DeviceCloud.

Keep this secret private, just like your API key.


# Overview

The DCD CLI is the primary way to interact with DeviceCloud from your terminal or CI/CD pipeline. It is a drop-in replacement for `maestro cloud` — in most cases you can swap `maestro cloud` for `dcd cloud`.

The CLI is published as `@devicecloud.dev/dcd` on npm and as a standalone binary. The same package also ships an [MCP server](/ai-agents-and-mcp/overview) so AI agents can drive DeviceCloud directly.

{% hint style="info" %}
**New in v5:** browser-based [`dcd login`](/cli-reference/dcd-login) (no more passing a key on every command), a standalone binary installer with `dcd upgrade`, interactive [`dcd live`](/cli-reference/dcd-live) device sessions, and an [MCP server](/ai-agents-and-mcp/overview). Existing API keys and `dcd cloud` usage continue to work unchanged.
{% endhint %}

## Installation

The recommended install is the standalone binary — it has no dependencies and doesn't require Node.

{% tabs %}
{% tab title="macOS / Linux" %}

```bash
curl -fsSL https://get.devicecloud.dev/install.sh | sh
```

{% endtab %}

{% tab title="Windows" %}

```powershell
irm https://get.devicecloud.dev/install.ps1 | iex
```

{% endtab %}

{% tab title="npm" %}
Requires Node 22 or newer.

```bash
npm install -g @devicecloud.dev/dcd
```

{% endtab %}
{% endtabs %}

In CI, you can skip a separate install step and run the CLI directly with `npx`:

```bash
npx --yes @devicecloud.dev/dcd@latest cloud <app-file> <flows-dir>
```

## Upgrading

If you installed the standalone binary, update it in place:

```bash
dcd upgrade
```

If you installed via npm, upgrade with npm instead:

```bash
npm install -g @devicecloud.dev/dcd@latest
```

{% hint style="info" %}
`dcd upgrade` is only for binary installs. Automatic upgrade is not yet supported on Windows — re-run the PowerShell installer to update.
{% endhint %}

## Authentication

There are two ways to authenticate. See [Authentication](/getting-started/api-keys) for full detail.

* **`dcd login`** (recommended for local use) — authenticate once in your browser. The CLI stores a session, so you don't have to pass a key on every command, and it unlocks live test updates.
* **API key** (recommended for CI/headless) — set the `DEVICE_CLOUD_API_KEY` environment variable, or pass `--api-key <key>` on any command.

```bash
# Local: log in once
dcd login

# CI / headless: provide an API key
export DEVICE_CLOUD_API_KEY=your-api-key
```

When both are present, precedence is: `--api-key` flag → `DEVICE_CLOUD_API_KEY` env var → stored `dcd login` session.

## Commands

| Command                                                     | Description                                          |
| ----------------------------------------------------------- | ---------------------------------------------------- |
| [`dcd cloud`](/cli-reference/dcd-cloud)                     | Upload an app and run Maestro flows on DeviceCloud   |
| [`dcd upload`](/cli-reference/dcd-upload)                   | Upload an app binary and get a reusable binary ID    |
| [`dcd status`](/cli-reference/dcd-status)                   | Check the status of a test upload                    |
| [`dcd list`](/cli-reference/dcd-list)                       | List recent uploads for your organisation            |
| [`dcd artifacts`](/cli-reference/dcd-artifacts)             | Download artifacts or reports for a completed run    |
| [`dcd login`](/cli-reference/dcd-login)                     | Authenticate via your browser                        |
| [`dcd logout`](/cli-reference/dcd-login#dcd-logout)         | Clear the stored session                             |
| [`dcd whoami`](/cli-reference/dcd-login#dcd-whoami)         | Show the logged-in user and active organisation      |
| [`dcd switch-org`](/cli-reference/dcd-login#dcd-switch-org) | Switch the active organisation                       |
| [`dcd live`](/cli-reference/dcd-live)                       | Start and interact with a live device session (beta) |
| `dcd upgrade`                                               | Upgrade the standalone binary in place               |

## Getting Help

```bash
dcd --help
dcd cloud --help
dcd live --help
```


# Cloud

Upload an app binary and run Maestro flows on DeviceCloud. This is the primary command and a drop-in replacement for `maestro cloud`.

```bash
dcd cloud <app-file> <flows-dir> [flags]
```

The command blocks until all tests have completed, then exits with an appropriate [exit code](/advanced/exit-codes).

## Arguments

| Argument      | Description                                                            |
| ------------- | ---------------------------------------------------------------------- |
| `<app-file>`  | Path to your app binary (`.apk` for Android, `.app` or `.zip` for iOS) |
| `<flows-dir>` | Path to the flow file or directory of flows to run                     |

## Flags

### Authentication

| Flag              | Description                                                                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `--api-key <key>` | Your DeviceCloud API key. Defaults to the `DEVICE_CLOUD_API_KEY` env var. Optional if you've run [`dcd login`](/cli-reference/dcd-login) |

See [Authentication](/getting-started/api-keys) for the full picture.

### App

| Flag                   | Description                                                                                                                                                                      |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--app-binary-id <id>` | Reuse a previously uploaded binary instead of uploading again                                                                                                                    |
| `--app-url <url>`      | Signed URL to an Expo iOS build (`.tar.gz`). The archive is downloaded and extracted automatically. Expo signed URLs expire after \~1 hour. Mutually exclusive with `--app-file` |
| `--app-file <path>`    | Path to the app binary (alternative to the positional `<app-file>` argument)                                                                                                     |
| `--ignore-sha-check`   | Force re-upload even if a binary with the same SHA already exists                                                                                                                |

### Device

| Flag                          | Description                                                                                                          |
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `--android-device <device>`   | Android device model to run on (see [Devices](/getting-started/devices-configuration))                               |
| `--android-api-level <level>` | Android API level                                                                                                    |
| `--ios-device <device>`       | iOS device model to run on (see [Devices](/getting-started/devices-configuration))                                   |
| `--ios-version <version>`     | iOS version                                                                                                          |
| `--device-locale <locale>`    | Device locale (see [Device Locale](/configuration/device-locale))                                                    |
| `--orientation <orientation>` | Device orientation, `0` (portrait) or `90` (landscape). Android only (see [Orientation](/configuration/orientation)) |
| `--google-play`               | Use a Google Play-enabled device. Android only (see [Google Play APIs](/configuration/google-play-apis))             |
| `--runner-type <type>`        | Runner type to use (see [Runner Types](/configuration/runner-type))                                                  |

### Flows

| Flag                      | Description                                                                                                    |
| ------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `--flows <paths>`         | Comma-separated list of flow files to run (alternative to positional arg)                                      |
| `--config <path>`         | Path to a `config.yaml` workspace config file (see [Workspace Configuration](/configuration/workspace-config)) |
| `--exclude-flows <paths>` | Comma-separated list of flow files or sub-directories to exclude                                               |
| `--include-tags <tags>`   | Only run flows with these tags (comma-separated)                                                               |
| `--exclude-tags <tags>`   | Skip flows with these tags (comma-separated)                                                                   |

### Test Configuration

| Flag                          | Description                                                                                                       |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `--maestro-version <version>` | Maestro version to use, or `latest` (see [Maestro Versions](/configuration/maestro-versions))                     |
| `--env <KEY=VALUE>`           | Environment variables to pass to the test. Repeat for multiple values                                             |
| `--metadata <key=value>`      | Arbitrary metadata to attach to the run (shown in the console). Repeat for multiple values                        |
| `--name <name>`               | Name for this upload (shown in the console)                                                                       |
| `--retry <n>`                 | Retry failed tests up to `n` times (free of charge). Max `2` (see [Retry Strategies](/advanced/retry-strategies)) |

### GitHub / PR Context

Attach Git and pull request metadata to a run. These values are displayed in the DeviceCloud console alongside the test results, making it easy to trace a run back to the exact commit or PR that triggered it.

| Flag                       | Description                                            |
| -------------------------- | ------------------------------------------------------ |
| `--branch <name>`          | Git branch name for this run                           |
| `--commit-sha <sha>`       | Git commit SHA for this run                            |
| `--repo-name <owner/repo>` | Repository in `owner/repo` format (e.g. `acme/my-app`) |
| `--pr-number <number>`     | Pull request number                                    |
| `--pr-url <url>`           | Pull request URL                                       |

### Android-Specific

| Flag                          | Description                                                                                                                                   |
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `--maestro-chrome-onboarding` | Force Maestro-based Chrome onboarding. Slows tests but can fix browser-related crashes (see [Chrome Onboarding](/advanced/chrome-onboarding)) |
| `--android-no-snapshot`       | Force cold boot instead of snapshot boot. Automatically enabled for API 35+                                                                   |
| `--show-crosshairs`           | Display crosshairs for screen interactions during test execution                                                                              |

### Performance

| Flag                   | Description                                                                                                                  |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `--disable-animations` | Disable device animations during test execution. On Android, disables system animation scales. On iOS, enables Reduce Motion |

### Output & Execution

| Flag                                 | Description                                                                                                                                            |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--async`                            | Submit tests and return immediately (exit `0`) without waiting for results (see [Async Execution](/advanced/async-execution))                          |
| `--quiet`, `-q`                      | Suppress per-test progress; print only the final summary                                                                                               |
| `--json`                             | Output results as JSON. Exits `0` on success, `2` on test failure, `1` on CLI/infrastructure errors                                                    |
| `--json-file`                        | Write JSON results to a file (`<upload_id>_dcd.json` by default). Exits `0` even if the test run fails; infrastructure errors still exit `1`           |
| `--json-file-name <name>`            | Custom name (or relative path) for the JSON file. Requires `--json-file`                                                                               |
| `--dry-run`                          | Simulate the run without uploading or triggering a test — useful for debugging workflow issues                                                         |
| `--report <format>`                  | Generate and download a report. Options: `junit`, `html`, `html-detailed`, `allure` (see [Report Formats](/test-artifacts-and-reports/report-formats)) |
| `--junit-path <path>`                | Output path for the JUnit report (requires `--report junit`)                                                                                           |
| `--html-path <path>`                 | Output path for the HTML report (requires `--report html` or `html-detailed`)                                                                          |
| `--allure-path <path>`               | Output path for the Allure report (requires `--report allure`)                                                                                         |
| `--download-artifacts <ALL\|FAILED>` | Download test artifacts after completion (see [Artifacts](/test-artifacts-and-reports/artifacts))                                                      |
| `--artifacts-path <path>`            | Output path for the artifacts zip (default `./artifacts.zip`). Requires `--download-artifacts`                                                         |
| `--debug`                            | Enable verbose debug logging                                                                                                                           |

## Examples

**Android:**

```bash
dcd cloud app.apk flows/ --android-device pixel-7 --android-api-level 34
```

**iOS:**

```bash
dcd cloud app.zip flows/ --ios-device iphone-16 --ios-version 18
```

**Filter by tag:**

```bash
dcd cloud app.apk flows/ --include-tags smoke
```

**Reuse a previously uploaded binary:**

```bash
dcd cloud flows/ --app-binary-id 67894274-b789-4c1e-80d4-da8998998999
```

**Save results to JSON:**

```bash
dcd cloud app.apk flows/ --json-file --json-file-name results.json
```


# Artifacts

Download artifacts or reports for a completed test run by upload ID. Use this command when you need to retrieve artifacts from a run that was submitted with `--async`, or to download reports independently after the fact.

```bash
dcd artifacts --upload-id <id> [flags]
```

## Flags

### Authentication

| Flag              | Description                                                          |
| ----------------- | -------------------------------------------------------------------- |
| `--api-key <key>` | Your DeviceCloud API key. Defaults to `DEVICE_CLOUD_API_KEY` env var |

### Required

| Flag                 | Description                                            |
| -------------------- | ------------------------------------------------------ |
| `--upload-id <uuid>` | UUID of the completed upload to download artifacts for |

### Artifact Download

| Flag                                 | Description                                                                                                                                         |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--download-artifacts <ALL\|FAILED>` | Download a zip of logs, screenshots, and videos. `ALL` includes every test; `FAILED` includes only failed tests. Mutually exclusive with `--report` |
| `--artifacts-path <path>`            | Output path for the artifacts zip (default: `./artifacts.zip`). Requires `--download-artifacts`                                                     |

### Report Download

| Flag                   | Description                                                                                                                 |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `--report <format>`    | Download a test report. Options: `junit`, `html`, `html-detailed`, `allure`. Mutually exclusive with `--download-artifacts` |
| `--allure-path <path>` | Output path for the Allure report (default: `./report.html`). Requires `--report allure`                                    |
| `--html-path <path>`   | Output path for the HTML report (default: `./report.html`). Requires `--report html` or `html-detailed`                     |
| `--junit-path <path>`  | Output path for the JUnit report (default: `./report.xml`). Requires `--report junit`                                       |

### Other

| Flag      | Description                   |
| --------- | ----------------------------- |
| `--debug` | Enable detailed debug logging |

## Examples

**Download artifacts for all tests:**

```bash
dcd artifacts --upload-id 123e4567-e89b-12d3-a456-426614174000 --download-artifacts ALL
```

**Download artifacts for failed tests only, to a custom path:**

```bash
dcd artifacts --upload-id 123e4567-e89b-12d3-a456-426614174000 --download-artifacts FAILED --artifacts-path ./failed-artifacts.zip
```

**Download a JUnit report:**

```bash
dcd artifacts --upload-id 123e4567-e89b-12d3-a456-426614174000 --report junit
```

**Download an Allure report to a custom path:**

```bash
dcd artifacts --upload-id 123e4567-e89b-12d3-a456-426614174000 --report allure --allure-path ./allure-report.html
```

## Using with Async Runs

`dcd artifacts` is especially useful alongside `--async`. Submit the run without blocking, then retrieve artifacts later:

```bash
# Submit tests and capture the upload ID
dcd cloud app.apk flows/ --async --json-file --json-file-name results.json

# ... later, once the run is complete ...
UPLOAD_ID=$(jq -r .uploadId results.json)
dcd artifacts --upload-id "$UPLOAD_ID" --download-artifacts ALL
```

See [Async Execution](/advanced/async-execution) and [Report Formats](/test-artifacts-and-reports/report-formats) for more details.


# Upload

Upload an app binary to DeviceCloud and get back a binary ID. Use the binary ID in subsequent `dcd cloud` runs to skip re-uploading the same binary.

DeviceCloud deduplicates binaries by SHA checksum — if you upload the same binary twice, the existing ID is returned and no upload occurs.

```bash
dcd upload <app-file> [flags]
```

## Arguments

| Argument     | Description                                                            |
| ------------ | ---------------------------------------------------------------------- |
| `<app-file>` | Path to your app binary (`.apk` for Android, `.app` or `.zip` for iOS) |

## Flags

| Flag                 | Description                                                          |
| -------------------- | -------------------------------------------------------------------- |
| `--api-key <key>`    | Your DeviceCloud API key. Defaults to `DEVICE_CLOUD_API_KEY` env var |
| `--ignore-sha-check` | Force re-upload even if a binary with the same SHA already exists    |
| `--json`             | Output the binary ID as JSON                                         |
| `--debug`            | Enable verbose debug logging                                         |

## Examples

```bash
dcd upload app.apk
```

```bash
dcd upload app.zip --api-key <key> --json
```

**Output:**

```
Binary ID: 67894274-b789-4c1e-80d4-da8998998999

You can use this Binary ID in subsequent test runs with:
dcd cloud --app-binary-id 67894274-b789-4c1e-80d4-da8998998999 path/to/flow.yaml
```

## Using Binary IDs

Uploading the binary separately is useful when running the same build across multiple test jobs — upload once, then pass `--app-binary-id` to each `dcd cloud` run:

```bash
# Upload once
BINARY_ID=$(dcd upload app.apk --json | jq -r '.appBinaryId')

# Run multiple jobs reusing the same binary
dcd cloud flows/smoke/ --app-binary-id $BINARY_ID --include-tags smoke
dcd cloud flows/regression/ --app-binary-id $BINARY_ID --include-tags regression
```


# Status

Check the status of a test upload by name or upload ID. Useful for polling an async run or retrieving results after the fact.

```bash
dcd status --upload-id <uuid> [flags]
dcd status --name <upload-name> [flags]
```

One of `--upload-id` or `--name` is required. If multiple uploads share the same name, the most recent is returned.

## Flags

| Flag                 | Description                                                                                                                                                       |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--upload-id <uuid>` | UUID of the upload to check                                                                                                                                       |
| `--name <name>`      | Name of the upload to check                                                                                                                                       |
| `--api-key <key>`    | Your DeviceCloud API key. Defaults to `DEVICE_CLOUD_API_KEY` env var                                                                                              |
| `--json`             | Output results as JSON. Exits `0` once a result is retrieved (including a `FAILED` run); usage errors, such as omitting both `--upload-id` and `--name`, exit `1` |

## Examples

```bash
dcd status --upload-id 7e12345f-eb12-12ec-a30b-bb1234f1d12a
```

```bash
dcd status --name "Nightly regression"
```

**Output:**

```
📊 Upload Status
════════════════════════════════════════════════════════════════════════════════
✓  Status:      FAILED
🆔 Upload ID:   7e12345f-eb12-12ec-a30b-bb1234f1d12a
📱 Binary ID:   67894274-b789-4c1e-80d4-da8998998999
🔗 Console:     https://console.devicecloud.dev/results?upload=7e12345f-...

📋 Test Results
────────────────────────────────────────────────────────────────────────────────
✗ ./login-test/onboarding.yaml
   Status: FAILED
   Fail reason: Element not found: Text matching regex next.* not found
   Duration: 32s
```

## JSON Output

Use `--json` for machine-readable output:

```bash
dcd status --name "Nightly regression" --json
```

```json
{
  "status": "FAILED",
  "tests": [
    {
      "name": "./login-test/onboarding.yaml",
      "status": "FAILED",
      "failReason": "Element not found: Text matching regex next.* not found",
      "durationSeconds": 32
    }
  ],
  "appBinaryId": "67894274-b789-4c1e-80d4-da8998998999",
  "uploadId": "7e12345f-eb12-12ec-a30b-bb1234f1d12a",
  "consoleUrl": "https://console.devicecloud.dev/results?upload=7e12345f-eb12-12ec-a30b-bb1234f1d12a&result=4500"
}
```

Use with [jq](https://jqlang.github.io/jq/) to extract specific values:

```bash
export BINARY_ID=$(dcd status --name "Nightly regression" --json | jq -r '.appBinaryId')
```

## Polling an Async Run

When using `dcd cloud --async`, use `dcd status` to poll for completion:

```bash
# GitHub Actions example
- name: Run tests
  run: dcd cloud app.apk flows/ --async --name "CI-${{ github.run_id }}"

- name: Wait for results
  run: |
    while true; do
      STATUS=$(dcd status --name "CI-${{ github.run_id }}" --json | jq -r '.status')
      echo "Status: $STATUS"
      if [[ "$STATUS" == "PASSED" || "$STATUS" == "FAILED" || "$STATUS" == "CANCELLED" ]]; then
        break
      fi
      sleep 30
    done
    [ "$STATUS" == "PASSED" ]
```

See [Async Execution](/advanced/async-execution) for more patterns.

## Underlying API

`dcd status` calls `GET /uploads/status` on the DeviceCloud API. See [Uploads API](/rest-api/uploads) if you need to call it directly.


# List

List recent test uploads for your organisation.

```bash
dcd list [flags]
```

## Flags

| Flag               | Default | Description                                                                                           |
| ------------------ | ------- | ----------------------------------------------------------------------------------------------------- |
| `--limit <n>`      | `20`    | Maximum number of uploads to return                                                                   |
| `--offset <n>`     | `0`     | Number of uploads to skip (for pagination)                                                            |
| `--name <pattern>` |         | Filter by upload name. Supports `*` as a wildcard — always quote wildcards to prevent shell expansion |
| `--from <date>`    |         | Return uploads created on or after this date (ISO 8601, e.g. `2024-01-01`)                            |
| `--to <date>`      |         | Return uploads created on or before this date (ISO 8601)                                              |
| `--api-key <key>`  |         | Your DeviceCloud API key. Defaults to `DEVICE_CLOUD_API_KEY` env var                                  |
| `--json`           |         | Output results as JSON                                                                                |

## Examples

```bash
# List the 20 most recent uploads
dcd list

# List with a wildcard name filter (note the quotes)
dcd list --name "nightly-*"

# Filter by date range
dcd list --from 2024-01-01 --to 2024-01-31

# Paginate
dcd list --limit 10 --offset 20

# JSON output
dcd list --json
```

{% hint style="info" %}
Always quote wildcard patterns to prevent your shell from expanding them before they reach the CLI — use `"nightly-*"` not `nightly-*`.
{% endhint %}

## Output

```
Recent Uploads
════════════════════════════════════════════════════════════════════════════════
   Showing 3 of 3 uploads

   Nightly regression
      ID:      7e12345f-eb12-12ec-a30b-bb1234f1d12a
      Created: Jan 15, 2026, 10:30 AM
      Console: https://console.devicecloud.dev/results?upload=7e12345f-...

   Smoke tests
      ID:      ...
      ...

Tip: Use dcd status --upload-id <id> for detailed test results
```


# Login & Accounts

These commands manage browser-based authentication and the team your runs are billed to. They're the recommended way to authenticate for local or interactive use. For CI and headless environments, you should use an [API key](/getting-started/api-keys) instead.

## `dcd login`

Authenticate with DeviceCloud via your browser. Run it once and the CLI stores your session, so subsequent commands don't need an API key.

```bash
dcd login
```

This opens your browser to complete sign-in (email OTP or Enterprise SSO). After you authorise the CLI, if you belong to more than one team you'll be prompted to pick the one to use. Your session is then saved locally (see [Where credentials are stored](#where-credentials-are-stored)).

### Flags

| Flag                   | Description                                                                                         |
| ---------------------- | --------------------------------------------------------------------------------------------------- |
| `--no-browser`         | Print the login URL instead of opening a browser. Useful over SSH or on machines without a browser. |
| `--frontend-url <url>` | Override the frontend URL used to complete login                                                    |

{% hint style="info" %}
`dcd login` uses a PKCE browser flow with no local server, so it works over SSH and when you open the URL on a different device. A leaked login URL is inert and can't be used to claim your session.
{% endhint %}

If you're already logged in, `dcd login` asks for confirmation before replacing the existing session.

## `dcd logout`

Clear the stored session and revoke it server-side (best effort).

```bash
dcd logout
```

This only affects the `dcd login` session and it does not touch the environment variables or the `--api-key` flag.

## `dcd whoami`

Show the user and team the CLI is currently acting as.

```bash
dcd whoami
```

Prints the logged-in user, the active team, and the environment. Does nothing if you're not logged in.

## `dcd switch-org`

Switch the active team for your logged-in session. Useful if you belong to more than one team.

```bash
# Interactive picker
dcd switch-org

# Switch directly by team name
dcd switch-org "example@example.com's Team"
```

Teams are matched by name (case-insensitive). Requires an active `dcd login` session and an exported `DEVICE_CLOUD_API_KEY` will not override the session.

## Where credentials are stored

The session from `dcd login` is written to:

* `$XDG_CONFIG_HOME/dcd/config.json`, or
* `~/.dcd/config.json` if `XDG_CONFIG_HOME` is not set

The file is created with `0600` permissions (owner read/write only) and holds your session tokens and the active team. Expiring sessions are refreshed automatically, so you should rarely need to log in again.


# Live Sessions

Start an interactive cloud device session and drive it from your terminal — install a build, run individual Maestro commands or a whole flow, capture screenshots, and dump the view hierarchy. This is ideal for authoring and debugging flows against a real cloud device.

{% hint style="warning" %}
**Live is in beta** and is billed at **$0.03/min**. Access is limited and by request only. If you would like to try Live, please contact support to request access. `dcd live` requires a [`dcd login`](/cli-reference/dcd-login) session.
{% endhint %}

```bash
dcd live <subcommand> [flags]
```

A typical session: start a device, install a build, interact, then stop.

```bash
dcd live start --platform android --wait
dcd live install --session <name> --app-binary-id <id> --wait
dcd live run --session <name> path/to/flow.yaml
dcd live stop --session <name>
```

Every subcommand (except `start`) takes a `--session <name>` flag identifying the session, and accepts `--api-key` / `--api-url`.

## `dcd live start`

Start a new live device session. Prints the session name and a console URL you can open to watch the device.

| Flag                          | Description                                                                          |
| ----------------------------- | ------------------------------------------------------------------------------------ |
| `--platform <android\|ios>`   | Device platform (default: `android`)                                                 |
| `--app-binary-id <id>`        | Binary upload ID to install on the device at start                                   |
| `--device-locale <locale>`    | Device locale, e.g. `de_DE`                                                          |
| `--android-device <device>`   | Android only. Device profile (`pixel-6`, `pixel-6-pro`, `pixel-7`, `pixel-7-pro`)    |
| `--android-api-level <level>` | Android only. API level, e.g. `34` (must be passed together with `--android-device`) |
| `--wait`                      | Block until the device is ready to accept commands                                   |

## `dcd live install`

Install a binary on a running session's device.

| Flag                   | Description                                            |
| ---------------------- | ------------------------------------------------------ |
| `--session <name>`     | **Required.** Live session name                        |
| `--app-binary-id <id>` | **Required.** Binary upload ID to install              |
| `--wait`               | Block until the device is ready again after installing |

## `dcd live exec`

Execute one or more Maestro YAML commands against the session.

| Flag                | Description                                                            |
| ------------------- | ---------------------------------------------------------------------- |
| `--session <name>`  | **Required.** Live session name                                        |
| `--yaml <commands>` | Inline Maestro YAML commands, e.g. `--yaml "- launchApp"`              |
| `--file <path>`     | Read the Maestro YAML to execute from a file (alternative to `--yaml`) |
| `--wait`            | Wait for the device to be ready before executing                       |

Pass either `--yaml` or `--file`, not both.

## `dcd live run`

Run a whole Maestro flow file against the session. The flow's `appId:` header is handled automatically.

| Flag                  | Description                                                       |
| --------------------- | ----------------------------------------------------------------- |
| `<flow-file>`         | **Required.** Path to the Maestro flow file (positional argument) |
| `--session <name>`    | **Required.** Live session name                                   |
| `--timeout <seconds>` | Max seconds to wait for the flow to finish (default: `600`)       |
| `--wait`              | Wait for the device to be ready before running                    |

## `dcd live screenshot`

Save the current device screen to an image file (PNG or JPEG, matching the device format).

| Flag                    | Description                                              |
| ----------------------- | -------------------------------------------------------- |
| `--session <name>`      | **Required.** Live session name                          |
| `--output <path>`, `-o` | File path to write to (default: `live-screenshot.<ext>`) |

## `dcd live hierarchy`

Dump the current view hierarchy — the selectors (text, accessibility labels, resource IDs) you can tap and assert on.

| Flag                    | Description                                  |
| ----------------------- | -------------------------------------------- |
| `--session <name>`      | **Required.** Live session name              |
| `--json`                | Output the raw hierarchy as JSON             |
| `--output <path>`, `-o` | Write the output to a file instead of stdout |

## `dcd live status`

Show a session's status — platform, readiness, current device phase, device model, locale, and time until auto-cancel.

| Flag               | Description                     |
| ------------------ | ------------------------------- |
| `--session <name>` | **Required.** Live session name |

## `dcd live stop`

Stop a live session.

| Flag               | Description                     |
| ------------------ | ------------------------------- |
| `--session <name>` | **Required.** Live session name |


# Overview

The DeviceCloud REST API gives you programmatic access to the same data available in the console — test results, upload history, and flow analytics.

## Authentication

Include your API key in every request:

```
x-app-api-key: <your-api-key>
```

You can find your API key in the console under **Settings → API Key**.

## Base URL

```
https://api.devicecloud.dev
```

## Endpoints

| Resource                               | Description                                                         |
| -------------------------------------- | ------------------------------------------------------------------- |
| [Uploads](/rest-api/uploads)           | List uploads and poll run status                                    |
| [Results](/rest-api/results)           | Fetch results, reports, and artifacts for an upload                 |
| [Flows](/rest-api/flows)               | Aggregated analytics and run history per flow file                  |
| [IP Addresses](/rest-api/ip-addresses) | Current egress IPs for firewall allow-listing (no API key required) |


# Uploads

## List uploads

Returns a paginated list of test uploads for your organisation.

```
GET /uploads/list
```

**Query parameters**

| Parameter | Type   | Required | Description                                               |
| --------- | ------ | -------- | --------------------------------------------------------- |
| `name`    | string | No       | Filter by upload name. Supports `*` as a wildcard.        |
| `from`    | string | No       | Return uploads created on or after this date (ISO 8601).  |
| `to`      | string | No       | Return uploads created on or before this date (ISO 8601). |
| `limit`   | number | No       | Maximum number of results. Default: `20`.                 |
| `offset`  | number | No       | Number of results to skip. Default: `0`.                  |

**Example**

```bash
curl https://api.devicecloud.dev/uploads/list \
  -H "x-app-api-key: <key>"
```

**Response**

```json
{
  "uploads": [
    {
      "id": "7e12345f-eb12-12ec-a30b-bb1234f1d12a",
      "name": "Nightly regression",
      "created_at": "2026-01-15T10:30:00Z",
      "consoleUrl": "https://console.devicecloud.dev/results?upload=7e12345f-eb12-12ec-a30b-bb1234f1d12a"
    }
  ],
  "total": 42,
  "limit": 20,
  "offset": 0
}
```

***

## Get upload status

Returns the current status and individual test results for a specific upload. Useful for polling a run in progress.

```
GET /uploads/status
```

**Query parameters**

| Parameter  | Type   | Required | Description                                                       |
| ---------- | ------ | -------- | ----------------------------------------------------------------- |
| `uploadId` | string | No\*     | The upload UUID.                                                  |
| `name`     | string | No\*     | The upload name. Returns the most recent match if multiple exist. |

\*One of `uploadId` or `name` is required.

**Example**

```bash
curl "https://api.devicecloud.dev/uploads/status?uploadId=7e12345f-eb12-12ec-a30b-bb1234f1d12a" \
  -H "x-app-api-key: <key>"
```

**Response**

```json
{
  "uploadId": "7e12345f-eb12-12ec-a30b-bb1234f1d12a",
  "status": "FAILED",
  "name": "Nightly regression",
  "createdAt": "2026-01-15T10:30:00Z",
  "appBinaryId": "67894274-b789-4c1e-80d4-da8998998999",
  "consoleUrl": "https://console.devicecloud.dev/results?upload=7e12345f-eb12-12ec-a30b-bb1234f1d12a",
  "tests": [
    {
      "name": "./login-test/onboarding.yaml",
      "status": "FAILED",
      "durationSeconds": 32,
      "failReason": "Element not found: Text matching regex next.* not found"
    }
  ]
}
```


# Results

All results endpoints operate on a specific upload identified by its UUID.

**Status values:** `PENDING`, `QUEUED`, `RUNNING`, `PASSED`, `FAILED`, `CANCELLED`

***

## Get results

Returns all test results for an upload, including status and failure reasons.

```
GET /results/:uploadId
```

**Example**

```bash
curl https://api.devicecloud.dev/results/7e12345f-eb12-12ec-a30b-bb1234f1d12a \
  -H "x-app-api-key: <key>"
```

**Response**

```json
{
  "statusCode": 200,
  "results": [
    {
      "id": 4500,
      "test_file_name": "./login-test/onboarding.yaml",
      "status": "FAILED",
      "fail_reason": "Element not found: Text matching regex next.* not found",
      "duration_seconds": 32,
      "retry_of": null
    }
  ]
}
```

***

## Download JUnit report

Returns a combined JUnit XML report for all tests in an upload. Compatible with most CI systems.

```
GET /results/:uploadId/report
```

**Example**

```bash
curl https://api.devicecloud.dev/results/7e12345f-eb12-12ec-a30b-bb1234f1d12a/report \
  -H "x-app-api-key: <key>" \
  -o report.xml
```

***

## Download HTML report

Returns a zipped HTML report with screenshots and logs for all tests in an upload.

```
GET /results/:uploadId/html-report
```

**Example**

```bash
curl https://api.devicecloud.dev/results/7e12345f-eb12-12ec-a30b-bb1234f1d12a/html-report \
  -H "x-app-api-key: <key>" \
  -o report.zip
```

***

## Download artifacts

Returns a zip file containing logs, screenshots, and videos. Filter to all results or failing only.

```
POST /results/:uploadId/download
```

**Body**

| Field     | Values          | Description                      |
| --------- | --------------- | -------------------------------- |
| `results` | `FAILED`, `ALL` | Which test artifacts to include. |

**Example**

```bash
curl -X POST https://api.devicecloud.dev/results/7e12345f-eb12-12ec-a30b-bb1234f1d12a/download \
  -H "x-app-api-key: <key>" \
  -H "Content-Type: application/json" \
  -d '{"results": "FAILED"}' \
  -o artifacts.zip
```


# Flows

The flows endpoints return aggregated analytics across all test runs for each flow file — the same data shown on the Flows page in the console.

Pass rates are calculated as `passed / (passed + failed)`, excluding cancelled and in-progress runs.

***

## Get flow summaries

Returns pass rates, run counts, average durations, and a daily status breakdown for every flow in your organisation.

```
GET /flows
```

**Query parameters**

| Parameter   | Type   | Required | Description                                                                                        |
| ----------- | ------ | -------- | -------------------------------------------------------------------------------------------------- |
| `platform`  | string | No       | Filter by platform: `android` or `ios`.                                                            |
| `appId`     | string | No       | Filter by app ID (e.g. `com.example.app`).                                                         |
| `days`      | number | No       | Lookback window in days. Default: `14`. Ignored if `startDate` is set.                             |
| `startDate` | string | No       | Start of date range (ISO 8601). Overrides `days`.                                                  |
| `endDate`   | string | No       | End of date range (ISO 8601). Defaults to now if `startDate` is set.                               |
| `tags`      | string | No       | Comma-separated tag filter. Returns flows that have any of the given tags (e.g. `smoke,critical`). |

**Example**

```bash
curl "https://api.devicecloud.dev/flows?platform=ios&startDate=2026-01-01&endDate=2026-01-31" \
  -H "x-app-api-key: <key>"
```

**Response**

```json
{
  "statusCode": 200,
  "flows": [
    {
      "flow_name": "Login flow",
      "file_name": "src/flows/login.yaml",
      "pass_rate": 94,
      "passed_runs": 47,
      "failed_runs": 3,
      "total_runs": 50,
      "avg_duration": 38,
      "last_run_at": "2026-01-31T14:22:00Z",
      "tags": ["smoke", "critical"],
      "daily_data": {
        "2026-01-31": "passed",
        "2026-01-30": "mixed",
        "2026-01-29": "passed"
      }
    }
  ]
}
```

**`daily_data` values:** `passed` (all runs passed), `failed` (all runs failed), `mixed` (both), `null` (no runs that day)

***

## Get runs for a flow

Returns individual run history for a specific flow file.

```
GET /flows/runs
```

**Query parameters**

| Parameter   | Type   | Required | Description                                       |
| ----------- | ------ | -------- | ------------------------------------------------- |
| `fileName`  | string | Yes      | The flow file path (e.g. `src/flows/login.yaml`). |
| `platform`  | string | No       | Filter by platform: `android` or `ios`.           |
| `appId`     | string | No       | Filter by app ID.                                 |
| `limit`     | number | No       | Maximum number of runs to return. Default: `100`. |
| `startDate` | string | No       | Start of date range (ISO 8601).                   |
| `endDate`   | string | No       | End of date range (ISO 8601).                     |

**Example**

```bash
curl "https://api.devicecloud.dev/flows/runs?fileName=src/flows/login.yaml&platform=ios" \
  -H "x-app-api-key: <key>"
```

**Response**

```json
{
  "statusCode": 200,
  "runs": [
    {
      "id": 4500,
      "status": "PASSED",
      "createdAt": "2026-01-31T14:22:00Z",
      "durationSeconds": 35,
      "failReason": null,
      "testUploadId": "7e12345f-eb12-12ec-a30b-bb1234f1d12a",
      "uploadName": "Nightly regression"
    }
  ]
}
```


# IP Addresses

Returns the current list of DeviceCloud test-runner egress IP addresses, so your DevOps tooling can automate firewall allow-listing instead of updating it by hand whenever the addresses change.

Unlike the other REST API endpoints, this one is **public** — no API key is required — so automation can poll it without credentials. Responses are cacheable (`Cache-Control: public, max-age=3600`); polling once an hour is plenty.

The same data is published in human-readable form on the [IP Addresses](/security-and-compliance/ip-addresses) security page, which is generated from this endpoint — so the two never drift.

***

## Get IP addresses

```
GET /ip-addresses
```

**Example**

```bash
curl https://api.devicecloud.dev/ip-addresses
```

**Response**

```json
{
  "updatedAt": "2026-07-06",
  "ipAddresses": [
    "46.17.215.144",
    "46.17.215.145",
    "83.217.174.249"
  ],
  "ranges": [
    {
      "cidr": "46.17.215.144/32",
      "platforms": ["android", "ios"],
      "type": "egress",
      "description": "Test runner egress"
    }
  ]
}
```

**Fields**

| Field                  | Type       | Description                                                                                                    |
| ---------------------- | ---------- | -------------------------------------------------------------------------------------------------------------- |
| `updatedAt`            | `string`   | ISO date (`YYYY-MM-DD`) the list last changed. Watch this to detect updates without diffing the whole payload. |
| `ipAddresses`          | `string[]` | Flat list of bare IP addresses — the simplest thing to allow-list.                                             |
| `ranges`               | `object[]` | Structured view of each range (see below).                                                                     |
| `ranges[].cidr`        | `string`   | The range in CIDR notation. Single hosts are expressed as `/32`.                                               |
| `ranges[].platforms`   | `string[]` | Runner platforms whose traffic egresses from this range — `android`, `ios`, or both.                           |
| `ranges[].type`        | `string`   | Traffic type. Currently always `egress` (outbound from the app under test).                                    |
| `ranges[].description` | `string`   | Human-readable note about the range.                                                                           |

> **Tip:** If your firewall accepts CIDR notation, allow-list `ranges[].cidr`; otherwise use the flat `ipAddresses` list. Poll on a schedule and reconcile against `updatedAt` to keep your allow-list in sync automatically.


# MCP Server

The `@devicecloud.dev/dcd` package ships a second binary, `dcd-mcp` — a [Model Context Protocol](https://modelcontextprotocol.io) server that lets AI agents (Claude, Cursor, VS Code, and other MCP clients) drive DeviceCloud directly: list devices, submit cloud test runs, check status, and download artifacts.

{% hint style="warning" %}
The MCP server is a new, beta capability. The tool surface may change.
{% endhint %}

{% hint style="warning" %}
Please note that we accept no liability for anything your agent(s) may read or execute when using our MCP server. LLMs are a new technology and should be used with caution at your own risk.
{% endhint %}

## Setup

Add the server to your MCP client's configuration. It runs over stdio via `npx`, so there's nothing to install separately:

```jsonc
{
  "mcpServers": {
    "devicecloud": {
      "command": "npx",
      "args": ["-y", "@devicecloud.dev/dcd", "dcd-mcp"],
      "env": { "DEVICE_CLOUD_API_KEY": "<your-api-key>" }
    }
  }
}
```

## Authentication

Auth is inherited from the CLI:

* Set `DEVICE_CLOUD_API_KEY` in the server's `env` (as above), **or**
* Run [`dcd login`](/cli-reference/dcd-login) once and the server will pick up the stored session.

When both are present, the environment variable wins.

Credentials are resolved lazily on the first tool call, not at startup — so your client can connect and enumerate the tools before you've supplied a key, and a bad credential surfaces as a tool error rather than a server that won't start.

## Tools

The server exposes five tools. Four are read-only; `dcd_run_cloud_test` submits a run and is billable.

| Tool                                                                        | What it does                                                  |
| --------------------------------------------------------------------------- | ------------------------------------------------------------- |
| [`dcd_list_devices`](/ai-agents-and-mcp/tools#dcd-list-devices)             | Discover available devices, OS versions, and Maestro versions |
| [`dcd_list_runs`](/ai-agents-and-mcp/tools#dcd-list-runs)                   | List recent test runs (filter by name/date, paginated)        |
| [`dcd_get_status`](/ai-agents-and-mcp/tools#dcd-get-status)                 | Get the status and per-test results of a run                  |
| [`dcd_download_artifacts`](/ai-agents-and-mcp/tools#dcd-download-artifacts) | Download a run's artifacts or report to disk                  |
| [`dcd_run_cloud_test`](/ai-agents-and-mcp/tools#dcd-run-cloud-test)         | Submit a flow to run on the cloud (**billable**)              |

See [Tools Reference](/ai-agents-and-mcp/tools) for every parameter, default, and return shape.

## Read-only mode

`dcd_run_cloud_test` consumes test credits, so it's annotated as a destructive/non-read-only tool so well-behaved clients can (and should) prompt before calling it. To hide it entirely (recommended for autonomous or untrusted agents), either:

* pass `--read-only` in `args`, or
* set `DCD_MCP_READONLY=1` in `env`.

The remaining tools are all read-only. A read-only server doesn't just refuse the tool — it never advertises it, so an agent can't attempt a billable run at all:

```jsonc
{
  "mcpServers": {
    "devicecloud": {
      "command": "npx",
      "args": ["-y", "@devicecloud.dev/dcd", "dcd-mcp", "--read-only"],
      "env": { "DEVICE_CLOUD_API_KEY": "<your-api-key>" }
    }
  }
}
```


# Tools Reference

Every tool the [MCP server](/ai-agents-and-mcp/overview) exposes, with its parameters and return shape.

All tools return pretty-printed JSON as their text payload. Failures come back as an error result the agent can read and react to — a failing tool never tears down the server, so an agent can recover and retry.

{% hint style="warning" %}
The MCP server is a new, beta capability. The tool surface may change.
{% endhint %}

## dcd\_list\_devices <a href="#dcd-list-devices" id="dcd-list-devices"></a>

Discover the devices, OS versions, and Maestro versions available to your organisation. Agents should call this **before** `dcd_run_cloud_test` so they pass valid device values.

Takes no parameters.

Returns `{ ios, android, androidPlay, maestro }`, where each platform maps an OS version to its supported device list. See [Devices & OS Versions](/getting-started/devices-configuration) and [Maestro Versions](/configuration/maestro-versions).

## dcd\_list\_runs <a href="#dcd-list-runs" id="dcd-list-runs"></a>

List recent flow uploads for your organisation, most recent first.

| Parameter | Type    | Description                                                            |
| --------- | ------- | ---------------------------------------------------------------------- |
| `name`    | string  | Filter by upload name. Supports a `*` wildcard, e.g. `nightly-*`       |
| `from`    | string  | Only uploads created on or after this ISO 8601 date, e.g. `2024-01-01` |
| `to`      | string  | Only uploads created on or before this ISO 8601 date                   |
| `limit`   | integer | Maximum uploads to return, 1–100 (default `20`)                        |
| `offset`  | integer | Uploads to skip, for pagination (default `0`)                          |

Returns `{ uploads: [{ id, name, created_at, consoleUrl }], total, limit, offset }`. Use a returned `id` with `dcd_get_status` or `dcd_download_artifacts`.

## dcd\_get\_status <a href="#dcd-get-status" id="dcd-get-status"></a>

Get the status and per-test results of a single upload. This is the polling primitive: after an async `dcd_run_cloud_test`, call this until `status` leaves `PENDING` / `QUEUED` / `RUNNING`.

| Parameter  | Type   | Description                                                                |
| ---------- | ------ | -------------------------------------------------------------------------- |
| `uploadId` | string | UUID of the upload, as returned by `dcd_run_cloud_test` or `dcd_list_runs` |
| `name`     | string | Name of the upload to look up                                              |

Provide **exactly one** of `uploadId` or `name` — passing both, or neither, is an error.

Returns `{ status, name, createdAt, tests: [{ name, status, durationSeconds, failReason }] }`.

## dcd\_download\_artifacts <a href="#dcd-download-artifacts" id="dcd-download-artifacts"></a>

Download a completed run's artifacts (the videos/logs zip) and/or a formatted report to local disk. It reads cloud state and writes local files, so it stays available in [read-only mode](/ai-agents-and-mcp/overview#read-only-mode).

| Parameter       | Type                          | Description                                                                                     |
| --------------- | ----------------------------- | ----------------------------------------------------------------------------------------------- |
| `uploadId`      | string                        | **Required.** UUID of the upload to download artifacts for                                      |
| `type`          | `ALL` \| `FAILED`             | Which tests to include artifacts for (default `ALL`)                                            |
| `artifactsPath` | string                        | Where to write the artifacts zip (default `./artifacts.zip`)                                    |
| `report`        | `junit` \| `allure` \| `html` | Also download a formatted report of this type                                                   |
| `reportPath`    | string                        | Where to write the report (defaults by type: `report.xml` for `junit`, otherwise `report.html`) |

Returns `{ uploadId, artifactsPath, reportPath, warnings }`. A **non-empty `warnings` array means a download could not be produced** — most often because the run has no results yet. An empty array means success. See [Artifacts & Downloads](/test-artifacts-and-reports/artifacts) and [Report Formats](/test-artifacts-and-reports/report-formats).

## dcd\_run\_cloud\_test <a href="#dcd-run-cloud-test" id="dcd-run-cloud-test"></a>

Submit a Maestro flow, or a directory of flows, to run on DeviceCloud.

{% hint style="warning" %}
This tool consumes test credits. It's annotated as destructive so clients can prompt before calling it, and it's hidden entirely in [read-only mode](/ai-agents-and-mcp/overview#read-only-mode).
{% endhint %}

| Parameter            | Type                                          | Description                                                                                   |
| -------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `flowFile`           | string                                        | **Required.** Path to a Maestro `.yaml`/`.yml` flow, or a directory of flows                  |
| `appFile`            | string                                        | Path to a local app binary — `.apk`, `.app`, or `.zip`. Mutually exclusive with `appBinaryId` |
| `appBinaryId`        | string                                        | ID of a previously uploaded binary. Mutually exclusive with `appFile`                         |
| `iosVersion`         | string                                        | iOS version, e.g. `17`                                                                        |
| `iosDevice`          | string                                        | iOS device, e.g. `iphone-14`                                                                  |
| `androidApiLevel`    | string                                        | Android API level, e.g. `34`                                                                  |
| `androidDevice`      | string                                        | Android device, e.g. `pixel-7`                                                                |
| `googlePlay`         | boolean                                       | Use a [Google Play-enabled](/configuration/google-play-apis) Android image                    |
| `name`               | string                                        | A name for this run                                                                           |
| `env`                | string\[]                                     | [Environment variables](/configuration/environment-variables) as `KEY=VALUE` strings          |
| `includeTags`        | string\[]                                     | Only run flows with these tags                                                                |
| `excludeTags`        | string\[]                                     | Skip flows with these tags                                                                    |
| `excludeFlows`       | string\[]                                     | Flow paths/patterns to exclude                                                                |
| `maestroVersion`     | string                                        | Pin a specific [Maestro version](/configuration/maestro-versions)                             |
| `retry`              | integer                                       | Auto-[retry](/advanced/retry-strategies) failed tests, 0–2                                    |
| `runnerType`         | `default` \| `m4` \| `m1` \| `gpu1` \| `cpu1` | [Runner type](/configuration/runner-type) (default `default`)                                 |
| `configFile`         | string                                        | Path to a [workspace config](/configuration/workspace-config)                                 |
| `ignoreShaCheck`     | boolean                                       | Force re-upload of the binary even if an identical one already exists                         |
| `dryRun`             | boolean                                       | Preview which flows would run, without submitting                                             |
| `wait`               | boolean                                       | Block until the run completes instead of returning immediately (default `false`)              |
| `waitTimeoutSeconds` | integer                                       | Max seconds to wait when `wait` is true, 30–3600 (default `600`)                              |

You must provide either `appFile` or `appBinaryId` — not both, and not neither. Use [`dcd upload`](/cli-reference/dcd-upload) or a previous run to get a reusable `appBinaryId`.

### Async by default

By default the tool returns as soon as the run is submitted:

```json
{ "uploadId": "...", "consoleUrl": "...", "status": "PENDING", "tests": [ ... ] }
```

Poll `dcd_get_status` with that `uploadId` until the status is terminal. This is the recommended shape for agents — it keeps the tool call short and avoids a long-blocking request.

Set `wait: true` to block instead. The tool polls every 10 seconds up to `waitTimeoutSeconds`, then returns with `timedOut: true` if the run hasn't finished. **The run continues in the cloud regardless** — a timeout is not a cancellation, and you can resume by polling `dcd_get_status` with the returned `uploadId`.

### Previewing a run

`dryRun: true` resolves the flows and returns them without submitting anything or spending credits:

```json
{
  "dryRun": true,
  "flows": [{ "file": "...", "flowName": "...", "tags": [ ... ] }],
  "sequentialFlowCount": 0
}
```

This is a cheap way for an agent to confirm it's about to run the flows you expect before it calls the billable path for real.


# Async Execution

By default, `dcd cloud` waits for all tests to complete before exiting. Async mode lets you start a test run and return immediately — useful when you want to avoid blocking your CI pipeline.

## Basic Usage

```bash
dcd cloud <appFile> <flowFile> --async
```

When tests are submitted successfully, the command exits with code `0` regardless of test outcome. If submission itself fails, it exits with code `1`.

Pair `--async` with `--name` to make it easy to look up results later:

```bash
dcd cloud <appFile> <flowFile> --async --name "build-$GIT_SHA"
```

## Checking Results After an Async Run

Use the `dcd status` command to poll for results by upload ID:

```bash
dcd status --upload-id <uploadId>
```

You can also look a run up by name with `--name` (handy when paired with `--async --name`). See [dcd status](/cli-reference/dcd-status) for the full reference.

## GitHub Actions Pattern

In GitHub Actions, async mode is useful when you want to kick off tests and check results in a later step, while other CI work continues in parallel:

```yaml
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Build app
        run: ./gradlew assembleDebug

      # Start tests immediately — don't wait for results
      - uses: devicecloud-dev/device-cloud-for-maestro@v2
        id: dcd
        with:
          api-key: ${{ secrets.DCD_API_KEY }}
          app-file: app/build/outputs/apk/debug/app-debug.apk
          async: true
          name: ${{ github.sha }}

      # Continue with other CI work while tests run in the background
      - name: Run unit tests
        run: ./gradlew test

      # Check test results at the end
      - name: Verify DeviceCloud status
        run: |
          echo "Test results: ${{ steps.dcd.outputs.DEVICE_CLOUD_UPLOAD_STATUS }}"
          echo "View at: ${{ steps.dcd.outputs.DEVICE_CLOUD_CONSOLE_URL }}"
```

## Considerations

* The console URL is available via the `DEVICE_CLOUD_CONSOLE_URL` action output so you can link to results from your CI summary.
* Retries (`--retry`) and async mode work together — DeviceCloud handles retries in the background.
* If you need the final pass/fail status in CI, use `dcd status` to poll or use the [dcd status](/cli-reference/dcd-status) directly.
* With the [DeviceCloud GitHub App](/ci-cd-integration/github-checks) installed, an async run reports its result back as a pass/fail check on the pull request, so you don't have to poll for it yourself.


# Retry Strategies

Failed tests can be automatically retried using the `--retry` flag. Retries are free of charge and capped at 2, giving a maximum of 3 total runs per test. If needed, you can queue additional retries on the [console](https://console.devicecloud.dev/results).

## Usage

```bash
dcd cloud app.apk flows/ --retry 2
```

If a test fails, DeviceCloud will re-run the test up to the specified number of times, stopping as soon as a run passes. The final status reflects the last run. You can view the other results using the dropdown menu on the console.

## Limitations

* The maximum value is `--retry 2` (3 runs total)
* Only failed test executions are retried — configuration errors (invalid flow files, authentication failures, missing dependencies) will not be retried
* Retries count toward your concurrency limit while running

{% hint style="info" %}
If your test is still failing after 2 retries, reach out on [Discord](https://discord.gg/gm3mJwcNw8) for help diagnosing the root cause.
{% endhint %}


# Chrome Onboarding

Chrome Onboarding pre-configures Chrome browser settings before your tests run on Android devices, ensuring that first-run dialogs don't interfere with test execution.

### Onboarding Methods

By default we use a config-based Chrome onboarding which adds minimal overhead and works for most scenarios.

If config-based onboarding fails, we automatically switch to Maestro-based onboarding which uses a Maestro script. While more thorough, Maestro-based onboarding adds significant overhead as it runs an additional flow on the device before your tests.

### Forcing Maestro-based Onboarding

If you're experiencing issues with Chrome, you can force Maestro-based onboarding using the CLI flag `--maestro-chrome-onboarding`.

**Important:** Using Maestro-based onboarding **will slow your test suite down**. We therefore we only recommend using it if you are experiencing issues with the default config-based method.


# Exit Codes

Device Cloud uses standard exit codes to indicate the status of command execution. Understanding these codes is useful for CI/CD integration and automation.

## Standard Exit Codes

### Success

```bash
0    # Command completed successfully
```

### General Errors

```bash
1    # CLI or infrastructure error (bad workspace, network failure, dcd bug)
2    # Test run explicitly failed
```

## JSON Output

The two JSON flags behave differently around test failures:

* **`--json`** — prints results as JSON to stdout but still signals the outcome through the exit code: `0` on success, `2` on test failure, `1` on CLI/infrastructure errors.
* **`--json-file`** — writes results to a file and exits `0` even when the test run fails, so a failing test won't stop your pipeline. CLI/infrastructure errors still exit `1`.

{% hint style="info" %}
Use `--json-file` when you want to inspect the result yourself rather than have a non-zero exit code fail the build. Use `--json` when you still want the exit code to gate your pipeline.
{% endhint %}


# Security Overview

As experienced engineers, we take security seriously and adopt industry standards wherever possible. If you have specific questions not covered by this document, please contact us via email - <support@devicecloud.dev> - we are happy to answer your security questionnaires.

### SOC2 Compliance Timeline

DeviceCloud has begun preparing and organising policies and controls that align with the SOC2 Type 1 compliance framework. The target compliance date is currently Q1 2027.

Want to work with DeviceCloud but require SOC2? Please contact us, we will fast-track this process if enough users request it.

### Infrastructure

DeviceCloud stores customer data and performs processing using Supabase and Railway. These cloud platforms are SOC2 compliant. Some test execution is done using our dedicated secure data centre in the UK.

### Data Encryption <a href="#data-encryption" id="data-encryption"></a>

All user data is encrypted at rest with AES-256 and in transit via TLS. Additionally, sensitive information such as access tokens and keys is encrypted at the application level before being stored in the database.

### Data Retention <a href="#data-encryption" id="data-encryption"></a>

Uploads (both binaries and workflow files) are automatically deleted 1 month after last use; result data including maestro logs, screenshots and videos are retained for 6 months. All account data is permanently deleted when you request account deletion. This can be done by contacting support or through the console UI.

Android emulators are instantiated into a clean operating system instance for every test run. iOS simulators undertake an extensive teardown process to remove all cached data, but the underlying operating system persists due to Apple licensing requirements. All customer test data is completely removed from the test runner as soon as the test is finished.

### Payments <a href="#payments" id="payments"></a>

DeviceCloud uses Paddle to handle payments. Paddle is PCI DSS SAQ A and SOC2 Type 1 compliant.

### Code Control <a href="#payments" id="payments"></a>

All code and configurations deployed inside DeviceCloud undergo a thorough code control process, which includes security considerations. All code is authored by UK-based employees.


# IP Addresses

The DeviceCloud Test Runner IP addresses are listed below. Add these to your firewall allowlist to permit traffic between the app under test and any servers it needs to reach.

| IP range            | Platforms    | Purpose            |
| ------------------- | ------------ | ------------------ |
| `46.17.215.144/32`  | Android, iOS | Test runner egress |
| `46.17.215.145/32`  | Android, iOS | Test runner egress |
| `83.217.174.249/32` | Android, iOS | Test runner egress |

## Automate your allow-list

These addresses are also published as an unauthenticated JSON endpoint, so your DevOps tooling can poll for the current list instead of tracking this page by hand. This endpoint is the source of truth for the table above, so the two never drift:

```
GET https://api.devicecloud.dev/ip-addresses
```

The response includes a flat `ipAddresses` array (for simple consumers) and a structured `ranges` array (CIDR, platforms, purpose), plus an `updatedAt` date you can watch for changes.

See the [IP Addresses API reference](/rest-api/ip-addresses) for the full response schema and examples.

*Last updated: 2026-07-06.*


# IT Security Policies

DeviceCloud implements a number of internal security policies to help protect data and systems.

You may request access to these policies by emailing <support@devicecloud.dev>

The policies available are:

1. Acceptable Use Policy
2. Access Control Policy
3. Authentication and Password Policy
4. Backup Policy
5. Change Management Policy
6. Data Classification Policy
7. Data Retention and Disposal Policy
8. Encryption Policy
9. Human Resources Policy
10. Security Incident Management Policy
11. Vendor Management Policy


# Enterprise SSO

SAML-based authentication for larger companies

{% hint style="info" %}
This feature is currently available to orgs who have the Max subscription or above.
{% endhint %}

Single sign-on solutions (such as OKTA) allow organisations to centrally manage user access to DeviceCloud.

We support the following identity providers:

* Okta, Auth0
* Google Workspaces (formerly known as GSuite)
* Microsoft Active Directory, Azure Active Directory, Microsoft Entra
* PingIdentity
* OneLogin

Using a provider that is not on the list? Contact us and we'd be happy to help setup the integration.

#### Integration Steps

These steps have been written with OKTA in mind, but are applicable to other identity providers too.

1. Create a new application with SAML in OKTA
2. Populate the SAML fields using the below values.

| Key                          | Value                                                     |
| ---------------------------- | --------------------------------------------------------- |
| Single sign-on URL (ACS URL) | `https://cloud.devicecloud.dev/auth/v1/sso/saml/acs`      |
| Audience URI (SP Entity ID)  | `https://cloud.devicecloud.dev/auth/v1/sso/saml/metadata` |
| Default Relay State          | `https://cloud.devicecloud.dev/`                          |

3. Ensure the application username is set to EMAIL
4. Ensure the Name ID Format is set also to EMAIL
5. Generate your metadata URL (or XML file for some providers)
6. Send the following to <support@devicecloud.dev> from an email address registered with DeviceCloud:
   1. your metadata URL (or XML)
   2. the email domain(s) you wish to protect
7. Our support team will then verify you have purchased the required amount of credits and enforce SSO for the requested domains.
8. Once activated, DeviceCloud supports Service Provider initiated login via SSO using the SSO button on the DeviceCloud login screen:

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


# Subscriptions

DeviceCloud offers subscriptions for use of the service.

These are being rolled out during 2026 and may not be available to all users initially. Reach out to support if you need help.

### Pro Plan

This subscription costs $25 per month and comes with $25 of usage included which can be used as per [Test Run Billing](/billing/test-run-billing).

Additional usage beyond $25 will be billed as overage and charged in your next monthly payment.

This plan lets you run up to 5 iOS and 5 Android tests in parallel. Read about [Concurrency & Parallel Runs](/getting-started/concurrency-and-parallel-runs) to understand how this may be reduced at peak times.

If you require support when using this plan, you can contact our team and community via [Discord](https://discord.gg/gm3mJwcNw8).\
\
This plan supports unlimited users and unlimited apps. Results are retained for 6 months.

### Max Plan

This subscription costs $200 per month and comes with $200 of usage included which can be used as per [Test Run Billing](/billing/test-run-billing).

Additional usage beyond $200 will be billed as overage and charged in your next monthly payment.

This plan lets you run up to 20 iOS and 20 Android tests in parallel.

In addition, it offers **Concurrency Protection** to ensure you always have at least 1 test running even at very busy times. Read more [Concurrency & Parallel Runs](/getting-started/concurrency-and-parallel-runs) to understand how this works.

This plan supports [Enterprise SSO](/security-and-compliance/enterprise-single-sign-on) and priority support via email.

This plan supports unlimited users and unlimited apps. Results are retained for 12 months.

### Enterprise Plan

This subscription starts at $6000 per year and allows for custom terms including volume discounts.

This plan is paid by invoice and includes private, dedicated support via Slack as standard.

Please reach out to <sales@devicecloud.dev> for more info.


# Test Run Billing

{% hint style="info" %}
All amounts exclude Sales Tax - this will be added at checkout for your country by our Merchant of Record, Paddle.
{% endhint %}

DeviceCloud debits customers account per test run.

In billing terms, a test run is a single top-level flow executing on a single device. Multiple flows in an upload will incur multiple charges.

For example, an upload with 10 flows running on iOS (at $0.11 per test run) will incur $1.10 of debits to the customer account.

Details of charges made to your account can be found on the [billing page](https://console.devicecloud.dev/billing).

| Test Run Type    | Includes                                                                                                                 | Cost (USD) |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------ | ---------- |
| Standard iOS     | Any iPhone device on our default [runner type](/configuration/runner-type)                                               | $0.11      |
| Advanced iOS     | Any runs on our `m4` [runner type](/configuration/runner-type) and any iPad runs (on either runner).                     | $0.15      |
| Standard Android | Any non-Play Pixel phone on our cpu1 [runner type](/configuration/runner-type)                                           | $0.09      |
| Advanced Android | Any runs on our `gpu1` and `m1` [runner types](/configuration/runner-type) and Tablet or Play devices (on either runner) | $0.15      |

{% hint style="info" %}
Enterprise customers with volume discounts or grandfathered users with older pricing may have different pricing. You can view your account's test run cost in the [billing page](https://console.devicecloud.dev/billing)
{% endhint %}

### Credits

Historically, DeviceCloud users have purchased credits in advance and these have been drawn down via usage.

{% hint style="info" %}
Note: credits are only valid for 12 months, after which they automatically expire.
{% endhint %}

New users are given $20 in free credits to test the service.

The ability to purchase credits is being phased out in favour of [subscriptions](/billing/subscriptions).


# Terms & Conditions

These are the terms and conditions of devicecloud.dev (the “Platform”), operated by Moropo Limited (the "Company")

By using the Platform, you are deemed to accept these terms and conditions, and a legally binding contract is formed between the Company and you. If you do not accept these terms and conditions, please do not use the Platform. You also agree to the terms of our privacy policy.

These terms and conditions apply to any user of the Platform and, where you purchase a subscription for the Platform on behalf of a company or organisation, you warrant and guarantee that you have authority to bind that organisation, and that all users will comply with these terms and conditions when using the Platform.

We may amend these terms and conditions from time to time, and such amendments shall apply to your use of the Platform. Please check back regularly to ensure you are aware of the terms and conditions that apply to you.

‍

**1. The Platform**

1.1 The Company is the exclusive owner of the Platform, and the Company and/or its licensors own all Intellectual Property rights in the Platform. The Company grants to you a non-exclusive, non-transferable, non-sublicensable, royalty-free licence to use the Platform, subject to these terms and conditions (the “**Licence**”). The Platform may be accessed by you via a web browser or other device owned or controlled by you. The Platform is designed to allow you to connect and automate your mobile app development workflow. The Platform utilises open source software, which is attributed [Open Source Notices](/legal/open-source-notices)‍

1.2 You acknowledge that you are licensed to use the Platform only in accordance with these express terms and conditions, and not further or otherwise. Nothing in these terms and conditions transfers any ownership right in the Platform to you or any other party.

1.3 The Company may modify, add or remove functionality of the Platform from time to time, without liability to you or any third party.

‍

**2. Credits**

2.1 The Platform grants each team account $20 USD for trial and experimentation purposes. This credit cannot be redeemed for anything other than test runs and may be withdrawn at any time.

2.2 Users may purchase additional credit via the billing page in the Platform. Purchased credits are valid for 12 months when offered as one-off purchases or for the subscription period (typically one month) when purchased as part of a subscription.

2.3 The Company may suspend your access to the Platform, and terminate the Licence, at any time where you (or a member of your organisation) have materially breached these terms and conditions (which includes failing to make payment). Where the Company does so, no refunds will be provided for the then-current period of your subscription. Read the full [Refund Policy](/legal/refund-policy)

2.4 Where your Licence ends for any reason, you will no longer have access to the information stored on the Platform. The Company has no obligation to maintain any of your data following termination, and is permitted to delete it once your Licence ends. The Company reserves the right, although is not obliged to, retain your data for a short period for record keeping purposes, or to ensure that any such termination of your Licence has not been inadvertent.

‍

**3. The Parties’ Obligations**

3.1 The Company is not obliged to provide copies of the Platform (including copies of any source code) to you.

3.2 You agree to use the Platform in compliance with applicable law and regulation.

3.3 The Company will provide you with basic support, at no additional charge, via the website for the Platform.

3.4 The Company uses commercially reasonable endeavours to ensure the Platform is available 24 hours a day, 7 days a week, but is not liable for any downtime or where the Platform is not available to you. Circumstances may occur that are outside the Company’s control (including but not limited to acts of God, floods, fires, earthquakes, pandemics, civil unrest, acts of terror, failure of internet Platform providers or other third party contractors), and the Company is not responsible for failure to provide the Platform in such circumstances.

3.5 The Company has implemented, and maintains, safeguards to ensure the protection and security of your data that is uploaded to the Platform. The Company complies with applicable data protection legislation, including the UK Data Protection Act 2018. For more information on how the Company processes and stores personal data, please see the Company’s [Privacy Policy](/legal/privacy-policy).

3.6 You are responsible for the quality, accuracy and completeness of your data. The Company does not verify or evaluate your data, nor does the Company advise you on any compliance or operational-related matters in any way. The Platform does not provide legal or regulatory guidance regarding compliance obligations in certain locations or industries. If you require such advice, please contact an independent professional. You are the owner of your data, and nothing in these terms and conditions transfers any such ownership to the Company.‍

3.7 You shall only permit authorised users, who are assigned unique user credentials (“**Authorised Users**”) to use the Platform, and use appropriate administrative controls to ensure that Authorised Users do not share access credentials with each other or any third party. You are solely responsible for the acts or omissions of your Authorised Users or any third parties who obtain access to the Platform through your usage. You will notify the Company promptly if you become aware of any unauthorised access or use.

‍

**4. Warranties and Liability**

4.1 The Company represents and warrants that:

4.1.1 the Platform will perform substantially in accordance with the specifications provided by the Company;

4.1.2 the Platform will comply with applicable data protection legislation; and

4.1.3 the Company will adhere to its privacy policy regarding your personal data.

4.2 The Company disclaims all other warranties, express or implied, including implied warranties or merchantability or fitness for a particular purpose, with respect to the Platform or any of our services. The Company does not warrant that the Platform will be uninterrupted or error-free.

4.3 Neither you nor the Company will be liable for any indirect, special or consequential loss arising under these terms and conditions.‍

4.4 The Company’s liability arising under, or in relation to, these terms and conditions shall be limited to the total fees paid by you during a Subscription Term.

4.5 You agree to indemnify and hold harmless the Company against any costs, damages, expenses, losses and other liabilities incurred as a result of a third party action, claim, demand, proceeding or suit where this arises from or in connection with your use of the Platform in violation of these terms and conditions, or any decision or action you take due to information available through your use of the Platform.

4.6 The Company accepts no liability for any decision you may take as a result of your use of the Platform.

4.7 The Company agrees to indemnify you and hold you harmless against any costs, damages, expenses, losses and other liabilities incurred as a result of a third party action, claim, demand, proceeding or suit where this arises from or in connection with your use of the Platform where this infringes the intellectual property rights of a third party. This indemnity shall not apply where you have used the Platform in an unauthorised or modified form.

**5. General**

5.1 If any part of these terms and conditions is or becomes invalid, illegal or unenforceable, it is deemed to be deleted, and the rest of these terms and conditions shall remain in full force and effect.

5.2 No third parties are provided with any rights under these terms and conditions, and the Contracts (Rights of Third Parties) Act 1999 does not apply.

5.3 Either party may give notice to the other party by sending an email to the last email address notified to the other party. Time of delivery is deemed to be the time of transmission. This shall not apply to the Platform of legal proceedings.

5.4 These terms and conditions, and any non-contractual obligations arising hereunder, shall be governed and construed in accordance with the laws of England and Wales, and the English courts shall have exclusive jurisdiction to deal with any dispute arising hereunder.

**6. Enterprise**

6.1 You may enter into an Enterprise Agreement for use of the Platform at the discretion of the Company.

6.2 Enterprise Agreements are annual unless otherwise stated in the devicecloud.dev Order Form.

6.3 Enterprise Agreements automatically renew every 12 months unless either party provides 90 days' written notice before the renewal date.

6.4 Enterprise Agreement customers gain the benefits of the [Service Level Agreements](/legal/service-level-agreements)


# Privacy Policy

This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.

We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy.

### Interpretation and Definitions

#### Interpretation

The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.

#### Definitions

For the purposes of this Privacy Policy:

* **Account** means a unique account created for You to access our Service or parts of our Service.
* **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to devicecloud.dev
* **Cookies** are small files that are placed on Your computer, mobile device or any other device by a website, containing the details of Your browsing history on that website among its many uses.
* **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet.
* **Personal Data** is any information that relates to an identified or identifiable individual.
* **Service** refers to the Website.
* **Service Provider** means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
* **Usage Data** refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
* **Website** refers to devicecloud.dev
* **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.

### Collecting and Using Your Personal Data

#### Types of Data Collected

**Personal Data**

While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to:

* Email address
* First name and last name
* Phone number
* Usage Data

**Usage Data**

Usage Data is collected automatically when using the Service.

Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.

When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.

We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.

**Tracking Technologies and Cookies**

We use Cookies and similar tracking technologies to track the activity on Our Service and store certain information. Tracking technologies used are beacons, tags, and scripts to collect and track information and to improve and analyze Our Service. The technologies We use may include:

* **Cookies or Browser Cookies.** A cookie is a small file placed on Your Device. You can instruct Your browser to refuse all Cookies or to indicate when a Cookie is being sent. However, if You do not accept Cookies, You may not be able to use some parts of our Service. Unless you have adjusted Your browser setting so that it will refuse Cookies, our Service may use Cookies.
* **Web Beacons.** Certain sections of our Service and our emails may contain small electronic files known as web beacons (also referred to as clear gifs, pixel tags, and single-pixel gifs) that permit the Company, for example, to count users who have visited those pages or opened an email and for other related website statistics (for example, recording the popularity of a certain section and verifying system and server integrity).

Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on Your personal computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close Your web browser.

We use both Session and Persistent Cookies for the purposes set out below:

* **Necessary / Essential Cookies**
* Type: Session Cookies
* Administered by: Us
* Purpose: These Cookies are essential to provide You with services available through the Website and to enable You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to provide You with those services.
* **Cookies Policy / Notice Acceptance Cookies**
* Type: Persistent Cookies
* Administered by: Us
* Purpose: These Cookies identify if users have accepted the use of cookies on the Website.
* **Functionality Cookies**
* Type: Persistent Cookies
* Administered by: Us
* Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your login details or language preference. The purpose of these Cookies is to provide You with a more personal experience and to avoid You having to re-enter your preferences every time You use the Website.

For more information about the cookies we use and your choices regarding cookies, please visit our Cookies Policy or the Cookies section of our Privacy Policy.

#### Use of Your Personal Data

The Company may use Personal Data for the following purposes:

* **To provide and maintain our Service**, including to monitor the usage of our Service.
* **To manage Your Account:** to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.
* **For the performance of a contract:** the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.
* **To contact You:** To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.
* **To provide You** with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.
* **To manage Your requests:** To attend and manage Your requests to Us.
* **For business transfers:** We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred.
* **For other purposes**: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.

We may share Your personal information in the following situations:

* **With Service Providers:** We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You.
* **For business transfers:** We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company.
* **With Affiliates:** We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.
* **With business partners:** We may share Your information with Our business partners to offer You certain products, services or promotions.
* **With other users:** when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside.
* **With Your consent**: We may disclose Your personal information for any other purpose with Your consent.

#### Retention of Your Personal Data

The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.

The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.

#### Transfer of Your Personal Data

Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.

Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.

The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.

#### Disclosure of Your Personal Data

**Business Transactions**

If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.

**Law enforcement**

Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).

**Other legal requirements**

The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:

* Comply with a legal obligation
* Protect and defend the rights or property of the Company
* Prevent or investigate possible wrongdoing in connection with the Service
* Protect the personal safety of Users of the Service or the public
* Protect against legal liability

#### Security of Your Personal Data

The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.

### Children's Privacy

Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.

If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.

### Links to Other Websites

Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.

We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.

### Changes to this Privacy Policy

We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.

We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy.

You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.

### Contact Us

If you have any questions about this Privacy Policy, You can contact us by emailing <hello@devicecloud.dev>


# Refund Policy

If you're not happy with our service, please email <hello@devicecloud.dev>.

When the platform has failed to perform as per documentation, you may be offered additional free credits.

If the platform fails to perform completely, you can request a refund. This will be granted at our discretion so long as the refund is requested within 14 days of purchase.


# Service Level Agreements

### Enterprise Platform Uptime SLA <a href="#enterprise-platform-uptime-sla" id="enterprise-platform-uptime-sla"></a>

The following Service Level Agreement forms part of the Subscription Agreement between devicecloud.dev and Customer (the "Agreement"), will apply to the Services for Enterprise Customers specified in an Order Form during the applicable Subscription Term:

#### 1. Uptime Commitment <a href="#id-1-uptime-commitment" id="id-1-uptime-commitment"></a>

devicecloud.dev will provide Actual Availability for at least 99.9% of the total time in each calendar month during the Subscription Term, as measured by devicecloud.dev (the **"Uptime Commitment"**).

#### 2. Service Credits <a href="#id-2-service-credits" id="id-2-service-credits"></a>

If the Uptime Commitment is not met during any particular calendar month during the Subscription Term, then Customer will be eligible for a service credit provided that Customer reports to devicecloud.dev such failure to meet the Uptime Commitment and requests such Service Credit per this process.

The amount of any Service Credit due hereunder shall be calculated as follows: X \* Y, where X = the total fees due from Customer to devicecloud.dev for the affected Services for the relevant calendar month (regardless of when billed or payable), and Y = the Credit Percentage corresponding with the Actual Availability provided (as a percentage of total time) for the relevant calendar month, as outlined in the table below.

| Actual Availability                                | Credit Percentage |
| -------------------------------------------------- | ----------------- |
| Less than 99.9% but greater than or equal to 99.0% | 10%               |
| Less than 99.0% but greater than or equal to 98.0% | 20%               |
| Less than 98.0% but greater than or equal to 96.0% | 30%               |
| Less than 96.0%                                    | 40%               |

#### 3. Credit Requests and Payment <a href="#id-3-credit-requests-and-payment" id="id-3-credit-requests-and-payment"></a>

To request a Service Credit, the Customer must send an email to <support@devicecloud.dev> within 30 days of the end of the month in which the Uptime Commitment was not met. Customer must include their registered email address and the previously reported dates and times that there was no Service Availability. If devicecloud.dev confirms that Customer is eligible for a Service Credit, devicecloud.dev will issue a credit to Customer's account within 30 days. Service Credits are not refunds, cannot be exchanged into a cash amount, and may only be used against future billing charges. Except as outlined below, the Service Credits shall be Customer's sole and exclusive remedy and devicecloud.dev's sole and exclusive liability, for any failure by devicecloud.dev to meet the Uptime Commitment.

#### 4. Definitions <a href="#id-4-definitions" id="id-4-definitions"></a>

All capitalized words used but not defined in this Service Level Agreement have the meaning outlined in the Agreement.

**4.1 Scheduled Availability**

"Scheduled Availability" means the time, in minutes, that the applicable Services are generally accessible and available to Customer's Permitted Users.

**4.2 Unscheduled Downtime**

"Unscheduled Downtime" means the time, in minutes, that the applicable Services are not generally accessible and available to Customer's Permitted Users, excluding inaccessibility or unavailability due to Customer's or Permitted Users' acts or omissions, force majeure events, scheduled maintenance disclosed with at least 24 hours notice by email, hacking or virus attacks, reasonable emergency maintenance or other product specific exclusions listed under SLA Exclusions.

**4.3 Actual Availability**

"Actual Availability" means Scheduled Availability less Unscheduled Downtime.

**4.4 Production**

"Production" is defined as a system serving live customer-facing or business systems with existing deployed and functional features.

"Development", "Staging", "uat", "pre-production" or new feature implementation even if in a production environment, are not considered Production.

#### SLA Exclusions <a href="#sla-exclusions" id="sla-exclusions"></a>

* Caused by factors outside of our reasonable control, including but not limited to any force majeure event or Internet access, ISP provider issues, and/or related problems beyond the demarcation point of devicecloud.dev. For the avoidance of doubt, this list is not exhaustive, and we will endeavour to inform you if the issue is beyond a factor that we can reasonably control.
* That results from any voluntary actions or inactions from you including erroneous Maestro test scripts or configuration.
* That results from you not following the basic operational guidelines described in our documentation
* That results from your equipment, software, or other technology.

### Support <a href="#support" id="support"></a>

devicecloud.dev provides Support Service Level Agreements for our Enterprise customers. We support Standard customers with "Best Effort" support.

#### 1. Urgent <a href="#id-1-urgent" id="id-1-urgent"></a>

**Critical Issue**

Defect resulting in full or partial system outage or a condition that makes devicecloud.dev unusable or unavailable for critical business systems in the Customer's workflows.

#### 2. High <a href="#id-2-high" id="id-2-high"></a>

**Significant Business Disruption**

Issue resulting in a situation meaning major functionality is impacted and significant performance degradation is experienced. The issue impacts a significant proportion of test runs and/or major devicecloud.dev functionality.

#### 3. Normal <a href="#id-3-normal" id="id-3-normal"></a>

**Minor Feature or Functional Issue / General Question**

The issue results in a component of devicecloud.dev not performing as expected or documented. An inquiry by a Customer representative regarding a general technical issue or general question.

#### 4. Low <a href="#id-4-low" id="id-4-low"></a>

**Minor Issue / Feature Request**

An information request about devicecloud.dev or feature request.

### Target initial response times <a href="#target-initial-response-times" id="target-initial-response-times"></a>

| Severity Level | Standard Account (pay-as-you-go) | Enterprise Account (annual contract) |
| -------------- | -------------------------------- | ------------------------------------ |
| 1. Urgent      | 4 hours                          | 1 hour                               |
| 2. High        | 1 business day                   | 4 hours                              |
| 3. Normal      | 2 business days                  | 1 business day                       |
| 4. Low         | 3 business days                  | 2 business days                      |

Support hours are from 8 am to 8 pm UK time, Monday-Friday, except where otherwise stated.


# Open Source Notices

devicecloud.dev uses open-source software.

**MAESTRO**

Read the full license [here](https://github.com/mobile-dev-inc/maestro/blob/main/LICENSE)

Copyright Mobile.dev

Licensed under the Apache License, Version 2.0 (the "License");

you may not use this file except in compliance with the License.

You may obtain a copy of the License at <http://www.apache.org/licenses/LICENSE-2.0>

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.


