Skip to content

YAML Reference

The RunnerHub pipeline configuration is defined in a single YAML file located at .runnerhub/runnerhub.yml in your repository root, or stored as branch-specific Cloud YAML in the dashboard.

By default, RunnerHub looks for the configuration file at:

repo-root/
├─ .runnerhub/
│ └─ runnerhub.yml ← primary (checked first)
└─ runnerhub.yml ← fallback (if primary not found)

If multiple apps share the same repository (e.g., white-label builds), each app can specify a custom pipeline YAML path in App Settings → Pipeline Configuration Path.

When a custom path is set:

  • RunnerHub fetches only that file (no fallback)
  • The path is relative to the repository root
  • Must end in .yml or .yaml
  • Path traversal (..) is not allowed

Example setup for white-label apps:

repo-root/
├─ .runnerhub/
│ ├─ client-a.yml ← App "Client A" uses this
│ ├─ client-b.yml ← App "Client B" uses this
│ └─ runnerhub.yml ← Default (apps without custom path)

The smallest valid pipeline configuration requires four fields:

name: My First Pipeline
platform: ios
environment:
xcode: "16.4"
triggers:
- push
steps:
- name: Build
run: echo "Building..."

Here is the full structure of a RunnerHub pipeline configuration:

name: string
platform: ios | macos | android | flutter | react-native
environment:
xcode: "16.4" # (Required for iOS/macOS/React Native iOS targets)
android_sdk: 34 # (Required for Android/React Native Android targets)
flutter: "3.24.0" # (Required for Flutter, see note below)
node: lts # (Required for React Native)
ruby: "3.2" # (Optional, for Bundler/CocoaPods)
variables:
KEY: value
triggers:
- push
- pull_request
# Optional: use object format for branch filtering
- event: push
branches:
- main
- release/*
steps:
- name: string
run: string
working_directory: string
env:
KEY: value
artifacts:
- glob/path/**
timeout: number
FieldRequiredTypeDescription
nameYesstringHuman-readable pipeline name, displayed in dashboard and logs
platformYesstringTarget platform: ios, macos, android, flutter, or react-native
environmentYesobjectPlatform-specific tool versions (see Platform Requirements below)
triggersYesarrayList of events that trigger the pipeline; use string format (push, pull_request) or object format with optional branch filtering (see Triggers)
stepsYesarrayOrdered list of commands to execute
artifactsNoarrayGlob patterns for build outputs to collect and store
timeoutNonumberMaximum job duration in minutes (default: 60, max: 30–120 depending on plan)

Triggers support two formats:

String format — trigger on all branches:

triggers:
- push
- pull_request

Object format — trigger with optional branch filtering:

triggers:
- event: push
branches:
- main
- develop
- release/*
- event: pull_request
branches:
- main

The branches field is optional and only works with push and pull_request events. Manual runs from the dashboard always work regardless of your triggers: array. Scheduled builds are configured in the dashboard (Settings → Scheduled Triggers) and do not require a YAML trigger entry.

For detailed information about branch patterns (wildcards, limits, and PR behavior), see the Triggers guide.

Each step in the steps array supports these fields:

FieldRequiredTypeDescription
nameYesstringHuman-readable step name, displayed in logs and dashboard
runYesstringShell command(s) to execute
working_directoryNostringDirectory where the command runs (useful for monorepos)
envNoobjectStep-level environment variables (override pipeline-level variables)

Here’s a complete iOS pipeline demonstrating all major features:

name: iOS Release Build
platform: ios
environment:
xcode: "16.4"
variables:
LC_ALL: en_US.UTF-8
FASTLANE_USER: $FASTLANE_USER
FASTLANE_PASSWORD: $FASTLANE_PASSWORD
triggers:
- push
- pull_request
steps:
- name: Install dependencies
run: bundle install
- name: Install pods
run: pod install
- name: Run tests
run: fastlane test
- name: Build for release
run: fastlane build
env:
BUILD_CONFIG: Release
- name: Upload to TestFlight
run: fastlane beta
working_directory: ios
env:
TESTFLIGHT_ENABLED: "true"
artifacts:
- build/**/*.ipa
- build/**/*.dSYM
timeout: 45

