> For the complete documentation index, see [llms.txt](https://docs.devicecloud.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.devicecloud.dev/ci-cd-integration/eas-workflows.md).

# 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.md).

### 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>`        | `16`, `17`, `18`, `26`. Default `17`.                                                                         |
| `--device-locale <code>`   | E.g. `de_DE`. See [Device Locale](/configuration/device-locale.md).                                           |
| `--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.md). |

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

### Test Configuration

| Flag                         | Description                                                                                                      |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `--maestro-version <semver>` | Maestro CLI version. See [Maestro Versions](/configuration/maestro-versions.md).                                 |
| `--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.md). |

### Execution Options

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

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

***

## 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.md) 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).
