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.
File Location
Section titled “File Location”By default, RunnerHub looks for the configuration file at:
repo-root/├─ .runnerhub/│ └─ runnerhub.yml ← primary (checked first)└─ runnerhub.yml ← fallback (if primary not found)Custom Pipeline Path
Section titled “Custom Pipeline Path”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
.ymlor.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)Minimal Valid Pipeline
Section titled “Minimal Valid Pipeline”The smallest valid pipeline configuration requires four fields:
name: My First Pipelineplatform: ios
environment: xcode: "16.4"
triggers: - push
steps: - name: Build run: echo "Building..."Complete Schema
Section titled “Complete Schema”Here is the full structure of a RunnerHub pipeline configuration:
name: stringplatform: 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: numberField Reference
Section titled “Field Reference”| Field | Required | Type | Description |
|---|---|---|---|
name | Yes | string | Human-readable pipeline name, displayed in dashboard and logs |
platform | Yes | string | Target platform: ios, macos, android, flutter, or react-native |
environment | Yes | object | Platform-specific tool versions (see Platform Requirements below) |
triggers | Yes | array | List of events that trigger the pipeline; use string format (push, pull_request) or object format with optional branch filtering (see Triggers) |
steps | Yes | array | Ordered list of commands to execute |
artifacts | No | array | Glob patterns for build outputs to collect and store |
timeout | No | number | Maximum job duration in minutes (default: 60, max: 30–120 depending on plan) |
Trigger Formats
Section titled “Trigger Formats”Triggers support two formats:
String format — trigger on all branches:
triggers: - push - pull_requestObject format — trigger with optional branch filtering:
triggers: - event: push branches: - main - develop - release/* - event: pull_request branches: - mainThe 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.
Step Properties
Section titled “Step Properties”Each step in the steps array supports these fields:
| Field | Required | Type | Description |
|---|---|---|---|
name | Yes | string | Human-readable step name, displayed in logs and dashboard |
run | Yes | string | Shell command(s) to execute |
working_directory | No | string | Directory where the command runs (useful for monorepos) |
env | No | object | Step-level environment variables (override pipeline-level variables) |
Full-Featured Example
Section titled “Full-Featured Example”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: 45Platform Requirements
Section titled “Platform Requirements”Each platform requires specific tool version specifications in the environment block:
iOS and macOS
Section titled “iOS and macOS”environment: xcode: "16.4" # Required: specify Xcode version variables: KEY: valueUse the xcode field to specify your Xcode version. Available versions depend on the agent image.
Android
Section titled “Android”environment: android_sdk: 34 # Required: specify Android SDK level variables: KEY: valueReact Native
Section titled “React Native”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: valueRequired: 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.
Multi-Job Workflows
Section titled “Multi-Job Workflows”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 vs Multi-Job Format
Section titled “Single-Job vs Multi-Job Format”Single-job format — uses steps: at the top level:
name: My Pipelineplatform: ios
triggers: - push
steps: - name: Build run: xcodebuild build - name: Test run: xcodebuild testMulti-job format — uses jobs: at the top level; each job has its own steps::
name: My Pipelineplatform: ios
triggers: - push
jobs: build: name: Build App steps: - name: Build run: xcodebuild build
test: name: Run Tests steps: - name: Test run: xcodebuild testWhen using jobs:, the top-level steps: field is ignored.
Job Keys and Names
Section titled “Job Keys and Names”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 inneeds:dependencies and logs name:— human-readable display name shown in the dashboard
Job Dependencies with needs:
Section titled “Job Dependencies with needs:”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 betaDependency 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
Parallel Execution
Section titled “Parallel Execution”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 UITestsIn this example:
buildruns firsttest-unitandtest-uirun in parallel afterbuildcompletes- This is faster than running them sequentially
Pipeline Status and Cascading Failures
Section titled “Pipeline Status and Cascading Failures”- 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:
testanddeployare 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 Pipelineplatform: 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 betaExecution flow:
buildrunstest-unitandtest-uirun in parallel after build completes (each installs its own deps)archivewaits for both tests to complete and installs its own depsdeployruns only on the main branch and only after archive succeeds
Job Structure Reference
Section titled “Job Structure Reference”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: 5All step properties (retry, env, working_directory, etc.) work the same way as single-job pipelines.
Conditional Execution
Section titled “Conditional Execution”Job-Level Conditionals (if: on jobs)
Section titled “Job-Level Conditionals (if: on jobs)”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/notifySupported job-level if: values:
success()— job runs if all dependencies succeededfailure()— job runs if any dependency failedalways()— job always runscancelled()— 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.
Step-Level Conditionals (if: on steps)
Section titled “Step-Level Conditionals (if: on steps)”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 buildSupported step-level if: functions:
success()— true if all previous steps succeededfailure()— true if any previous step failedalways()— 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 YAMLtriggers:array only acceptspushandpull_request;manualandscheduleare 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'Step Retries
Section titled “Step Retries”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: 10Behavior:
- Step runs up to
retrytimes before failing - If
retry_delayis specified, wait that many seconds between attempts - All attempts are logged
- Final failure fails the entire job
Parallel Steps
Section titled “Parallel Steps”Run multiple steps simultaneously within a single job. All parallel steps share the same filesystem, environment variables, and working directory.
Key Concept
Section titled “Key Concept”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.
YAML Syntax
Section titled “YAML Syntax”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 ReleaseImportant: The parallel: key contains an array of steps (note the proper indentation with nested dashes).
Step Features Inside Parallel Groups
Section titled “Step Features Inside Parallel Groups”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 stepretry:— automatically retry if the step failsretry_delay:— seconds to wait between retriesenv:— step-level environment variablesworking_directory:— run in a specific directory
Behavior Details
Section titled “Behavior Details”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 betaParallel Steps vs Multi-Job Parallelism
Section titled “Parallel Steps vs Multi-Job Parallelism”Common Use Cases
Section titled “Common Use Cases”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 Release2. 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 MyAppShare3. Lint and Test in Parallel
- parallel: - name: Run Tests run: fastlane test - name: Lint Code run: swiftlint - name: Check Syntax run: swift buildWhen NOT to Use Parallel Steps
Section titled “When NOT to Use Parallel Steps”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
Matrix Builds
Section titled “Matrix Builds”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 testThis 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_OSare 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 matrixexclude:— Exclude specific combinationsmax-parallel:— Max simultaneous jobs (1-50)
Interpolation and Variable Expansion
Section titled “Interpolation and Variable Expansion”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 }} --versionImportant: The following interpolations are NOT supported and will not be expanded:
${{ env.VARIABLE }}— Use shell$VARIABLEinstead${{ secrets.SECRET }}— Use shell$SECRETinstead (secrets are injected as env vars)${{ vars.VAR }}— Use shell$VARinstead (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.
Complete Multi-Job Example
Section titled “Complete Multi-Job Example”name: Complete CI Pipelineplatform: 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=failureExecution order:
lintruns first (no dependencies)buildruns 2× (Debug + Release) after lint succeedstestruns after both build variants completenotifyalways runs (regardless of test success) with appropriate message
Use the android_sdk field to specify your target Android SDK level.
Flutter
Section titled “Flutter”environment: flutter: "3.24.0" # Required: specify Flutter version for golden image selection variables: KEY: valueUse 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.fvmrcfile
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.
Platform Support Status
Section titled “Platform Support Status”| Platform | Status | Notes |
|---|---|---|
ios | Fully Supported | iPhone and iPad builds with code signing and deploy to TestFlight/App Store |
macos | Fully Supported | Mac app builds with code signing |
android | Fully Supported | Native Android builds with keystore signing, Google Play deploy, and build-number auto-increment |
flutter | Fully Supported | Cross-platform builds via FVM with Apple and Android signing support |
react-native | Fully Supported | Dual iOS/Android builds with automatic Node provisioning, unified signing, Metro caching, and deploys to TestFlight/App Store/Google Play/Firebase |
Cloud YAML
Section titled “Cloud YAML”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.
Next Steps
Section titled “Next Steps”- Learn about Triggers to control when your pipeline runs
- Configure Environment Variables for your build
- Set up Artifacts to collect build outputs
- Understand Timeout & Limits for your plan