The deviceyard CLI

A single command that uploads your app, runs your flows on whichever real device in the farm's pool is free, streams the verdicts back and exits with a code your pipeline can gate on. It mirrors maestro cloud and DeviceCloud's dcd cloud, so a team already using either one changes the binary name and little else.

deviceyard cloud app-testing.apk e2e/scenarios

For the raw HTTP contract underneath it, see the API reference.

Install

With npm — needs Node 20 or newer:

npm install -g @deviceyard/cli

Without Node — a standalone binary, nothing else required:

curl -fsSL https://your-farm.example.com/cli/install.sh | sh

The installer resolves the binary for your platform from the public npm registry and drops it in ~/.deviceyard/bin. It prints the PATH line to add if that directory is not already on yours. Supported platforms: macOS and Linux, on arm64 and x64.

In CI, without installing anything:

npx --yes @deviceyard/cli@latest cloud app-testing.apk e2e/scenarios

Keeping a binary install current:

deviceyard upgrade          # binary installs
npm install -g @deviceyard/cli@latest   # npm installs

Authenticate

Locally, store a personal access token. Create one in the dashboard under Account settings, then:

deviceyard login --url https://your-farm.example.com
# paste the token, then Ctrl-D
#   or:  pbpaste | deviceyard login --url https://your-farm.example.com

The token is read from stdin only, never from an argument — an argument lands in your shell history and in ps output, where any other user on the box can read it. It is stored 0600 in ~/.deviceyard/credentials.

A personal token belongs to a person, not an org, so every command that touches a job needs --org <id>. deviceyard whoami lists the ids you have:

deviceyard whoami
# https://your-farm.example.com  (personal credential)
#   1  example-team
#   2  momentum

In CI, use an org token instead. It fixes its own org, so no --org is needed:

export DEVICEYARD_URL=https://your-farm.example.com
export DEVICEYARD_API_KEY=...        # an org token from the dashboard
deviceyard cloud app-testing.apk e2e/scenarios

Credentials resolve in this order: --api-key, then DEVICEYARD_API_KEY, then ~/.deviceyard/credentials. MOBILE_FARM_URL and MOBILE_FARM_TOKEN are still accepted, so a pipeline already set up for the MCP client needs no edit.

Your flows directory

This farm resolves a flow at e2e/scenarios/<name>.yaml, so your flows must live in an e2e/scenarios tree:

your-repo/
  e2e/
    scenarios/
      login.yaml        ->  the flow "login"
      map/filter.yaml   ->  the flow "map/filter"
    shared/             ->  shipped too, so relative references keep working

Point the CLI at that tree, at a sub-directory of it, or at a single .yaml file:

deviceyard cloud app.apk e2e/scenarios
deviceyard cloud app.apk e2e/scenarios/map
deviceyard cloud app.apk e2e/scenarios/login.yaml

The CLI walks up to find the e2e/ root and ships the whole directory, so files your flows reference from e2e/shared/ keep resolving exactly as they do locally. If there is no e2e/scenarios above the path you gave, it says so rather than uploading an archive the farm would reject.

Flows run in the order the CLI submits them, which is alphabetical by id. Only .yaml is recognised — a .yml file is not a flow here.

A first run

deviceyard cloud app-testing.apk e2e/scenarios --org 1
....  login
PASS  login
....  map/filter
FAIL  map/filter

job 1756800000000-a1b2c3: 1/2 passed
  FAIL  map/filter

Check what would be submitted without uploading anything:

deviceyard cloud app-testing.apk e2e/scenarios --dry-run

Commands

Command What it does
cloud <app-file> <flows> Upload and run, waiting for every verdict
status <job-id> Show one job and its steps
list Recent jobs, newest first
artifacts <job-id> Download a job's screenshots, recordings and logs
login / logout / whoami Manage the stored credential
upgrade Update a binary install

Flags

Authentication and target

Flag Meaning
--api-key <key> Credential to use. Defaults to DEVICEYARD_API_KEY, then the stored one
--url <url> The farm. Defaults to DEVICEYARD_URL
--org <id> Which org to act as. Required for a personal access token

Choosing flows

Flag Meaning
--flows <path> The flows path, instead of the positional argument
--exclude-flows <paths> Comma-separated flow ids or sub-directories to skip
--include-tags <tags> Only run flows carrying one of these tags
--device-tags <tags> Require all comma-separated device tags (Pro); separate from flow tags. See device routing
--exclude-tags <tags> Skip flows carrying one of these tags

Tag filtering happens on your machine, before submission: the CLI already reads each flow's header to build the job, so it reads tags: while it is there. --dry-run shows you exactly which flows and tags it found.

The run