Each platform requires specific tool version specifications in the environment block:

environment:
xcode: "16.4" # Required: specify Xcode version
variables:
KEY: value

Use the xcode field to specify your Xcode version. Available versions depend on the agent image.

environment:
android_sdk: 34 # Required: specify Android SDK level
variables:
KEY: value
environment:
node: lts # Required: Node.js version ("lts", "latest", or semver like "20.11")
xcode: "16.4" # Optional: for iOS targets
android_sdk: 34 # Optional: for Android targets
ruby: "3.2" # Optional: advisory only (see note below)
variables:
KEY: value

Required: You must specify node and at least one of xcode (iOS) or android_sdk (Android).

Accepted values for node:

  • Semantic version: "20", "20.11", "20.11.1" — Install specific version
  • Release channels: "lts" — Install current LTS version; "latest" — Install latest version

The specified Node version is activated before any steps run.

About ruby: The ruby: field is currently advisory only — the platform does not enforce a Ruby version based on this setting. To pin a Ruby version, commit a .ruby-version file to your repository root. rbenv (pre-installed on the VM) will automatically detect and activate the specified Ruby version before your pipeline runs.


For complex pipelines with multiple stages, you can define multiple jobs that run in sequence or parallel, with dependency management. This section covers the multi-job (DAG) format where you use jobs: instead of steps: at the top level.

Single-job format — uses steps: at the top level:

name: My Pipeline
platform: ios
triggers:
- push
steps:
- name: Build
run: xcodebuild build
- name: Test
run: xcodebuild test

Multi-job format — uses jobs: at the top level; each job has its own steps::

name: My Pipeline
platform: ios
triggers:
- push
jobs:
build:
name: Build App
steps:
- name: Build
run: xcodebuild build
test:
name: Run Tests
steps:
- name: Test
run: xcodebuild test

When using jobs:, the top-level steps: field is ignored.

Each key under jobs: is a unique identifier for that job:

jobs:
build: # Job key (identifier)
name: Build App # Display name
steps: [...]
test-unit: # Job key
name: Unit Tests # Display name
steps: [...]
  • Job key (e.g., build, test-unit) — used in needs: dependencies and logs
  • name: — human-readable display name shown in the dashboard

The needs: field specifies which jobs must complete successfully before this job runs:

jobs:
build:
name: Build App
steps:
- name: Build
run: xcodebuild build
test:
name: Run Tests
needs: [build] # Test waits for build to succeed
steps:
- name: Test
run: xcodebuild test
deploy:
name: Deploy
needs: [test] # Deploy waits for test to succeed
steps:
- name: Deploy
run: fastlane beta

Dependency behavior:

  • If a dependency fails, the dependent job is automatically cancelled
  • A job waits for all items in needs: to complete successfully before starting
  • Jobs without dependencies start immediately

Jobs without mutual needs: dependencies run concurrently:

jobs:
build:
name: Build
steps:
- name: Build
run: xcodebuild build
test-unit:
name: Unit Tests
needs: [build] # Waits for build
steps:
- name: Run unit tests
run: xcodebuild test
test-ui:
name: UI Tests
needs: [build] # Also waits for build
steps:
- name: Run UI tests
run: xcodebuild test -scheme UITests

In this example:

  1. build runs first
  2. test-unit and test-ui run in parallel after build completes
  3. This is faster than running them sequentially
  • Pipeline SUCCESS: All jobs succeed
  • Pipeline FAILED: Any job fails
  • Cascading cancellation: When a job fails, all dependent jobs are automatically cancelled

Example:

jobs:
setup:
steps: [...]
build:
needs: [setup]
steps: [...]
test:
needs: [build]
steps: [...]
deploy:
needs: [test]
steps: [...]

If build fails:

  • test and deploy are automatically cancelled (marked CANCELLED)
  • Pipeline status becomes FAILED

Complete Example: Build → Parallel Tests → Archive

Section titled “Complete Example: Build → Parallel Tests → Archive”

Here’s a realistic iOS pipeline with parallel test stages. Note: Each job installs its own dependencies (cache makes this fast).

