Run Maestro flows
lim ios maestro wraps the stock Maestro CLI. Your flows and Maestro itself stay unmodified: lim 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. For Android UI automation, use Appium over the ADB tunnel.
Before you start
- The
limCLI (0.22.0 or newer) andLIM_API_KEYset up as in the Quickstart. - 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 that already runs against a local simulator.
- 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 the 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. Pre-install the runner at create time to skip that step.
- It runs your local
maestro testwith the driver's HTTP traffic routed to the runner over an authenticated HTTPS tunnel, and answers thesimctlcalls through the Limrun API. The tunnel authenticates with the instance token, the same mechanism the Appium integration uses for WebDriverAgent.
Maestro's exit codes, console output, and report flags work 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 and the app under test pre-installed, 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. Either way, the first run on an instance takes a few extra seconds 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 workspace (your git repository or worktree). In scripts and CI, pass --id <instance-id> before test:
lim ios maestro --id <instance-id> test flow.yamlExtra Maestro flags go after --:
lim ios maestro -- test flows/ --include-tags smoke --test-output-dir artifactsDo not pass --platform, --device, --udid, --no-reinstall-driver, or --driver-host-port. lim sets those itself and rejects duplicates.
The runnable example, including the same wiring from the TypeScript SDK with prepareMaestroRun, is at typescript-sdk/examples/maestro-ios.
Run an Expo Go flow
Pre-install Expo Go alongside the runner and open your project URL from the flow. Environment variables must start 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: 90000To serve the project from your machine, see Connect to local services.
Differences from local Maestro
lim answers a fixed subset of simctl for Maestro through a shim, and rejects commands that take or return host filesystem paths. In practice:
- The
startRecordingandstopRecordingflow commands are not supported. Record around the run instead:lim ios record startbefore the flow,lim ios record stop -o video.mp4after. See Run a simulator. addMediaandclearKeychainare not supported.takeScreenshotworks and saves the PNG locally into the working directory (or--test-output-dir), like stock Maestro.- HTTP calls from
runScriptandevalScriptmust 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. An app that is mid-foreground can drop deep links, 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 }}The job labels the instance with the run ID and deletes it at the end, including on failure. Inactivity timeouts clean up after jobs that crash.
Troubleshooting
Next steps
Was this guide helpful?