self-host →

Job labels

Configure RunsOn runners with per-job runs-on labels and reusable runner definitions — CPU, GPU, RAM, instance type, volume size, and more.

RunsOn jobs pick a runner through runs-on: labels. What the label carries depends on the product: in Flex the label configures the runner shape per job (CPU, RAM, family, image, volume, extras, …); in Fleet the label only targets a platform-owned runner fleet whose shape is fixed in Terraform.

Usage

Flex

Flex configures the runner shape directly from the runs-on: label, at runtime, per job — set CPU, RAM, instance family, image, volume, nested virtualization, extras, and more:

jobs:
build:
runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64/family=c7+m7/extras=s3-cache

The rest of this page is the full reference for these Flex labels.

Fleet

Fleet workflows do not set the runner shape per job. They target a platform-owned runner fleet; the shape (CPU, RAM, image, extras) lives in the Terraform runners catalog:

jobs:
build:
runs-on: runs-on/fleet=linux-small/env=production

Only fleet and env are chosen in the workflow. To change a shape, add or edit a runner fleet in Terraform. See Fleet installation guide and Custom runners & images.

Available labels

family

Instance type family. Can either be:

  • instance type full name e.g. family=c7a.large,
  • a partial name e.g. family=c7 (this will automatically get expanded to c7* wildcard),
  • a wildcard name e.g. family=c7a.*, particularly useful when multiple instance types have the same prefix but want a specific one (e.g. m7i vs m7i-flex, c7g vs c7gd, etc.)
  • multiple values, separated by +: e.g. family=c7+c6, family=m7i.*+m7a, etc.

Partial names and wildcards are useful when you want to specify a range of instance types, but don’t want to specify each one individually.

If the family definition matches multiple instance types, AWS will select the instance type that matches the requirements, and is ranked best according to the selected spot allocation strategy, at the time of launch. Sometimes it can happen that a beefier instance is cheaper than a smaller one on the spot marklet.

E.g.

  • family=c7a+c6 will ensure that the runner is scheduled on an instance type in the c7a* or c6* instance type family.
  • family=c7a.2xlarge will ensure that the runner always runs on a c7a.2xlarge instance type (however if AWS has no capacity left, the runner could fail to launch. It’s always recommended to use a range of instance types, instead of a single one).
.github/workflows/my-workflow.yml
jobs:
test:
runs-on:
- runs-on=${{github.run_id}}/runner=2cpu-linux-x64/family=m6+c6

nested-virt

Enable nested virtualization on the selected EC2 launch template when the instance type supports it.

For the concept, constraints, and Fleet guidance, see Nested virtualization. This section only covers the Flex label syntax.

This is a host capability, not an extras feature. Bare nested-virt means true, and you can explicitly disable it with nested-virt=false.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/family=c8i+m8i+r8i/cpu=2+4/nested-virt/image=windows25-full-x64

Nested-enabled jobs require a RunsOn stack deployment that includes the nested launch templates. Existing stacks need to be upgraded before nested-virt jobs can run.

cpu

Number of vCPUs to request (default: 2). If you set multiple values, RunsOn will request any instance matching the lowest up to the highest value.

E.g.

  • cpu=4 will ensure that the runner has 4 vCPUs (min=4, max=4).
  • cpu=4+16 will ensure that the runner has at least 4 vCPUs but also consider instances with up to 16 vCPUs.

Setting a variable amount of vCPUs is useful for expanding the pool of available spot instances, if your workflow is not to sensitive to the exact number of vCPUs.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/family=m7+c7+r7/cpu=2+8/image=ubuntu22-full-x64

ram

Amount of memory to request, in GB (default: 0). If you set multiple values, RunsOn will request any instance matching the lowest up to the highest value.

E.g.

  • ram=16 will ensure that the runner has 16GB of RAM (min=16, max=16).
  • ram=16+64 will ensure that the runner has at least 16GB of RAM but also consider instances with up to 64GB of RAM.
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/family=m7+c7/ram=16/image=ubuntu22-full-x64

image

Runner image to use (see Runner images). Especially useful when you want to use a custom image, or don’t want to specify a runner label (in this case, family is required).

E.g.

  • image=ubuntu22-full-x64 will ensure that the runner is launched with the ubuntu22-full-x64 image.
  • image=ubuntu22-full-arm64 will ensure that the runner is launched with the ubuntu22-full-arm64 image.
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/family=m7+c7/cpu=2/image=ubuntu22-full-arm64