name: CI Pipeline
platform: ios
environment:
xcode: "16.4"
triggers:
- push
- pull_request:
branches: [main, develop]
jobs:
build:
name: Build App
steps:
- name: Install dependencies
run: bundle install
- name: Install pods
run: pod install
- name: Build
run: xcodebuild build -scheme MyApp -destination 'generic/platform=iOS'
test-unit:
name: Unit Tests
needs: [build]
steps:
- name: Install dependencies
run: bundle install && pod install
- name: Run unit tests
run: xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 16'
test-ui:
name: UI Tests
needs: [build]
steps:
- name: Install dependencies
run: bundle install && pod install
- name: Run UI tests
run: xcodebuild test -scheme MyAppUITests -destination 'platform=iOS Simulator,name=iPhone 16'
archive:
name: Archive
needs: [test-unit, test-ui]
steps:
- name: Install dependencies
run: bundle install && pod install
- name: Archive
run: xcodebuild archive -scheme MyApp -archivePath build/MyApp.xcarchive
deploy:
name: Deploy to TestFlight
needs: [archive]
if: success()
steps:
- name: Upload to TestFlight
if: branch == 'main'
run: fastlane beta

Execution flow:

  1. build runs
  2. test-unit and test-ui run in parallel after build completes (each installs its own deps)
  3. archive waits for both tests to complete and installs its own deps
  4. deploy runs only on the main branch and only after archive succeeds

Each job in jobs: supports:

jobs:
job-id:
name: Display name # Required: human-readable name
needs: [other-job-id, ...] # Optional: job dependencies
if: condition # Optional: conditional execution
steps: # Required: list of steps for this job
- name: Step name
run: command
working_directory: path
env:
KEY: value
retry: 2
retry_delay: 5

All step properties (retry, env, working_directory, etc.) work the same way as single-job pipelines.


The if: field on a job can only use status functions — no branch or event conditions:

jobs:
build:
name: Build
steps:
- run: xcodebuild build
notify:
name: Notify
needs: [build]
if: success() # Only status functions allowed here
steps:
- run: curl https://example.com/notify

Supported job-level if: values:

  • success() — job runs if all dependencies succeeded
  • failure() — job runs if any dependency failed
  • always() — job always runs
  • cancelled() — job runs if any dependency was cancelled

Branch and event conditions (e.g., branch == 'main', event == 'pull_request') are not allowed at the job level. Use them at the step level instead, or gate job execution via triggers: branch filtering.

Execute steps only when specific conditions are met:

steps:
- name: Build
run: xcodebuild build
- name: Notify on Success
if: success()
run: curl https://example.com/notify?status=success
- name: Notify on Failure
if: failure()
run: curl https://example.com/notify?status=failure
- name: Deploy (main branch only)
if: success() && branch == 'main'
run: fastlane beta
- name: Cleanup (always)
if: always()
run: rm -rf build

Supported step-level if: functions:

  • success() — true if all previous steps succeeded
  • failure() — true if any previous step failed
  • always() — true (step always runs)
  • cancelled() — true if the job was cancelled

Context variables (step-level only):

  • branch — Current git branch name (e.g., main, develop)
  • event — Trigger type (e.g., push, pull_request, manual, schedule). Note: The YAML triggers: array only accepts push and pull_request; manual and schedule are runtime trigger types available when builds are triggered from the dashboard or schedules.
  • env.VARIABLE_NAME — Environment variable

Examples:

# Deploy only on main branch (step-level)
if: success() && branch == 'main'
# Run on pull requests only (step-level)
if: event == 'pull_request'
# Cleanup if env var is set (step-level)
if: always() && env.CLEANUP_ENABLED == 'true'

Automatically retry a step if it fails:

steps:
- name: Build
run: xcodebuild build
retry: 2
retry_delay: 5 # seconds between attempts
- name: Upload Artifact
run: curl -X POST -T app.ipa https://webhook.example.com/upload
retry: 3
retry_delay: 10

Behavior:

  • Step runs up to retry times before failing
  • If retry_delay is specified, wait that many seconds between attempts
  • All attempts are logged
  • Final failure fails the entire job

