Maestro
Point your existing Maestro flows at a Limrun iOS simulator with lim ios maestro. Your flows and the Maestro CLI are unmodified; the lim CLI points Maestro's iOS driver at the remote simulator and the flow runs as if the simulator were local.
Maestro on Limrun covers iOS simulator instances today. For Android UI automation, use Appium over the ADB tunnel.
Before you start
- A Limrun API key in
LIM_API_KEY(get one from console.limrun.com). If you've never used the CLI before, walk through the Quickstart first. limCLI 0.22.0 or newer:npm install --global lim.- The Maestro CLI on your PATH, which needs a Java runtime. See Installing Maestro. Maestro 2.5.x and 2.6+ both work;
limadapts to the installed version. - A Maestro flow you can already run locally. We won't cover authoring flows, only pointing them at Limrun.
- The app under test as a simulator build in an archive (
MyApp.app.zipor.tar.gz), for example from Build with Xcode.
How the integration works
Maestro drives iOS through a small XCTest runner app that serves HTTP inside the simulator, plus xcrun simctl calls for app lifecycle. lim ios maestro provides both against a remote instance:
- It installs Limrun's patched build of the Maestro runner on the instance the first time you use it, or you preinstall it at create time to skip that step.
- It runs your local
maestro testwith the driver's HTTP routed to the runner over an authenticated HTTPS tunnel (the instance's bearer token, the same mechanism the Appium integration uses for WebDriverAgent), and withsimctlcalls answered by the Limrun API.
Maestro's exit codes, console output, and report flags work exactly as they do locally, so existing CI wiring carries over. Only the test subcommand is wrapped; maestro studio, record, and hierarchy are not available against remote instances.
Run a flow
Create an instance with the runner preinstalled and the app under test, then run your flow:
lim ios create --install ./MyApp.app.zip \
--install-asset appstore/maestro-ios-runner-2.5.1.tar.gz
lim ios maestro test flow.yamlWithout the --install-asset line, lim ios maestro installs the runner on first use; the first run on an instance takes a few extra seconds either way while the runner launches. If you run the same app often, upload it as an asset once instead of re-uploading with --install on every create.
lim ios maestro targets the last iOS instance the CLI created for this repo. Pass --id <id> (before test) in scripts and CI:
lim ios maestro --id <id> test flow.yamlExtra Maestro flags go after --:
lim ios maestro -- test flows/ --include-tags smoke --test-output-dir artifactsDon't pass --platform, --device, --udid, --no-reinstall-driver, or --driver-host-port; lim sets those itself and rejects duplicates.
The full runnable example, including driving the same wiring from the TypeScript SDK with prepareMaestroRun, is at typescript-sdk/examples/maestro-ios.
Run an Expo Go flow
Preinstall Expo Go alongside the runner and open your project URL from the flow. Environment variables must be prefixed with MAESTRO_ to be visible inside flows:
lim ios create \
--install-asset appstore/Expo-Go-54.0.6.tar.gz \
--install-asset appstore/maestro-ios-runner-2.5.1.tar.gz
MAESTRO_EXPO_URL='exp://<your-tunnel-host>' lim ios maestro test flow.yamlThe flow opens the project and waits out the first bundle load:
appId: host.exp.Exponent
---
- openLink: ${MAESTRO_EXPO_URL}
- runFlow:
when:
visible: Open
commands:
- tapOn: Open
- extendedWaitUntil:
visible: My App Home
timeout: 90000Differences from local Maestro
lim answers a fixed subset of simctl for Maestro through a shim; commands that take or return host filesystem paths are rejected. In practice:
startRecording/stopRecordingflow commands are not supported. Record around the run instead:lim ios record startbefore the flow,lim ios record stop -o video.mp4after. See Record a video.addMediaandclearKeychainare not supported.takeScreenshotworks and saves the PNG locally into the working directory (or--test-output-dir), like stock Maestro.- HTTP calls from
runScript/evalScriptmust usehttps://URLs. Maestro's plain-HTTP traffic is routed to the remote simulator's driver, sohttpscalls go out directly while plainhttpcalls are refused. - When a flow runs against an app that is already open (Expo Go especially), start with
- stopAppbeforeopenLinkorlaunchApp. Deep links can be dropped by an app that is mid-foreground, and a stale screen fails early assertions.
Run from a Linux CI runner
No macOS host is needed; the Mac lives on Limrun's side. A standard ubuntu-latest GitHub Actions job is enough:
name: Maestro iOS
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Install lim and Maestro
run: |
npm install --global lim
curl -fsSL https://get.maestro.mobile.dev | bash
echo "$HOME/.maestro/bin" >> $GITHUB_PATH
- name: Run flows
env:
LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
run: |
ID=$(lim ios create --install ./MyApp.app.zip \
--install-asset appstore/maestro-ios-runner-2.5.1.tar.gz \
--label session=${{ github.run_id }} \
--no-open --quiet --json | jq -r .metadata.id)
echo "INSTANCE_ID=$ID" >> "$GITHUB_ENV"
lim ios maestro --id "$ID" -- test flows/ --test-output-dir artifacts
- name: Delete instance
if: always()
run: lim ios delete "$INSTANCE_ID"
env:
LIM_API_KEY: ${{ secrets.LIM_API_KEY }}Label instances with the run id as above and tear them down at job end, including on failure. Inactivity timeouts handle the rest if a job crashes. See Reuse and clean up instances.
Troubleshooting
Next steps
Was this guide helpful?