ami

AMI to use for the runner. Can be used if you don’t want to declare a custom image (see above), or for quick testing. For long-term use, declaring a custom image is recommended, because it can match AMIs based on a wildcard.

The AMI must be a valid AMI ID for the region where the runner is launched, and must either be a public image, or be accessible to the stack’s IAM role (by default the AMIs within the same account are accessible).

E.g.

  • ami=ami-0123456789abcdef0 will ensure that the runner is launched with the ami-0123456789abcdef0 AMI.
.github/workflows/my-workflow.yml
jobs:
test:
runs-on:
- runs-on=${{github.run_id}}/family=m7+c7/ami=ami-0123456789abcdef0

volume

Added in v2.9

Volume configuration with flexible size and performance options. Format: size:type:throughput:iops (e.g., volume=80gb:gp3:125mbs:3000iops).

All parts are optional and can be specified in any order:

  • Size: Volume size (e.g., 80gb, 500g, 1tb)
  • Type: EBS volume type - gp3, gp2, io1, io2, st1, sc1, standard (default: gp3)
  • Throughput: Throughput in MiB/s (e.g., 125mbs, 250mbps) - only for gp3 volumes. RunsOn clamps the value to the 125-2000 range; the effective maximum is further capped at 0.25 MiB/s per IOPS (so 1000 MiB/s requires at least 4000 IOPS).
  • IOPS: IOPS performance (e.g., 3000iops, 16000iops) - for gp3, io1, io2 volumes. RunsOn clamps the value to the 3000-80000 range. IOPS are additionally capped at 500 IOPS per GB of volume size.

For gp3 volumes, AWS requires the throughput/IOPS ratio to be ≤ 0.25 MiBps per IOPS. RunsOn will automatically adjust IOPS if needed to meet this requirement.

If you require more flexible disk sizes or maximum performance, consider using instance types that come with locally-attached NVMe disks (see here). RunsOn will automatically mount them (in a RAID-0 for better performance if multiple disks are detected) and point the runner workspace and docker lib folder to it.

Personal recommendation: if you have access to it, the i7ie family is very good (a mix of both great CPU performance, and local instance storage).

Examples:

  • volume=80gb - 80GB volume with default type (gp3)
  • volume=200gb:gp3 - 200GB gp3 volume
  • volume=100gb:gp3:500mbs:4000iops - 100GB gp3 volume with 500 MiB/s throughput and 4000 IOPS
  • volume=gp3:750mbs:4000iops - Use default size with custom throughput and IOPS
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/volume=80gb:gp3:500mbs:4000iops

disk

Added in v2.5.7. Deprecated - use volume instead.

Legacy disk configuration. One of default or large (default: default).

In v3 the disk label is parsed but ignored (no longer translated to a volume) and emits a deprecation warning. Use explicit volume=... values instead.

Migration guide: Use the volume label for more flexible configuration:

  • disk=large -> volume=80gb
  • disk=default -> an explicit size that matches your workload, such as volume=40gb
  • For custom sizes and performance: volume=100gb:gp3:500mbs:4000iops
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/disk=large # deprecated

spot

Whether to attempt to use spot pricing (default: true, equivalent to price-capacity-optimized). Can be set to an explicit spot allocation strategy.

For the concept, interruption tradeoffs, and when to disable Spot, see Spot pricing. This section only covers the Flex label syntax.

E.g. spot=false will ensure that the runner is launched with regular on-demand pricing.

Supported allocation strategies on RunsOn include:

  • spot=price-capacity-optimized or spot=pco: This strategy balances between price and capacity to optimize cost while minimizing the risk of interruption.
  • spot=lowest-price or spot=lp: This strategy focuses on obtaining the lowest possible price, which may increase the risk of interruption.
  • spot=capacity-optimized or spot=co: This strategy prioritizes the allocation of instances from the pools with the most available capacity, reducing the likelihood of interruption.

For more details on each strategy, refer to the official AWS documentation on Spot Instance allocation strategies.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/spot=lowest-price

retry

Added in v2.6.0

Retry behaviour. Currently only supported for spot instances.

  • retry=when-interrupted: default for spot instances. Will retry at most once the interrupted job, using an on-demand instance.
  • retry=false: opt out of the retry mechanism.
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/retry=false

ssh

Whether to enable SSH access (default: false).

E.g.

  • ssh=false will ensure that the runner is launched with SSH access fully disabled.
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/ssh=false