Run multiple steps simultaneously within a single job. All parallel steps share the same filesystem, environment variables, and working directory.

Parallel steps execute concurrently on the same VM, unlike multi-job pipelines where each job gets its own isolated VM. This means:

  • All steps can read and write to the same files
  • Environment variables and working directory changes persist across parallel steps
  • There is no filesystem isolation between parallel steps

This is ideal for running independent operations (tests, linting, builds) that don’t depend on each other’s output.

steps:
- name: Setup
run: pod install
- parallel:
- name: Run Unit Tests
run: xcodebuild test -scheme UnitTests
- name: Run UI Tests
run: xcodebuild test -scheme UITests
- name: Lint Code
run: swiftlint
- name: Build For Release
run: xcodebuild build -scheme MyApp -configuration Release

Important: The parallel: key contains an array of steps (note the proper indentation with nested dashes).

All standard step features work inside parallel: blocks:

- parallel:
- name: Test with Retry
run: xcodebuild test
retry: 2
retry_delay: 5
- name: Conditional Lint
if: branch == 'main'
run: swiftlint
- name: Build in Subdirectory
run: xcodebuild build
working_directory: ios
- name: Test with Custom Env
run: xcodebuild test
env:
ENABLE_TESTING: "true"
SWIFT_VERSION: "5.10"
  • if: — conditionally include a parallel step
  • retry: — automatically retry if the step fails
  • retry_delay: — seconds to wait between retries
  • env: — step-level environment variables
  • working_directory: — run in a specific directory

Execution:

  • All steps in a parallel: group start at the same time
  • The pipeline waits for ALL parallel steps to finish before moving to the next sequential step
  • Order of execution within the parallel group is not guaranteed

Failure Handling:

  • If one parallel step fails, the other parallel steps continue running to completion (they don’t cancel each other)
  • After a parallel group, if any step failed, subsequent sequential steps are skipped (the job fails)
  • Use if: always() on a sequential step to run it regardless of previous failures

Example with failure handling:

steps:
- name: Setup
run: pod install
- parallel:
- name: Unit Tests
run: xcodebuild test -scheme UnitTests
- name: UI Tests
run: xcodebuild test -scheme UITests
- name: Generate Report
if: always() # Runs even if tests failed
run: xcrun xcodebuild -resultBundlePath build/test-results
- name: Deploy
if: success() # Only if everything succeeded
run: fastlane beta

1. Test Suite in Parallel

steps:
- name: Install dependencies
run: pod install
- parallel:
- name: Unit Tests
run: xcodebuild test -scheme MyAppTests
- name: Integration Tests
run: xcodebuild test -scheme IntegrationTests
- name: Snapshot Tests
run: xcodebuild test -scheme SnapshotTests
- name: Build for Distribution
run: xcodebuild build -scheme MyApp -configuration Release

2. Build Multiple Targets

- parallel:
- name: Build Main App
run: xcodebuild build -scheme MyApp
- name: Build Watch App
run: xcodebuild build -scheme MyAppWatch
- name: Build Share Extension
run: xcodebuild build -scheme MyAppShare

3. Lint and Test in Parallel

- parallel:
- name: Run Tests
run: fastlane test
- name: Lint Code
run: swiftlint
- name: Check Syntax
run: swift build

Parallel steps are not suitable when:

  • Steps depend on each other’s output — e.g., build produces artifacts that tests need. Use sequential steps instead
  • Steps modify the same files — concurrent writes can conflict. Sequence them or use different file paths
  • You need VM isolation — use multi-job pipelines (jobs:) instead of parallel steps

Create multiple job variants from a single job definition:

jobs:
test:
name: Test ${{ matrix.swift_version }}
strategy:
matrix:
swift_version: ["5.8", "5.9", "5.10"]
os: [ios, macos]
steps:
- name: Setup Swift ${{ matrix.swift_version }}
run: swift --version
- name: Test on ${{ matrix.os }}
run: swift test

This creates 6 jobs (3 Swift versions × 2 OSes).

