---
modificationDate: July 08, 2026
title: 'Automate development builds with EAS Workflows'
description: Learn how to automate Android and iOS development builds with EAS Workflows and use fingerprints to skip rebuilds when only JavaScript changes.
---

<AgentInstructions>

## Submitting Feedback

If this page contains errors, outdated information, or gaps that blocked you from completing a task, report it so the docs can be improved:

curl -X POST https://api.expo.dev/v2/feedback/docs-send -H 'Content-Type: application/json' -d '{"url":"/tutorial/cicd/development-builds/","feedback":"🤖 Agent feedback for docs: <specific, actionable description> (<model>, <harness>)"}'

Only submit when you have something specific and actionable to report. Replace <model> with the model you are running as and <harness> with the tool you are running in (for example, Claude Code, Cursor, Codex CLI).

## Navigation

When answering a related or follow-up question, fetch the relevant page below as Markdown (.md) instead of guessing; use llms.txt for the full map.

You are here: Learn > CI/CD tutorial
Pages in this section:
- [Introduction](https://docs.expo.dev/tutorial/cicd/introduction.md)
- [First EAS Workflows job](https://docs.expo.dev/tutorial/cicd/first-workflow.md)
- [Development builds](https://docs.expo.dev/tutorial/cicd/development-builds.md) (this page)
- [Preview builds](https://docs.expo.dev/tutorial/cicd/preview-builds.md)
- [E2E tests](https://docs.expo.dev/tutorial/cicd/e2e-tests.md)
- [Production deployments](https://docs.expo.dev/tutorial/cicd/production.md)
- [Tag-based releases](https://docs.expo.dev/tutorial/cicd/tag-based-releases.md)
- [Web deployments](https://docs.expo.dev/tutorial/cicd/web-deployments.md)
- [Next steps](https://docs.expo.dev/tutorial/cicd/next-steps.md)
Full documentation tree: [llms.txt](https://docs.expo.dev/llms.txt)

</AgentInstructions>

This documentation is available as Markdown for AI agents and LLMs. See the [full Markdown index](/llms.txt) or append .md to any documentation URL.

# Automate development builds with EAS Workflows

Learn how to automate Android and iOS development builds with EAS Workflows and use fingerprints to skip rebuilds when only JavaScript changes.

Manually rebuilding a development client on every push to a GitHub repository can be slow. Team members or new contributors who pull native changes (a new module, a new permission, an SDK upgrade) wait for a fresh build before they can run the project.

Automating development builds on the `main` branch keeps a current build ready to install. We can run `eas build:dev` to install the latest compatible build. If the project fingerprint matches an existing build, EAS downloads that build instead of creating a new one.

## Learning outcomes

-   Automate [development builds](/develop/development-builds/introduction.md) for Android and iOS with the `build` job type
-   Use `fingerprint` and `get-build` jobs to skip rebuilding when native code hasn't changed
-   Add a custom unit testing job that gates the workflow on passing tests

#### Prerequisites

##### EAS Build configured for development

EAS CLI prompts to generate credentials the first time we trigger a build for a profile. Trigger the development build manually for both Android and iOS so the credentials exist before the workflow runs:

```sh
eas build --profile development --platform all
```

## The `build` job type for development builds

The `build` job type is one of the [pre-packaged jobs](/tutorial/cicd/first-workflow.md#types-of-jobs-in-eas-workflows) from EAS Workflows. Instead of defining each step of the build process, we use the `build` job type to run an EAS Build for Android and iOS.

For this chapter, we use the `development` build profile from our **eas.json**. We use this profile during Expo app development.

### Add a build.yml

Inside **.eas/workflows/**, add a new file called **build.yml**. It uses the `build` job type with `profile` and `platform` parameters to create development builds for Android and iOS.

Add the following code to the **build.yml** file:

```yaml
name: Development builds

jobs:
  build_android:
    name: Build Android
    type: build
    params:
      platform: android
      profile: development
  build_ios:
    name: Build iOS
    type: build
    params:
      platform: ios
      profile: development
```

We add two jobs: `build_android` and `build_ios`. Both are of type `build` with different parameters for platform and build profile.

The `params` key for each job includes:

-   [`platform`](/eas/workflows/pre-packaged-jobs.md#build) sets the target OS for the build (`android` or `ios`)
-   [`profile`](/build/eas-json.md#build-profiles) specifies the build profile from our **eas.json**

### Run the workflow manually

Let's run the workflow manually using the command:

```sh
eas workflow:run .eas/workflows/build.yml
```

### Verify on the EAS dashboard

On the EAS dashboard, notice that it says "Triggered manually" under the **Workflow graph** tab. Any time the **Trigger** shows **Manual**, that means the workflow was started with the `eas workflow:run` command.

The logs for each build appear inside the workflow interface. Click a build's ID to open its EAS Build page.

> Jobs without dependencies run in parallel by default in EAS Workflows. In the above workflow, since both `build_android` and `build_ios` jobs don't have any dependencies, they start at the same time.

Wait for both builds to complete before moving to the next step. Once they complete, we can use the **Install** or **Open with [Orbit](/build/orbit.md)** buttons, or download from **Artifacts** to install on our device/emulator/simulator.

## Skip unnecessary builds with fingerprints

When our project has only TypeScript/JavaScript changes, the existing development build is still compatible, so we do not need a new one.

[**Expo Fingerprint**](/versions/latest/sdk/fingerprint.md) automates this decision. It hashes the project's native characteristics (dependencies, native project files, configuration). If the hash matches an existing build, native code has not changed, and EAS skips the rebuild. If the hash is different, native code changed, and EAS creates a new build.

```
Fingerprint hashes native characteristics. If hash matches an existing build, rebuild is skipped. If hash differs, a new build is created.
```

Let's update **build.yml** step by step to detect native changes and build only when needed.

### Add a fingerprint job

The [`fingerprint`](/eas/workflows/pre-packaged-jobs.md#fingerprint) pre-packaged job hashes the project's native characteristics and outputs a fingerprint hash for each platform. The `environment` field tells EAS which [environment variable configuration](/eas/environment-variables.md) to use. Note that `environment` and `profile` can share the same name but refer to different things. `profile` points to a build profile in **eas.json** (how to build). `environment` points to environment variables configured on EAS (which values to inject at build time).

Update **build.yml** to add the `fingerprint` job before the build jobs:

```yaml
name: Development builds

jobs:
  fingerprint:
    name: Fingerprint
    type: fingerprint
    environment: development
  build_android:
    # ...
  build_ios:
    # ...
```

The `fingerprint` job produces two outputs: `android_fingerprint_hash` and `ios_fingerprint_hash`. We use these hashes in the next step to check if a compatible build already exists.

### Add get-build jobs

The [`get-build`](/eas/workflows/pre-packaged-jobs.md#get-build) pre-packaged job checks if a build already exists for a given fingerprint hash and profile. If a matching build exists, it outputs a `build_id`. If not, `build_id` is empty.

In our workflow file, let's add `get_android_build` and `get_ios_build` jobs between `fingerprint` and the build jobs. Each one uses `needs: [fingerprint]` to wait for the fingerprint job to complete and access its output:

```yaml
name: Development builds

jobs:
  fingerprint:
    # ...
  get_android_build:
    name: Check for existing Android build
    needs: [fingerprint]
    type: get-build
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
      profile: development
  get_ios_build:
    name: Check for existing iOS build
    needs: [fingerprint]
    type: get-build
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
      profile: development
  build_android:
    # ...
  build_ios:
    # ...
```

In the above workflow, `get-build` jobs receive the fingerprint hash from the previous job using the `${{ needs.fingerprint.outputs.* }}` expression syntax.

### Add conditional builds

The [`if`](/eas/workflows/syntax.md#jobsjob_idif) field controls whether a job runs. The expression `${{ !needs.get_android_build.outputs.build_id }}` means: _run a job only if `get-build` did not find a matching build_. The `!` is a negation operator. If a compatible build already exists for this fingerprint, the build job is skipped.

Let's update each build job to depend on its `get-build` job and add the `if` condition:

```yaml
name: Development builds

jobs:
  fingerprint:
    # ...
  get_android_build:
    # ...
  get_ios_build:
    # ...
  build_android:
    name: Build Android
    needs: [get_android_build]
    if: ${{ !needs.get_android_build.outputs.build_id }}
    type: build
    params:
      platform: android
      profile: development
  build_ios:
    name: Build iOS
    needs: [get_ios_build]
    if: ${{ !needs.get_ios_build.outputs.build_id }}
    type: build
    params:
      platform: ios
      profile: development
```

### Add a trigger and run the workflow

Let's add the `on.push` trigger to the workflow so it runs automatically on every push to `main`:

```yaml
name: Development builds

on:
  push:
    branches: ['main']

jobs:
  fingerprint:
    name: Fingerprint
    type: fingerprint
    environment: development
  get_android_build:
    name: Check for existing Android build
    needs: [fingerprint]
    type: get-build
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}
      profile: development
  get_ios_build:
    name: Check for existing iOS build
    needs: [fingerprint]
    type: get-build
    params:
      fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
      profile: development
  build_android:
    name: Build Android
    needs: [get_android_build]
    if: ${{ !needs.get_android_build.outputs.build_id }}
    type: build
    params:
      platform: android
      profile: development
  build_ios:
    name: Build iOS
    needs: [get_ios_build]
    if: ${{ !needs.get_ios_build.outputs.build_id }}
    type: build
    params:
      platform: ios
      profile: development
```

Let's assume we made some TypeScript/JavaScript changes to our Expo project, like changing a button's color or updating the text on a screen. After doing so, we can push a commit to the `main` branch to trigger the workflow from our GitHub repository:

```sh
git add .
git commit -m "Update button label and tweak styling"
git push origin main
```

This triggers a new workflow that completes in seconds. On the EAS dashboard, both Android and iOS builds are skipped because a matching build already exists from the previous section.

We pull the latest from the `main` branch and run `npx expo start`. The existing development build picks up the JS changes.

## Custom jobs for unit testing

> This section assumes Jest is set up in the project. For setup instructions, see [Unit testing with Jest](/develop/unit-testing.md).

When working in a team, we want every change pushed to the `main` branch to pass tests before triggering a new build.

Without automation, the team has to remember to run tests locally before pushing to `main`. If they forget and the code has a failing test, the next workflow can create a build from broken code.

With **EAS Workflows**, we can automate this process by mixing a custom job with our existing pre-packaged jobs. This custom job runs before fingerprint and build jobs. If a test fails, EAS stops the workflow and creates no build.

```
Unit tests run first as a custom job. If tests pass, the build pipeline continues. If tests fail, the workflow stops and no build is created.
```

### Add a custom job for unit tests

In our **build.yml** workflow file, let's add a custom job that runs unit tests before the builds start:

```yaml
name: Development builds

on:
  push:
    branches: ['main']

jobs:
  run_tests:
    name: Run unit tests
    steps:
      - uses: eas/checkout # Check out the repo
      - uses: eas/install_node_modules
      - run: npx jest --ci
  fingerprint:
    name: Fingerprint
    needs: [run_tests]
    type: fingerprint
    environment: development
  get_android_build:
    # ...
  get_ios_build:
    # ...
  build_android:
    # ...
  build_ios:
    # ...
```

The custom `run_tests` job does the following:

-   `uses: eas/checkout` checks out the project source files from the GitHub repository. Custom jobs run on a new virtual machine, so the code is not available by default. This step is required before accessing any project files.
-   `uses: eas/install_node_modules` installs dependencies using the package manager detected in the project (bun, npm, pnpm, or Yarn). This is needed before running any commands that depend on packages from **node_modules**, like Jest.
-   `run: npx jest --ci` executes the test suite. The `run` key runs any shell command, similar to the `echo` command. The `--ci` flag tells Jest to run in CI mode, which skips watching for file changes and exits after running all tests.

Now, the `fingerprint` job has `needs: [run_tests]`, so it only runs after the tests pass. If tests fail, EAS skips the rest of the workflow.

### Trigger the workflow

Commit and push the updated workflow along with the test file to `main`:

```sh
git add .
git commit -m "Add unit tests to development builds workflow"
git push origin main
```

After triggering the workflow, open the EAS dashboard and notice the flow of the jobs inside our workflow.

## Summary

Chapter 2: Development builds

We automated development builds using pre-packaged jobs, added fingerprints to skip unnecessary rebuilds, and integrated unit tests as a custom job before the build pipeline.

In the next chapter, learn how to create preview builds with Slack notifications and PR preview updates.

[Next: Chapter 3: Preview builds](/tutorial/cicd/preview-builds.md)