private

Whether to launch a runner in a private subnet, and benefit from a static egress IP.

The default for this label depends on your Stack configuration for the Private parameter:

  • If the stack parameter Private is set to true, private subnets will be enabled but runners will be public by default. You need to set the job label private=true to launch a runner in the private subnet.

  • If the stack parameter Private is set to always, runners will be private by default and you must set the job label private=false to launch a runner in the public subnet.

  • If the stack parameter Private is set to only, runners can only be launched in private subnets and you will get an error if you try to specify the job label private=false.

  • If the stack parameter Private is set to false, runners can only be launched in public subnets and you will get an error if you try to specify the job label private=true.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/private=true

extras

Extra configuration for the runner.

Currently supports

  • s3-cache (since v2.6.3): enables the magic cache feature (available for Linux and Windows runners).
  • ecr-cache (since v2.8.2): enables the ephemeral registry feature, if enabled at the stack level (available for Linux runners only).
  • efs (since v2.8.2): enables the EFS feature, if enabled at the stack level (available for Linux runners only).
  • tmpfs (since v2.8.2): enables the tmpfs feature (available for Linux runners only).
  • otel (since v2.12.0, beta): enables runner-side OpenTelemetry collection. It starts the local collector, keeps the inline job-summary metrics flow available, and when your stack has an OTLP endpoint configured it can export bootstrap logs, RunsOn traces, and profile-dependent host metrics. See OpenTelemetry for the exact signal behavior.

E.g. extras=s3-cache will enable the magic cache.

otel uses the stack OTLP settings such as OtelExporterEndpoint and OtelExporterHeaders.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/extras=s3-cache

You can also combine multiple extras:

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/extras=s3-cache+ecr-cache+tmpfs+efs+otel

debug

Whether to enable debug mode for the job (default: false). Note: this is only available on Linux runners for now.

E.g. debug=true will ensure that the runner is launched with debug mode enabled.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/debug=true

When enabled, the runner will pause before executing the first step of the job. You can then connect to the runner using SSH or SSM to debug the job. When you are ready to resume the job, you simply need to remove the debug lock file:

sudo rm /runs-on/hooks/debug.lock

At that point the runner will resume the job execution.

Special labels

runner

Using the previous labels, you can configure every aspect of a runner right from the runs-on: workflow job definition.

However, if you want to reuse a runner configuration across multiple workflows, you can define a custom runner type in a .github/runs-on.yml configuration file in the repository where you want those runners to be available, and reference that runner type with the runner label.

E.g.

  • runner=16cpu-linux-x64 will ensure that the runner is launched with the 16cpu-linux-x64 runner type. Learn more about default and custom runner configurations for Linux and Windows.

Important: this label cannot be set as part of a custom runner configuration in the .github/runs-on.yml file.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=16cpu-linux-x64

env

The RunsOn Environment to target. Defaults to production.

E.g.

  • env=staging will ensure that only a runner from the RunsOn staging stack is used to execute this workflow. This allows you to isolate different workflows in different environments, with different IAM permissions or stack configurations, etc.

See Environments for more details.

Important: this label cannot be set as part of a custom runner configuration in the .github/runs-on.yml file.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/env=staging

region

This label is only useful if you have set up multiple RunsOn stacks in different AWS regions. If so, then you can use this label to specify which region to launch the runner in.

If you have multiple stacks in different regions listening on the same repositories, make sure that all your workflows use the region label, to ensure that only one stack launches a runner for a given job.

E.g.

  • region=eu-west-1 will ensure that the runner is launched in the eu-west-1 region (assuming a RunsOn stack has been set up in that region).
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/region=eu-west-1

pool

Target a specific runner pool for this job. Pools are pre-provisioned runners that stay warmed up and ready, dramatically reducing queue times from ~25 seconds (cold-start) to under 6 seconds for hot instances.

When using the pool label, all other RunsOn labels (like cpu, ram, family) are ignored. The runner specification is determined entirely by the pool configuration defined in .github-private/.github/runs-on.yml.

E.g. pool=small-x64 will route the job to instances from the small-x64 pool.

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on/pool=small-x64
# or for more deterministic runner assignment:
runs-on: runs-on=${{github.run_id}}/pool=small-x64

Automatic overflow: If the pool is exhausted (all instances in use), RunsOn automatically creates a cold-start instance to handle the job, ensuring jobs never fail due to lack of pool capacity.