Matrix features:

  • ${{ matrix.KEY }} substitutes values in step names and commands
  • Environment variables like MATRIX_SWIFT_VERSION, MATRIX_OS are injected
  • Each matrix combination becomes an independent job
  • Matrix variants are scheduled independently — one variant failing does not cancel its sibling variants. However, any job that needs: a matrix job waits for all variants of that job to succeed

Optional fields:

strategy:
matrix:
node: [14, 16, 18]
exclude:
- node: 14
max-parallel: 2 # Limit concurrent jobs from this matrix
  • exclude: — Exclude specific combinations
  • max-parallel: — Max simultaneous jobs (1-50)

RunnerHub supports limited expression interpolation in pipeline YAML. Only the ${{ matrix.* }} namespace is expanded by the platform:

jobs:
test:
name: Test ${{ matrix.swift_version }}
strategy:
matrix:
swift_version: ["5.8", "5.9", "5.10"]
steps:
- run: swift ${{ matrix.swift_version }} --version

Important: The following interpolations are NOT supported and will not be expanded:

  • ${{ env.VARIABLE }} — Use shell $VARIABLE instead
  • ${{ secrets.SECRET }} — Use shell $SECRET instead (secrets are injected as env vars)
  • ${{ vars.VAR }} — Use shell $VAR instead (variables are injected as env vars)
  • ${{ github.* }} — GitHub context is not available

Instead, reference environment variables and secrets directly as shell variables (e.g., $MY_VAR, $MY_SECRET), since they are all injected into the shell environment and available via the standard shell $ syntax.


name: Complete CI Pipeline
platform: ios
environment:
xcode: "16.4"
triggers:
- push
- pull_request
jobs:
lint:
name: Lint
steps:
- run: swiftlint
build:
name: Build ${{ matrix.scheme }}
needs: [lint]
strategy:
matrix:
scheme: [Release, Debug]
steps:
- run: pod install
- run: xcodebuild build -scheme MyApp -configuration ${{ matrix.scheme }}
test:
name: Test
needs: [build]
steps:
- run: pod install
- run: fastlane test
notify:
name: Notify
if: always()
needs: [test]
steps:
- name: Success notification
if: success()
run: curl https://example.com/notify?status=success
- name: Failure notification
if: failure()
run: curl https://example.com/notify?status=failure

Execution order:

  1. lint runs first (no dependencies)
  2. build runs 2× (Debug + Release) after lint succeeds
  3. test runs after both build variants complete
  4. notify always runs (regardless of test success) with appropriate message

Use the android_sdk field to specify your target Android SDK level.

environment:
flutter: "3.24.0" # Required: specify Flutter version for golden image selection
variables:
KEY: value

Use the flutter field to specify which Flutter SDK to install and use. The agent automatically installs the Flutter SDK based on this field. Supported values:

  • Semver version (e.g., "3.24.0") — Install a specific Flutter version
  • Channel (e.g., stable, beta, master) — Install from a Flutter release channel
  • fvm — Read the version from your repository’s .fvmrc file

When invoking Flutter in your steps, use the fvm prefix (e.g., fvm flutter build or fvm dart test) since the Flutter bin directory is not on the default PATH.

For details, see the Flutter quick start guide.

PlatformStatusNotes
iosFully SupportediPhone and iPad builds with code signing and deploy to TestFlight/App Store
macosFully SupportedMac app builds with code signing
androidFully SupportedNative Android builds with keystore signing, Google Play deploy, and build-number auto-increment
flutterFully SupportedCross-platform builds via FVM with Apple and Android signing support
react-nativeFully SupportedDual iOS/Android builds with automatic Node provisioning, unified signing, Metro caching, and deploys to TestFlight/App Store/Google Play/Firebase

If your app uses Cloud YAML (configured in the dashboard under App → Cloud YAML), branch-specific configurations override the repository runnerhub.yml for that branch. Cloud YAML is organized per branch, allowing different pipeline configurations for different branches.

Requirements:

  • Your app must have a connected repository to create Cloud YAML entries (so branch names can be validated)

Lock behavior: Each branch’s Cloud YAML can be locked independently to prevent modifications until explicitly unlocked via the dashboard. When locked, the backend-stored configuration is used; when unlocked or not configured, the repository file is used.