Using cloud simulators

How to run an iOS Simulator in GitHub Actions with Expo Simulators

Run an iOS Simulator from GitHub Actions on Linux: build with EAS, start an Expo Simulators session, drive the app, save screenshots, and stop the session.

6 min read

Quick answer

A GitHub Actions workflow can start an Expo Simulators session on every pull request. The job signs in with an EXPO_TOKEN secret from a robot user, builds the app for the iOS Simulator with EAS Build, and starts a session. It drives the app through eas simulator:exec with a controller: agent-device in this example, or Argent or Appium. It uploads the screenshots as workflow artifacts, and stops the session in a step that always runs.

The iOS Simulator runs on EAS, so the CI machine does not need macOS or a local simulator.

What do you need first?

  • An app config that contains the EAS project ID (extra.eas.projectId). Commands that run with an access token fail without it.
  • An Expo account with access to Expo Simulators.
  • A build profile in eas.json for the iOS Simulator:

iOS Simulator builds are not signed, so EAS Build does not ask for Apple credentials.

Add EXPO_TOKEN to GitHub

CI jobs cannot use eas login. EAS CLI reads an access token from the EXPO_TOKEN environment variable.

  1. Create a robot user on the Expo account that owns the project. Give it the Developer role or higher. Developer is the lowest role that can create and stop sessions.
  2. Create an access token for the robot user.
  3. Add the token to the repository as a secret named EXPO_TOKEN, in Settings > Secrets and variables > Actions, or with the GitHub CLI:

Add the workflow

Save this file as .github/workflows/expo-simulators.yml. Replace com.example.app with the ios.bundleIdentifier from npx expo config --json.

What each step does

  • Concurrency: a new push cancels the older run. If that run started a session, its stop step still runs.
  • Shell: shell: bash adds pipefail, so a failed command in a pipe fails the step.
  • Check access: simulator:availability exits with code 0 even without access. jq -e fails the step when available is false. The runner includes jq.
  • Build: eas build waits for the build and prints it as JSON, and jq reads the build ID. The build contains the pull request's JavaScript.
  • Start: --non-interactive returns when the session is ready. The session keeps running, and --max-duration-minutes stops it if the stop step never runs.
  • Drive: the start command writes .env.eas-simulator to the project directory. Steps share the workspace, so simulator:exec reads the file in later steps. agent-device needs an open app before a screenshot, and it downloads each file to the runner. Replace the scroll with your flow, such as snapshot -i, press, and fill.
  • Stop: if: always() runs the step after a failure or a cancel. The steps.start.outcome check skips it when the start step did not run.

Use Argent or Appium instead

Each session gets the controller for its session type. Change --type in the start step, then change the commands in the drive step:

  • Argent: start with --type argent, install the Argent CLI (npm install --global @swmansion/argent), and run its tools with eas simulator:exec argent run <tool>.
  • Appium: start with --type appium, and run your Appium client or test suite through eas simulator:exec. It loads APPIUM_URL and APPIUM_CAPS, so your client reads the server URL and capabilities from them.

Keep tokens out of the logs

GitHub redacts secrets such as EXPO_TOKEN from logs. The session tokens that the start command creates are not GitHub secrets, so GitHub does not redact them.

  • Do not print or upload .env.eas-simulator. It holds the controller tokens. Upload only the artifacts folder.
  • Do not print --json output from simulator:start or simulator:get. It includes connection details, and simulator:get also lists artifact download URLs.
  • Add .env.eas-simulator to .gitignore.

Other ways to get a build

  • To reuse a build when native code has not changed, pass --build-fingerprint with the hash from eas fingerprint:generate. That build can contain older JavaScript, because the fingerprint covers only native code.
  • In EAS Workflows, the get-build job can find an iOS Simulator build, and a build job can create one. Pass the build to a later step that starts the session.
  • For Android, build an APK and use --platform android. Android support is still in development, so some features that work on iOS may not work on Android yet.

Let an agent set it up

If an agent writes or changes the workflow for you, have it load these Expo Skills first:

  • expo-overview: the entry point that routes the agent to the right Expo skill.
  • eas-simulator: the current eas simulator commands, session types, and cleanup steps.
  • eas-workflows: EAS workflow YAML, if you move the job to EAS Workflows.

Install them:

  • Claude Code: claude plugin install expo@claude-plugins-official
  • Codex: codex plugin add expo@openai-curated
  • Cursor and other agents: npx skills add expo/skills

The full list is at docs.expo.dev/skills.

Try it

Expo Simulators is in a limited beta release. The docs on running sessions in CI also cover the REST API and parallel sessions.

Try Expo Simulators

Verified on 20 October 2026.

Keep reading

Frequently Asked Questions