Important notes:

  • Pool configurations must be defined in .github-private/.github/runs-on.yml
  • The .github-private repository must be accessible to the RunsOn GitHub App
  • This label cannot be set as part of a custom runner configuration

For comprehensive documentation about configuring and using pools, see the Runner pools guide and pool configuration reference.

Label syntax

How it works

The way you can define your requirements is by specifying custom labels for the runs-on: parameter in your workflow.

For instance, if you want a runner with 4 CPUs, 16GB RAM, using either m7a or m7i-flex instance types:

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{ github.run_id }}/cpu=4/ram=16/family=m7a+m7i-flex/image=ubuntu22-full-x64
# can also be written with comma-separated values instead of slash-separated values:
runs-on: runs-on=${{ github.run_id }},cpu=4,ram=16,family=m7a+m7i-flex,image=ubuntu22-full-x64

RunsOn also supports the array syntax, but we recommend the single-string syntax for most workflows:

.github/workflows/my-workflow.yml
jobs:
test:
runs-on:
- runs-on=${{ github.run_id }}
- cpu=4
- ram=16
- family=m7a+m7i-flex
- image=ubuntu22-full-x64

Single string vs array syntax

GitHub does not interpret these two forms the same way:

  • With the single-string syntax, GitHub sees one label. Another job can only reuse that runner if it requests the exact same string.
  • With the array syntax, GitHub sees multiple labels. Another job can match that runner if it requests a subset of those labels.

That means these two array-based jobs can overlap:

jobs:
test1:
runs-on:
- runs-on=${{ github.run_id }}
- cpu=4
- family=m7a+m7i-flex
test2:
runs-on:
- runs-on=${{ github.run_id }}
- family=m7a+m7i-flex

test2 can steal the runner launched for test1, because GitHub matches all requested labels and test2 asks for a subset of test1’s labels.

The equivalent single-string syntax does not overlap:

jobs:
test1:
runs-on: runs-on=${{ github.run_id }}/cpu=4/family=m7a+m7i-flex
test2:
runs-on: runs-on=${{ github.run_id }}/family=m7a+m7i-flex

Those are two different labels, so GitHub will not consider them interchangeable.

Array syntax can still be safe if one of the labels is fully unique, for example runs-on=${{ github.run_id }}-${{ strategy.job-index }}. The reason we still recommend the single-string syntax is that it is much harder to accidentally create overlapping label sets, and easier to reason about when debugging runner stealing.

Custom runner definitions

Instead of repeating labels across workflows, you can define reusable runner configurations in a .github/runs-on.yml file at the repository or organization level. Reference them with the runner label (e.g. runner=my-custom-runner).

See Repository configuration below for the full reference.

Repository configuration

Beyond per-job labels, RunsOn can read reusable definitions from a configuration file located at .github/runs-on.yml (must be named exactly that) in the repository where you are using RunsOn runners.

The configuration file supports defining:

  • custom images
  • custom runners
  • runner pools for faster job pickup times
  • the list of admins having SSH access to the runners
  • a global configuration file to extend from

Structure

The configuration file is a YAML file with the following top-level keys: _extends, runners, images, pools, and admins.

.github/runs-on.yml
_extends: ...
runners: {...}
images: {...}
pools: {...}
admins: [...string]

_extends

If set, the configuration file will inherit from the configuration file in the repository specified by the _extends value.

A common use case is to define a global configuration file in the .github-private repository of your organization, and then inherit from it in other repositories. Do not forget to allow the RunsOn GitHub App to access the repository hosting the global configuration file.

Example

.github/runs-on.yml
_extends: .github-private
runners:
...

In this example, the configuration file in the repository will inherit from the configuration file stored at .github/runs-on.yml in the .github-private repository.

runners

Mapping of custom runner names to runner configuration.

Available configuration options: cpu, ram, volume, disk (deprecated), family, spot, image, ssh, extras, private, nested-virt, retry, preinstall, prerun, tags.

For more details about each option, please refer to the job-level labels documentation above.

Example

.github/runs-on.yml
runners:
docker_with_ipv6:
cpu: [2, 4]
ram: [2, 8]
volume: 80gb:gp3
family: ["c7", "m7", "r7"]
nested-virt: true
spot: capacity-optimized
tags:
- custom-tag-key1:custom-tag-value1
- custom-tag-key2-no-value
preinstall: |
cat > /etc/docker/daemon.json <<EOF
{
"ipv6": true,
"fixed-cidr-v6": "2001:db8:1::/64"
}
EOF
systemctl restart docker
prerun: |
echo "Refresh short-lived credentials right before the job starts"
su - runner -c "aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com"
other-runner:
...

