Llim.run

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

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:

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

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

Extra Maestro flags go after --:

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

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

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

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 }}

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

smartphone

Run a simulator

Create, configure, and drive iOS simulators directly.

flask-conical

Appium

The WebDriver alternative, including Android automation.

package

Asset Storage

Upload the app under test once and install it on every create.

flask-conical

XCTest

Native unit and UI tests with lim xcode test.