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.

mobai

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

mobai-ci

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.

FlagEnvDefaultDescription
--base-urlMOBAI_URLhttp://127.0.0.1:8686MobAI HTTP API base URL
--device, -dMOBAI_DEVICEautoTarget device ID. Auto-picked when exactly one device is connected
--timeoutMOBAI_TIMEOUT30sPer-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
KeyMatches
idAccessibility identifier, exact
textVisible label, exact
text-containsSubstring of the label
text-regexRegular expression against the label
text-starts-withLabel prefix
typeElement class (button, cell, textfield, ...)
indexNth match when several elements match
boundsRestrict matching to a screen region
near / near-dir / near-distRelative to another element, with direction and max distance
enabled / visible / selectedElement state
cssCSS 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.

CodeMeaning
0Success
1Generic error
2Element not found / no match
3Timeout
4Bridge crashed / app not running
5User cancelled / safety blocked
6Assertion failed
10API unreachable (is the desktop app running?)
11Device selection failed (multiple devices, none specified)

mobai-ci uses three:

CodeMeaning
0All tests passed
1A test failed
2Setup or usage error

Device CLI

mobai

Devices & Bridge

CommandDescription
mobai devices listList connected Android and iOS devices
mobai devices infoShow details for the target device
mobai bridge startStart the automation bridge on a device
mobai bridge stopStop 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

CommandKey flagsDescription
tap <selector>-Tap an element or coordinate
double-tap <selector>-Double-tap
two-finger-tap <selector>-Two-finger tap
long-press <selector>--duration-msLong-press. Predicate form only, no coordinates
type <text>--into, --clear, --dismiss-keyboardType into the focused element, or focus --into first
press <key>--contextKeyboard or hardware key: enter, tab, home, back, volume_up, ...
swipe [dir]--from, --to, --distance, --duration-msSwipe by direction or between points
scroll [dir]--to, --amount, --max-scrollsScroll, optionally until a target is visible
drag--from, --to, --press-duration-ms, --hold-duration-msDrag between elements or points
pinch <selector>--scale, --velocityPinch to zoom in (scale > 1) or out (scale < 1)
toggle <selector>--stateToggle a switch, skipping the tap when already correct
navigate <target>-home, back, or recent_apps
delay--msWait 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

CommandKey flagsDescription
observe--include, --context, --only-visible, --include-keyboardRead UI state: ui_tree, screenshot, activity, installed_apps, dom, ocr
screenshot--path (required), --name, --fullSave an image to disk. Low-quality JPEG by default, --full for PNG
wait [selector]--stable, --timeout-ms, --poll-ms, --contextWait 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.

CommandDescription
assert existsAn element matching the selector is present
assert not-existsNo element matches
assert countThe number of matching elements
assert propertyA property value on the matched element
assert screen-changedThe 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

CommandDescription
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 listList 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.

CommandDescription
web pagesList 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

CommandDescription
metrics startBegin collecting cpu, memory, fps, network, battery, process samples
metrics stopStop collection and print the summary
record startCapture screenshots in the background
record stopSave 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

CommandDescription
location setSet a simulated GPS location
location resetReturn 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.

CommandDescription
debug attachStart a session: launch via --bundle-id, or attach to --pid
debug breakpointAdd or remove a breakpoint: "File.swift:42", "Module.Type.method", "-[Class method:]"
debug stepAdvance the target: --direction in|over|out blocks until the next stop, continue is fire-and-forget
debug stateSession state. --stack, --vars, --threads opt into expensive fetches
debug evalEvaluate an lldb expression at the current pause
debug detachEnd 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.

FlagDescription
-p, --param K=VValue for ${name} substitution. Repeatable
-P, --projectProject 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

CommandDescription
desktop installDownload the latest desktop installer and hand it to the OS
desktop launchLaunch the installed desktop app
skills install <dir>Install the bundled using-mobai-cli skill into a directory such as ~/.claude/skills
versionPrint 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.

CommandDescription
test <path|glob...>Run flows, write JUnit and artifacts, set the exit code
install <app>Install a build on the target device
devicesList reachable devices
validate <path|glob...>Parse flows without touching a device
sim boot | sim prepareBoot or pre-build a test-ready iOS simulator (macOS)
emu boot | emu prepareBoot or pre-build a test-ready Android emulator
versionPrint the runner version

Target flags (test, install, devices)

FlagDescription
--deviceTarget by name, UDID, or serial. Defaults to the only running device
--wait-device durWait for the device to boot or appear instead of failing immediately
--mobai-addrRemote MobAI host base URL (env MOBAI_ADDR)
--tokenAPI token for the remote host (env MOBAI_TOKEN)
--discoverFind MobAI hosts automatically: mdns or tailscale
--portAPI port to probe during discovery (default 8686)
-v, --verboseShow internal logs

Running Flows

FlagDescription
--appApp to install before running
--outputReport and artifact directory (default mobai-ci-out)
--allureAlso write allure-results/ for Allure Report or TestOps
--report-bundleAlso write run.json plus artifacts, the canonical machine-readable run
--shard i/NRun one shard. Tests are assigned round-robin across shards
--timeout durOverall run budget (0 = none)
--test-timeout durPer-test budget (0 = none)
--param K=VOverride ${name} in flows. Repeatable
--no-settleTap immediately instead of waiting for animating targets
--startup-timeout durOn-device runner bring-up budget (default 5m; cold CI simulators need 1.5 to 2.5m)
--waitRemote: wait for a free device instead of failing when all are leased
--api-keyMobAI 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.

FlagDescription
--device-typeSimulator model (default iPhone 16 Pro)
--runtimeiOS runtime id (default: newest installed)
--cacheDirectory for a reusable simulator image. The first run creates it, later runs boot much faster
--slim pathTurn off the iOS background services listed in a simslim profile file. See Slim Simulators
--startup-timeoutBudget 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.

Cost: paid once, then cached

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 needKeep this category enabled
Push notifications, StoreKitstore
Spotlight or Settings searchsearch
iCloud sync, Apple Account, Keychainicloud
Universal links, Safari syncweb
Contacts, Calendar, Reminders, Mailpim
Widgets, Live Activities, wallpaperwidgets
Photo picker, media analysisphotos
HealthKit, HomeKit, Fitnesshealth

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

FlagDescription
--apiAndroid API level (default 35)
--system-imageFull sdkmanager system-image id (default: derived from --api)
--profileavdmanager device profile (default pixel_7)
--cacheDirectory for a reusable emulator image. Required
--startup-timeoutBudget 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.

FlagDescription
--cloudbrowserstack, saucelabs, or awsdevicefarm
--osDevice OS version, for example 17
--platformios or android. Inferred from --app when omitted
--virtualTarget an emulator or simulator catalog entry
--cloud-opt K=VForward an option to the provider job. Repeatable
ProviderEnvironment variables
browserstackBROWSERSTACK_USERNAME, BROWSERSTACK_ACCESS_KEY
saucelabsSAUCE_USERNAME, SAUCE_ACCESS_KEY, SAUCE_REGION (optional)
awsdevicefarmAWS_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.