And the runner will be available for use in the repository:

jobs:
test:
runs-on: runs-on=${{ github.run_id }}/runner=docker_with_ipv6

runners.family

Required. Array of strings. e.g ["c7", "m7"].

runners.image

Required. String. e.g ubuntu22-full-x64.

runners.cpu

Array of integers. e.g [2, 4].

runners.ram

Array of integers. e.g [2, 8].

runners.volume

String specifying volume configuration. Format: size:type:throughput:iops (e.g., 80gb:gp3:125mbs:3000iops).

All parts are optional and can be in any order:

  • Size: 80gb, 500g, 1tb
  • Type: gp3, gp2, io1, io2, st1, sc1, standard
  • Throughput: 125mbs, 250mbps (gp3 only)
  • IOPS: 3000iops, 4000iops (gp3, io1, io2)

Examples: 100gb, 80gb:gp3, 200gb:gp3:500mbs:4000iops

runners.disk

Deprecated - use volume instead.

String. e.g default or large.

Do not rely on this in v3. The old stack-level default-disk compatibility knobs are gone, and the correct migration path is to use explicit volume=... configuration.

runners.spot

Boolean or string. e.g capacity-optimized, co, lowest-price, lp, price-capacity-optimized, pco, true, false.

runners.ssh

Boolean. e.g true, false.

runners.extras

Array of strings. e.g ["s3-cache", "otel"].

Supported values: s3-cache, ecr-cache, efs, tmpfs, otel.

runners.private

Boolean. e.g true, false.

runners.nested-virt

Boolean. e.g true, false.

Enable nested virtualization for the runner. This requires a stack deployment that includes the nested launch templates, and the runner image must be x64.

runners.retry

String. e.g when-interrupted, false.

runners.preinstall

String. Script to run on the runner, before the GitHub Actions runner agent is executed (see images.preinstall for more details).

Use this for setup that can happen ahead of time, such as installing packages or writing config files. Use prerun instead for docker login and other short-lived credentials that must still be fresh when the job begins.

runners.prerun

String. Script to run on the runner just before the workflow job starts (see images.prerun for more details).

runners.tags

Array of strings containing custom key:value tags to apply to the runner underlying EC2 instance. e.g. ["custom-tag-key1:custom-tag-value1", "custom-tag-key2-no-value"].

Cannot start with the runs-on- prefix. Tag keys and values will be automatrically sanitized to remove any special characters (/ is allowed for values).

images

Mapping of custom image names to image configuration.

Available configuration options: name, ami, platform, arch, owner, preinstall, prerun, tags.

Example

.github/runs-on.yml
images:
custom:
owner: "123456789"
name: "my-org/my-image-name-*"
arch: x64
platform: linux
tags:
# filter with specific value
is-production-ready: "true"
# allow any value
other-tag: "*"
fixed-ami:
ami: "ami-0abcd159cbfafefgh"

And you can use the image in your workflows like this:

jobs:
test:
runs-on: runs-on=${{ github.run_id }}/image=custom
# or
runs-on: runs-on=${{ github.run_id }}/image=fixed-ami

images.name

Required (unless ami is set). String. Can include wildcards, in which case RunsOn will pick the most recent image matching the pattern.

images.ami

Required (unless name is set). String. If set, the image will be pinned to the specified AMI, irrespective of the name option.

images.platform

String. Either linux or windows. Not required if ami is set.

images.arch

String. Either x64 or arm64. Not required if ami is set.

images.owner

String. AWS account ID where the image is hosted.

images.preinstall

String. Script to run on the runner, before the GitHub Actions runner agent is executed. Takes precedence over the preinstall option at the runner level if both are set.

Useful to perform setup that can happen ahead of time, such as installing packages, pulling large base images, or writing config files. Use prerun instead for docker login and other short-lived credentials. Installing additional software will make your runner boot time slower, it is recommended that you create your own AMI instead.

If the preinstall script fails, the job will fail. You will find the preinstall exit status and logs in the “Set up runner” section of the job logs.

Platform behaviour:

  • On Linux, the preinstall script is executed as the root user, before the GitHub Actions runner agent is launched. The script must be a valid bash script, and will be executed with bash -e (so any failure will fail the script immediately).

  • This option is not yet available for Windows images.

