Llim.run

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

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:

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

Without 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.yaml

Extra Maestro flags go after --:

lim ios maestro -- test flows/ --include-tags smoke --test-output-dir artifacts

Don'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.yaml

The flow opens the project and waits out the first bundle load:

flow.yaml
appId: host.exp.Exponent
---
- openLink: ${MAESTRO_EXPO_URL}
- runFlow:
    when:
      visible: Open
    commands:
      - tapOn: Open
- extendedWaitUntil:
    visible: My App Home
    timeout: 90000

Differences 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:

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:

.github/workflows/maestro-ios.yml
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

Run an iOS Simulator

Provision, configure, and drive iOS instances directly. The control surface Maestro sits on top of.

Appium

The WebDriver alternative, including Android automation over the ADB tunnel.

Asset Storage

Upload the app under test once and reference it from every instance create.