Flag Meaning
--app-file <path> The app binary, instead of the positional argument
--env KEY=VALUE An environment variable for every flow. Repeat for more
--name <name> A label for the run
--async Submit and exit 0 immediately, printing only the job id
--dry-run Resolve and print everything, upload nothing

--env is validated locally against the farm's own rules — identifier-shaped keys, no newlines, and a four-character minimum for anything named like a secret (PASSWORD, TOKEN, SECRET, API_KEY) — so a typo fails before a 500 MB upload rather than after it. Values of secret-named variables are masked in stored logs and artifacts; see Environment variables.

Output

Flag Meaning
--quiet, -q Suppress per-step lines; still prints the summary
--json Print the result as JSON. Still exits 2 on a flow failure
--json-file Write the result as JSON. Exits 0 even on a flow failure
--json-file-name <name> Where to write it. Defaults to <job-id>_deviceyard.json
--report junit Write a JUnit XML report
--junit-path <path> Where to write it. Defaults to ./report.xml
--download-artifacts ALL|FAILED Download artifacts after the run
--artifacts-path <path> Directory to download into. Defaults to ./artifacts
--debug Verbose diagnostics

Use --json when you want the exit code to gate the build, and --json-file when your pipeline will read the result itself and decide.

Exit codes

Code Meaning
0 Every flow passed — or --async submitted, or --json-file wrote its file
1 A CLI or infrastructure problem: bad arguments, unreachable farm, bad credential, empty device pool
2 A flow failed

The 1 / 2 split is load-bearing. A job that fails before any flow runs — no device enrolled, the install failed, the archive would not extract — exits 1, so a pipeline that retries on 2 does not retry an infrastructure outage as though a test had flaked.

Migrating from maestro cloud or dcd cloud

Most flags are the same. These behave identically:

maestro cloud / dcd cloud deviceyard cloud
--api-key --api-key, or DEVICEYARD_API_KEY
--app-file, --flows Same
--exclude-flows Same
--include-tags, --exclude-tags Same, applied client-side before submission
--env KEY=VALUE Same
--name Same
--async Same
--quiet / -q Same
--json, --json-file, --json-file-name Same
--dry-run Same
--debug Same
--report junit, --junit-path Same

One flag means something different:

Flag Difference
--artifacts-path Names a directory, not dcd's artifacts.zip. Files land at <path>/<flow name>/<artifact>

Everything else dcd cloud accepts is recognised and rejected with a reason, never as an unknown option — so a migrating pipeline gets a diagnosis rather than a parser error:

dcd cloud flag deviceyard Why
--ios-device — This farm's pool is Android-only; iOS is not available
--ios-version — This farm's pool is Android-only; iOS is not available
--android-device — The scheduler picks whichever enrolled device is free - a job cannot pick a model
--android-api-level — A job's API level is whatever the device it lands on runs, and that pick is the scheduler's, not the caller's
--retry — Retries are not supported; re-run the job, or handle the retry in your pipeline
--maestro-version — The farm pins one Maestro version - GET /api/version reports which
--app-binary-id — Uploaded binaries are not retained for reuse; every run uploads its app file
--app-url — The app must be uploaded from disk; the farm does not fetch app binaries over the network
--google-play — Device capabilities are fixed by the attached hardware
--orientation — Orientation is not controllable per run
--device-locale — Device locale is not controllable per run
--runner-type — This farm has one runner
--disable-animations — Animation scales are a property of the device a job lands on, not of a run
--show-crosshairs — Crosshair overlays are not supported
--android-no-snapshot — The farm runs physical hardware, not snapshot-booted emulators
--maestro-chrome-onboarding — Chrome onboarding control is not supported
--ignore-sha-check — Every run uploads its app file, so there is no SHA cache to bypass
--config — Workspace config files are not read; pass the flags explicitly
--metadata — Arbitrary run metadata is not stored; use --name
--branch — Git metadata is not stored against a run
--commit-sha — Git metadata is not stored against a run
--repo-name — Git metadata is not stored against a run
--pr-number — Git metadata is not stored against a run
--pr-url — Git metadata is not stored against a run

For feature requests, describe your use case in a repository issue. Several of these are farm capabilities rather than CLI gaps.

Troubleshooting

the platform binary @deviceyard/cli-... is not installed — npm skipped the optional dependency for your platform. Reinstall with npm install -g @deviceyard/cli --include=optional.

this credential is a personal access token ... name the org with --org — you are using a PAT. Run deviceyard whoami for your org ids, or use an org token in CI.

could not find an e2e/scenarios directory above ... — see Your flows directory.

no device is enrolled - add a device and enable it from the Devices page first — the pool is empty; no device is enrolled at all. Add one and enable it on the Devices page; see Operating the farm. This is never returned just because every enrolled device happens to be busy — the job queues instead and runs once one frees up.

View this page on GitHub ↗ · nmehlei/DeviceYard