images.prerun

String. Script to run on the runner just before the workflow job starts.

Available since v2.12.0. This is mainly useful with warm pools, where preinstall runs during the warm-up phase and can therefore happen long before a job is assigned. Use prerun for just-in-time setup that must still be fresh when the job begins, such as docker login or other short-lived credentials.

images.tags

Mapping of key-value tags to filter the image when searching for it.

pools

Mapping of pool names to pool configurations. Pools allow you to pre-provision runners that stay warmed up and ready to pick up jobs immediately, reducing queue times from ~25 seconds (cold-start) to under 6 seconds for hot instances.

For comprehensive documentation about pools, see the Runner pools guide.

Available configuration options: env, runner, timezone, schedule.

Example

.github-private/.github/runs-on.yml
runners:
small-x64:
image: ubuntu24-full-x64
ram: 1
family: [t3]
volume: gp3:30gb:125mbps:3000iops
pools:
small-x64:
env: production
runner: small-x64
timezone: "America/New_York"
schedule:
- name: business-hours
match:
day: ["monday", "tuesday", "wednesday", "thursday", "friday"]
time: ["08:00", "18:00"]
stopped: 5
hot: 2
- name: default
stopped: 2
hot: 1

And you can use the pool in your workflows like this:

jobs:
test:
runs-on: runs-on/pool=small-x64

pools.env

String. The stack environment this pool belongs to (e.g., production, dev). This ensures the pool only serves jobs targeting that environment.

pools.runner

Required. String. Reference to a runner definition in the runners section. The pool will create instances matching this runner specification.

pools.timezone

String. IANA timezone for schedule calculations (e.g., America/New_York, Europe/Paris). Defaults to UTC if not specified.

pools.schedule

Required. Array of schedule rules. Each rule defines target capacity (hot/stopped instances) for specific time periods.

Schedule rule fields:

  • name: Human-readable name for this schedule
  • match.day: Array of days when this schedule applies (e.g., ["monday", "tuesday"])
  • match.time: Time range [start, end] in 24-hour format (e.g., ["08:00", "18:00"])
  • stopped: Number of stopped instances to maintain (pre-warmed but not running)
  • hot: Number of hot instances to maintain (running and ready)

Schedules are evaluated in order. The first matching schedule is used. The last schedule without a match field serves as the default.

admins

List of GitHub usernames that have SSH access to the runners launched for this repository.

In v3, there is no stack-level DefaultAdmins parameter anymore. If you need broader privileged instance access, use SSM rather than expecting a stack-wide SSH admin list.

Example

.github/runs-on.yml
admins:
- crohr
- other-github-user

Full example

.github/runs-on.yml
_extends: .github-private
images:
mycustomimage:
platform: "linux"
arch: "x64"
owner: "099720109477"
# will take the most recent AMI matching the wildcard pattern
name: "ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"
otherimage:
platform: "linux"
arch: "x64"
ami: "ami-0abcd159cbfafefgh"
runners:
cheap:
# Useful for workflows that do not require a lot of CPU / RAM.
ram: [2, 4, 8]
# Burstable instances, valid for both x64 and arm64
family: ["t3", "t4"]
image: otherimage
ssh: false
fast:
cpu: 32
volume: 200gb:gp3:750mbs:4000iops
family: ["c7a", "m7a"]
spot: false
# reference custom image defined above
image: mycustomimage
preinstall: |
echo "doing some stuff before the GitHub Actions runner agent is executed..."
echo "For instance, disabling the host's IPv6 if needed"
sysctl -w net.ipv6.conf.all.disable_ipv6=1
admins:
- crohr
- other-github-user

Using the configuration in a GitHub Workflow

.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/image=mycustomimage
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=2cpu-linux-x64/image=otherimage
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=cheap
.github/workflows/my-workflow.yml
jobs:
test:
runs-on: runs-on=${{github.run_id}}/runner=fast

Sharing configuration across repositories

RunsOn comes with a feature that allows a local configuration file to inherit from a globally defined configuration file, by using the _extends directive.

The recommendation is to store the global configuration file in the special .github-private repository of your organization, but you can choose any other repository as well (public or private).

Example:

your-repo/.github/runs-on.yml
_extends: .github-private
.github-private/.github/runs-on.yml
runners:
cheap-arm64:
cpu: [1, 2]
family: ["t4g"]
image: ubuntu22-full-arm64