CLI Reference
Two command-line tools. mobai
drives a device from your shell, one action at a time.
mobai-ci
runs whole test suites in a pipeline and writes JUnit.
Overview
Both tools speak to the same automation stack, but they are built for different jobs.
Pick mobai when you are
exploring, scripting, or debugging on a machine with the desktop app running. Pick
mobai-ci when a pipeline
needs to boot a device, run a suite, and produce reports without a desktop app anywhere.
A thin client over the desktop app's local HTTP API. Every action subcommand builds a one-step DSL script and posts it to the device.
Needs the MobAI desktop app running (locally or reachable via --base-url).
A self-contained runner for .mob
scripts and Maestro YAML flows. Talks to devices directly, writes JUnit, screenshots, and failure captures.
No desktop app needed. Local runs are free; remote and cloud runs are Pro.
Install
mobai
# global install
npm install -g @mobai-app/cli
# one-off, no install
npx @mobai-app/cli devices list
# as a dev dependency of a test project
npm install -D @mobai-app/cli
npm installs the meta package plus the one native binary matching your platform (macOS arm64/x64, Linux x64/arm64, Windows x64). Node 18+ is only needed for the install shim; the CLI itself is a Go binary.
mobai-ci
# GitHub Actions
- uses: MobAI-App/mobai-ci@v1
# anywhere else
curl -fsSL https://raw.githubusercontent.com/MobAI-App/mobai-ci/main/install.sh | sh
Pin a version with MOBAI_CI_VERSION=x.y.z.
Global Flags
Accepted by every mobai
subcommand. Each has an environment variable, so a shell session or CI step can set the target once.
| Flag | Env | Default | Description |
|---|---|---|---|
| --base-url | MOBAI_URL | http://127.0.0.1:8686 | MobAI HTTP API base URL |
| --device, -d | MOBAI_DEVICE | auto | Target device ID. Auto-picked when exactly one device is connected |
| --timeout | MOBAI_TIMEOUT | 30s | Per-request timeout |
| --json | - | - | Structured JSON output for scripts |
| --quiet, -q | - | - | Suppress non-error output on success |
# point a whole session at one device on another machine
export MOBAI_URL=http://192.168.1.40:8686
export MOBAI_DEVICE=00008101-000A4D2E1234001E
mobai app launch com.example.app
mobai tap "id:login_btn"
Selectors
Every command that targets an element takes the same selector string: coordinates, percentages, or a predicate built from comma-separated keys.
"300,500" # pixel coordinates
"50%,30%" # percent of screen size, portable across devices
"id:login_btn" # accessibility id
"text-contains:Login,type:button" # combined predicate
| Key | Matches |
|---|---|
| id | Accessibility identifier, exact |
| text | Visible label, exact |
| text-contains | Substring of the label |
| text-regex | Regular expression against the label |
| text-starts-with | Label prefix |
| type | Element class (button, cell, textfield, ...) |
| index | Nth match when several elements match |
| bounds | Restrict matching to a screen region |
| near / near-dir / near-dist | Relative to another element, with direction and max distance |
| enabled / visible / selected | Element state |
| css | CSS selector, for web contexts |
Predicates are the same ones the DSL uses. See the predicate reference for full semantics.
Exit Codes
mobai distinguishes
failure kinds so a script can branch on them instead of parsing output.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Generic error |
| 2 | Element not found / no match |
| 3 | Timeout |
| 4 | Bridge crashed / app not running |
| 5 | User cancelled / safety blocked |
| 6 | Assertion failed |
| 10 | API unreachable (is the desktop app running?) |
| 11 | Device selection failed (multiple devices, none specified) |
mobai-ci uses three:
| Code | Meaning |
|---|---|
| 0 | All tests passed |
| 1 | A test failed |
| 2 | Setup or usage error |
Device CLI
mobai
Devices & Bridge
| Command | Description |
|---|---|
| mobai devices list | List connected Android and iOS devices |
| mobai devices info | Show details for the target device |
| mobai bridge start | Start the automation bridge on a device |
| mobai bridge stop | Stop the bridge |
$ mobai devices list
iOS iPhone 15 Pro 00008101-000A4D2E1234001E ready
Android Pixel 7 39061FDJH00CBP ready
$ mobai devices info --json | jq .platform
"ios"
The bridge auto-starts on the first device request, so
bridge start is only needed when you want to pay that cost up front.
Interaction
| Command | Key flags | Description |
|---|---|---|
| tap <selector> | - | Tap an element or coordinate |
| double-tap <selector> | - | Double-tap |
| two-finger-tap <selector> | - | Two-finger tap |
| long-press <selector> | --duration-ms | Long-press. Predicate form only, no coordinates |
| type <text> | --into, --clear, --dismiss-keyboard | Type into the focused element, or focus --into first |
| press <key> | --context | Keyboard or hardware key: enter, tab, home, back, volume_up, ... |
| swipe [dir] | --from, --to, --distance, --duration-ms | Swipe by direction or between points |
| scroll [dir] | --to, --amount, --max-scrolls | Scroll, optionally until a target is visible |
| drag | --from, --to, --press-duration-ms, --hold-duration-ms | Drag between elements or points |
| pinch <selector> | --scale, --velocity | Pinch to zoom in (scale > 1) or out (scale < 1) |
| toggle <selector> | --state | Toggle a switch, skipping the tap when already correct |
| navigate <target> | - | home, back, or recent_apps |
| delay | --ms | Wait a fixed duration |
mobai tap "id:login_btn"
mobai type "[email protected]" --clear --into "id:email_field"
mobai press enter
# scroll a long list until the row shows up, then act on it
mobai scroll down --to "text-contains:Advanced" --max-scrolls 15
mobai toggle "type:switch,near:Advanced" --state on
# drag a card onto another one
mobai drag --from "id:card_a" --to "id:card_b" --duration-ms 800
Observation
| Command | Key flags | Description |
|---|---|---|
| observe | --include, --context, --only-visible, --include-keyboard | Read UI state: ui_tree, screenshot, activity, installed_apps, dom, ocr |
| screenshot | --path (required), --name, --full | Save an image to disk. Low-quality JPEG by default, --full for PNG |
| wait [selector] | --stable, --timeout-ms, --poll-ms, --context | Wait for an element, or for the UI to stop changing |
# the default: just the UI tree
mobai observe
# visible elements plus a screenshot, as JSON
mobai observe --include ui_tree,screenshot --only-visible true --json
# wait for the screen to settle after a transition
mobai wait --stable --timeout-ms 5000
mobai screenshot --path ./artifacts --name checkout --full
Assertions
Assertions exit with code 6 when they fail, so they drop into shell scripts and CI steps without extra glue.
| Command | Description |
|---|---|
| assert exists | An element matching the selector is present |
| assert not-exists | No element matches |
| assert count | The number of matching elements |
| assert property | A property value on the matched element |
| assert screen-changed | The screen changed since the last observe |
mobai tap "id:submit"
mobai wait "text-contains:Welcome" --timeout-ms 8000
mobai assert exists "id:home_tab" || { echo "login broke"; exit 1; }
Apps
| Command | Description |
|---|---|
| app install <path> | Install an APK or IPA from a local file |
| app uninstall <bundle> | Uninstall by bundle ID |
| app launch <bundle> | Launch by bundle ID |
| app kill <bundle> | Force-kill a running app |
| app list | List installed apps |
mobai app install ./build/MyApp.ipa
mobai app launch com.example.myapp
mobai app list --json | jq -r '.[].bundle_id'
Web Automation
Drives page content through Safari WebInspector on iOS and Chrome remote debugging on Android. Browser chrome (address bar, tabs) stays native automation; the DOM is web automation.
| Command | Description |
|---|---|
| web pages | List open pages and WebViews |
| web select <id> | Choose the tab or WebView for later web actions |
| web navigate <url> | Navigate the selected page |
| web click <css> | Click a DOM element by CSS selector |
| web type <css> <text> | Type into a DOM element |
| web wait <css> | Wait for a DOM element to appear |
| web js <script> | Execute JavaScript in the selected page |
mobai web pages
mobai web select 1
mobai web navigate https://example.com/login
mobai web type "#email" "[email protected]"
mobai web click "button[type=submit]"
mobai web js "document.title"
Metrics & Recording
| Command | Description |
|---|---|
| metrics start | Begin collecting cpu, memory, fps, network, battery, process samples |
| metrics stop | Stop collection and print the summary |
| record start | Capture screenshots in the background |
| record stop | Save frames and run transition anomaly detection |
mobai metrics start
mobai app launch com.example.myapp
mobai scroll down --amount full
mobai metrics stop --json > perf.json
Location & Siri
| Command | Description |
|---|---|
| location set | Set a simulated GPS location |
| location reset | Return to the real GPS location |
| siri "prompt" | Send a voice command to Siri (iOS). Auto-approves consent dialogs, captures the response, dismisses the UI |
mobai location set --lat 37.7749 --lon -122.4194
mobai siri "Search YouTube for cat videos"
If Siri asks a follow-up question, reformulate the prompt with more detail and run it again.
Debugging
Live lldb control of an iOS app: attach, break, step, and evaluate expressions while automation drives the UI.
| Command | Description |
|---|---|
| debug attach | Start a session: launch via --bundle-id, or attach to --pid |
| debug breakpoint | Add or remove a breakpoint: "File.swift:42", "Module.Type.method", "-[Class method:]" |
| debug step | Advance the target: --direction in|over|out blocks until the next stop, continue is fire-and-forget |
| debug state | Session state. --stack, --vars, --threads opt into expensive fetches |
| debug eval | Evaluate an lldb expression at the current pause |
| debug detach | End the session. The target keeps running unless --kill |
mobai debug attach --bundle-id com.example.myapp
mobai debug breakpoint "CheckoutViewModel.submit"
mobai tap "id:pay_button"
mobai debug state --vars
mobai debug eval "po self.cart.total"
mobai debug detach
Running Tests
Runs a whole test file on the target device and reports per-step results. The file extension picks the
parser: .mob for MobAI
scripts, .yaml or
.yml for Maestro flows.
| Flag | Description |
|---|---|
| -p, --param K=V | Value for ${name} substitution. Repeatable |
| -P, --project | Project root, so run "..." sub-scripts and relative refs resolve. Defaults to the current directory |
mobai test ./flows/checkout.mob -p [email protected]
mobai test ./flows/login.yaml -P ./flows
A file outside the current directory falls back to using its own directory as the project root. For suites, reports, and CI, use mobai-ci instead.
Desktop & Skills
| Command | Description |
|---|---|
| desktop install | Download the latest desktop installer and hand it to the OS |
| desktop launch | Launch the installed desktop app |
| skills install <dir> | Install the bundled using-mobai-cli skill into a directory such as ~/.claude/skills |
| version | Print the CLI version |
# bootstrap a fresh machine from the CLI alone
npm install -g @mobai-app/cli
mobai desktop install
mobai desktop launch
mobai skills install ~/.claude/skills
CI Runner
mobai-ci
Overview
Runs .mob scripts and
Maestro YAML flows against a local device, a machine you own, or a rented cloud device. It writes JUnit,
every explicit screenshot step's image, and an automatic screenshot plus UI tree for each failure.
| Command | Description |
|---|---|
| test <path|glob...> | Run flows, write JUnit and artifacts, set the exit code |
| install <app> | Install a build on the target device |
| devices | List reachable devices |
| validate <path|glob...> | Parse flows without touching a device |
| sim boot | sim prepare | Boot or pre-build a test-ready iOS simulator (macOS) |
| emu boot | emu prepare | Boot or pre-build a test-ready Android emulator |
| version | Print the runner version |
Target flags (test, install, devices)
| Flag | Description |
|---|---|
| --device | Target by name, UDID, or serial. Defaults to the only running device |
| --wait-device dur | Wait for the device to boot or appear instead of failing immediately |
| --mobai-addr | Remote MobAI host base URL (env MOBAI_ADDR) |
| --token | API token for the remote host (env MOBAI_TOKEN) |
| --discover | Find MobAI hosts automatically: mdns or tailscale |
| --port | API port to probe during discovery (default 8686) |
| -v, --verbose | Show internal logs |
Running Flows
| Flag | Description |
|---|---|
| --app | App to install before running |
| --output | Report and artifact directory (default mobai-ci-out) |
| --allure | Also write allure-results/ for Allure Report or TestOps |
| --report-bundle | Also write run.json plus artifacts, the canonical machine-readable run |
| --shard i/N | Run one shard. Tests are assigned round-robin across shards |
| --timeout dur | Overall run budget (0 = none) |
| --test-timeout dur | Per-test budget (0 = none) |
| --param K=V | Override ${name} in flows. Repeatable |
| --no-settle | Tap immediately instead of waiting for animating targets |
| --startup-timeout dur | On-device runner bring-up budget (default 5m; cold CI simulators need 1.5 to 2.5m) |
| --wait | Remote: wait for a free device instead of failing when all are leased |
| --api-key | MobAI account API key (env MOBAI_API_KEY). Remote and cloud runs are Pro |
mobai-ci test ./flows --app ./build/MyApp.app --output reports
mobai-ci test ./flows --shard 2/4 --timeout 20m --test-timeout 3m
mobai-ci validate ./flows # parse only, no device needed
mobai-ci test attaches to a running device, it does not boot one.
Boot first with sim boot / emu boot.
With exactly one device running it is auto-detected.
iOS Simulators
sim boot creates a
test-ready simulator and prints its UDID as the last line, so a pipeline can capture it. The boot continues
in the background: pass the UDID to test with
--wait-device and overlap it with your build.
| Flag | Description |
|---|---|
| --device-type | Simulator model (default iPhone 16 Pro) |
| --runtime | iOS runtime id (default: newest installed) |
| --cache | Directory for a reusable simulator image. The first run creates it, later runs boot much faster |
| --slim path | Turn off the iOS background services listed in a simslim profile file. See Slim Simulators |
| --startup-timeout | Budget for one-time preparation (default 5m) |
# boot in the background, build meanwhile, then run
UDID=$(mobai-ci sim boot --cache ~/.cache/mobai-sim | tail -1)
xcodebuild -scheme MyApp -destination 'generic/platform=iOS Simulator' build
mobai-ci test ./flows --device "$UDID" --wait-device 4m --app "$APP_PATH"
Run sim boot before any other Xcode or simulator step in the job.
sim prepare takes the same flags and builds the reusable image without
booting a test simulator, which suits a scheduled job that warms the cache.
Slim Simulators
A stock iOS simulator runs a few hundred background daemons that a test never touches: Siri, Photos
analysis, iCloud sync, lock screen posters, News, Spotlight indexing. Turning them off is what lets one
machine host several simulators at once.
simslim
is our open-source tool for that, and mobai-ci applies it for you.
# 1. build a profile once, interactively, and commit it
brew install mobai-app/tap/simslim
simslim profile ./ci/slim.json
# 2. pass it on every run
mobai-ci sim boot --cache ~/.cache/mobai-sim --slim ./ci/slim.json
The profile file is required rather than implied by a bare flag. Disabling services changes what your tests can exercise, and the committed file is the record of exactly which capabilities you gave up. A profile that leaves everything enabled is rejected, and an unknown category or daemon name fails in seconds, before any simulator work starts.
The disable entries live in the simulator's own launchd database. They stick across reboots and travel
inside the --cache image, so the reconfigure happens once, while the
image is built. Every later run restores a simulator that is already slim and pays nothing for it.
Pair --slim with --cache and, if you
want the one-time cost off your PR path entirely, build the image from a scheduled
sim prepare job.
One consequence: a copy of a simulator does not inherit them, so slim runs boot the prepared
simulator directly instead of taking a fresh copy per run. Runs on one machine share it, and
--app reinstalls your build either way.
| If your tests need | Keep this category enabled |
|---|---|
| Push notifications, StoreKit | store |
| Spotlight or Settings search | search |
| iCloud sync, Apple Account, Keychain | icloud |
| Universal links, Safari sync | web |
| Contacts, Calendar, Reminders, Mail | pim |
| Widgets, Live Activities, wallpaper | widgets |
| Photo picker, media analysis | photos |
| HealthKit, HomeKit, Fitness | health |
simslim profiles lists every category with its daemon count and what
breaks when it is off. simslim doctor <udid> --requires push,storekit
checks a booted simulator against the features you depend on and exits non-zero when slimming broke one.
Android Emulators
| Flag | Description |
|---|---|
| --api | Android API level (default 35) |
| --system-image | Full sdkmanager system-image id (default: derived from --api) |
| --profile | avdmanager device profile (default pixel_7) |
| --cache | Directory for a reusable emulator image. Required |
| --startup-timeout | Budget for one-time preparation (default 10m) |
SERIAL=$(mobai-ci emu boot --api 35 --cache ~/.cache/mobai-emu | tail -1)
mobai-ci test ./flows --device "$SERIAL" --wait-device 4m
--cache is required. The first run prepares a reusable image, which can
take several minutes; later runs resume from it in seconds. emu prepare
builds the image without leaving an emulator running.
Remote Devices
Point the runner at a machine you own: an office Mac, a lab box, or a self-hosted runner with real devices
attached. The runner talks to that host's MobAI HTTP API, and how the address is reachable (Tailscale,
cloudflared, SSH tunnel, or the same LAN) is your choice. Pro, needs
MOBAI_API_KEY.
mobai-ci test ./flows \
--mobai-addr https://mac-mini.tailnet.ts.net:8686 \
--token "$MOBAI_TOKEN" \
--device "iPhone 15 Pro" --wait
Remote runs claim the device for the duration: the host rejects other runners that try to drive it and
frees the claim on release, or after a TTL if the runner dies. --wait
queues for a free device instead of failing when every match is leased.
Cloud Device Farms
Run the flows on a device you rent from BrowserStack, Sauce Labs, or AWS Device Farm, from any runner, with
no simulator or emulator needed. Pro, needs MOBAI_API_KEY, and uses your
own farm account.
| Flag | Description |
|---|---|
| --cloud | browserstack, saucelabs, or awsdevicefarm |
| --os | Device OS version, for example 17 |
| --platform | ios or android. Inferred from --app when omitted |
| --virtual | Target an emulator or simulator catalog entry |
| --cloud-opt K=V | Forward an option to the provider job. Repeatable |
| Provider | Environment variables |
|---|---|
| browserstack | BROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY |
| saucelabs | SAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION (optional) |
| awsdevicefarm | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
mobai-ci devices --cloud saucelabs # list valid device names
mobai-ci test ./flows \
--cloud saucelabs \
--device "iPhone 14" --os 16 \
--app ./build/MyApp.ipa \
--output reports
--app takes a local .ipa or .apk, which is uploaded for you, or an
already-uploaded provider reference. --timeout defaults to 30m on cloud
runs, and the rented session always ends when the run finishes, fails, times out, or is cancelled.
CI Example
A complete GitHub Actions job on a hosted macOS runner. Nothing leaves the runner, and the simulator boot overlaps the app build.
name: Mobile smoke (iOS simulator)
on: [pull_request]
jobs:
smoke:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: MobAI-App/mobai-ci@v1
# reuse the prepared simulator image across runs
- uses: actions/cache@v4
with:
path: ~/.cache/mobai-sim
key: mobai-sim-${{ runner.os }}-v1
# boot first, before any other Xcode step
- name: Boot simulator
run: echo "UDID=$(mobai-ci sim boot --cache ~/.cache/mobai-sim --slim ./ci/slim.json | tail -1)" >> "$GITHUB_ENV"
- name: Build app
run: |
xcodebuild -scheme MyApp -destination 'generic/platform=iOS Simulator' \
-configuration Debug -derivedDataPath build build
echo "APP_PATH=$(find build/Build/Products -name '*.app' -type d | head -1)" >> "$GITHUB_ENV"
- name: Run flows
run: mobai-ci test ./flows --device "$UDID" --wait-device 4m --app "$APP_PATH" --output reports
- uses: actions/upload-artifact@v4
if: always()
with:
name: mobai-reports
path: reports
Ready-to-copy workflows for simulators, remote devices over Tailscale, and cloud farms live in the repo.