diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # CHANGELOG
 
+## Version 0.3.0.0
+
+- Changed `runsOn` from `Maybe Text` to `Maybe RunsOn` to support GitHub Actions
+  runner label lists (e.g. `runs-on: [self-hosted, linux]`), while preserving
+  the original YAML representation during round-trip serialization
+
 ## Version 0.2.0 (2025-07-21)
 
 - Changed `cancelInProgress` from `Maybe Bool` to `Maybe Text` to support GitHub Actions expressions
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@
 import Language.Github.Actions.Workflow
 import qualified Language.Github.Actions.Job as Job
 import qualified Language.Github.Actions.Job.Id as JobId
+import qualified Language.Github.Actions.Job.RunsOn as RunsOn
 import qualified Language.Github.Actions.Step as Step
 import qualified Language.Github.Actions.Workflow.Trigger as Trigger
 
@@ -38,7 +39,7 @@
 buildJob :: Job.Job
 buildJob = Job.new
   { Job.jobName = Just "Build and Test"
-  , Job.runsOn = Just "ubuntu-latest"
+  , Job.runsOn = Just (RunsOn.RunsOnString "ubuntu-latest")
   , Job.steps = Just $ checkoutStep :| [buildStep, testStep]
   }
 
diff --git a/github-actions.cabal b/github-actions.cabal
--- a/github-actions.cabal
+++ b/github-actions.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               github-actions
-version:            0.2.0.0
+version:            0.3.0.0
 synopsis:           Github Actions
 description:
   This library provides types and instances for serializing and deserializing
@@ -24,7 +24,7 @@
   test/golden/configuration-main.hs.txt
   test/golden/configuration-main.yml
 
-tested-with:        GHC ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.4
+tested-with:        GHC ==9.6.7 || ==9.8.4 || ==9.10.3 || ==9.12.4 || ==9.14.1
 
 common opts
   default-language: Haskell2010
@@ -37,7 +37,7 @@
 
 common deps
   build-depends:
-    , aeson               ^>=2.2.3.0
+    , aeson               ^>=2.3.0.0
     , base                >=4.14      && <4.23
     , containers          ^>=0.6.7    || ^>=0.7   || ^>=0.8
     , hedgehog            ^>=1.5      || ^>=1.6   || ^>=1.7
@@ -60,6 +60,7 @@
     Language.Github.Actions.Job.Environment
     Language.Github.Actions.Job.Id
     Language.Github.Actions.Job.Needs
+    Language.Github.Actions.Job.RunsOn
     Language.Github.Actions.Job.Strategy
     Language.Github.Actions.Permissions
     Language.Github.Actions.RunIf
diff --git a/src/Language/Github/Actions/Job.hs b/src/Language/Github/Actions/Job.hs
--- a/src/Language/Github/Actions/Job.hs
+++ b/src/Language/Github/Actions/Job.hs
@@ -45,6 +45,8 @@
 import qualified Language.Github.Actions.Job.Environment as JobEnvironment
 import Language.Github.Actions.Job.Needs (JobNeeds)
 import qualified Language.Github.Actions.Job.Needs as JobNeeds
+import Language.Github.Actions.Job.RunsOn (RunsOn)
+import qualified Language.Github.Actions.Job.RunsOn as RunsOn
 import Language.Github.Actions.Job.Strategy (JobStrategy)
 import qualified Language.Github.Actions.Job.Strategy as JobStrategy
 import Language.Github.Actions.Permissions (Permissions)
@@ -73,7 +75,7 @@
 -- myJob :: Job
 -- myJob = new
 --  { jobName = Just "Build and Test"
---  , runsOn = Just "ubuntu-latest"
+--  , runsOn = Just (RunsOn.RunsOnString "ubuntu-latest")
 --  , steps = Just $ Step.new :| []
 --  }
 -- @
@@ -102,8 +104,8 @@
     permissions :: Maybe Permissions,
     -- | Condition for running this job
     runIf :: Maybe RunIf,
-    -- | Runner type (e.g., "ubuntu-latest")
-    runsOn :: Maybe Text,
+    -- | Runner type (e.g., "ubuntu-latest") or a list of runner labels
+    runsOn :: Maybe RunsOn,
     -- | Secrets available to this job
     secrets :: Map Text Text,
     -- | Services to run alongside this job
@@ -185,7 +187,7 @@
   outputs <- genTextMap
   permissions <- Gen.maybe Permissions.gen
   runIf <- Gen.maybe RunIf.gen
-  runsOn <- Gen.maybe genText
+  runsOn <- Gen.maybe RunsOn.gen
   secrets <- genTextMap
   services <- Gen.map (Range.linear 1 5) $ liftA2 (,) ServiceId.gen Service.gen
   steps <- Gen.maybe (Gen.nonEmpty (Range.linear 1 20) Step.gen)
@@ -208,7 +210,7 @@
 -- @
 -- buildJob = new
 --   { jobName = Just "Build"
---   , runsOn = Just "ubuntu-latest"
+--   , runsOn = Just (RunsOn.RunsOnString "ubuntu-latest")
 --   , steps = Just $ checkoutStep :| [buildStep]
 --   }
 -- @
diff --git a/src/Language/Github/Actions/Job/RunsOn.hs b/src/Language/Github/Actions/Job/RunsOn.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Github/Actions/Job/RunsOn.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+
+-- |
+-- Module      : Language.Github.Actions.Job.RunsOn
+-- Description : Runner specification for GitHub Actions jobs
+-- Copyright   : (c) 2025 Bellroy Pty Ltd
+-- License     : BSD-3-Clause
+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>
+--
+-- This module provides the 'RunsOn' type for representing the runner
+-- configuration of a GitHub Actions job. GitHub Actions allows both strings
+-- and lists of strings for the 'runs-on' field.
+--
+-- Examples of valid 'runs-on' specifications:
+-- * @runs-on: ubuntu-latest@ - A single runner specified as a string
+-- * @runs-on: [self-hosted, linux]@ - A runner specified as a list of labels
+-- * @runs-on: [self-hosted, linux, x64]@ - A runner specified as a list of labels
+--
+-- For more information about GitHub Actions runner selection, see:
+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on>
+module Language.Github.Actions.Job.RunsOn
+  ( RunsOn (..),
+    gen,
+  )
+where
+
+import Data.Aeson (FromJSON, ToJSON (..), Value (..))
+import qualified Data.Aeson as Aeson
+import Data.List.NonEmpty (NonEmpty)
+import Data.Text (Text)
+import GHC.Generics (Generic)
+import Hedgehog (MonadGen)
+import qualified Hedgehog.Gen as Gen
+import qualified Hedgehog.Range as Range
+
+-- | Runner specification that preserves YAML representation.
+--
+-- GitHub Actions supports flexible runner specification:
+--
+-- * 'RunsOnString' - A single runner label as a string like @runs-on: ubuntu-latest@
+-- * 'RunsOnArray' - A list of runner labels like @runs-on: [self-hosted, linux]@
+--
+-- Examples:
+--
+-- @
+-- -- Single runner label (string form)
+-- stringRunner :: RunsOn
+-- stringRunner = RunsOnString "ubuntu-latest"
+--
+-- -- Multiple runner labels (array form)
+-- arrayRunner :: RunsOn
+-- arrayRunner = RunsOnArray ("self-hosted" :| ["linux", "x64"])
+-- @
+--
+-- The type preserves the original YAML format during round-trip serialization.
+-- A string input will serialize back to a string, and an array input will
+-- serialize back to an array, preventing information loss.
+data RunsOn
+  = RunsOnString Text
+  | RunsOnArray (NonEmpty Text)
+  deriving stock (Eq, Generic, Ord, Show)
+
+instance FromJSON RunsOn where
+  parseJSON v@(Array _) = RunsOnArray <$> Aeson.parseJSON v
+  parseJSON v = RunsOnString <$> Aeson.parseJSON v
+
+instance ToJSON RunsOn where
+  toJSON (RunsOnString label) = toJSON label
+  toJSON (RunsOnArray labels) = toJSON labels
+
+gen :: (MonadGen m) => m RunsOn
+gen =
+  Gen.choice
+    [ RunsOnString <$> genText,
+      RunsOnArray <$> Gen.nonEmpty (Range.linear 1 5) genText
+    ]
+  where
+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum
diff --git a/test/golden/configuration-main.hs.txt b/test/golden/configuration-main.hs.txt
--- a/test/golden/configuration-main.hs.txt
+++ b/test/golden/configuration-main.hs.txt
@@ -22,7 +22,7 @@
               , outputs = fromList []
               , permissions = Nothing
               , runIf = Nothing
-              , runsOn = Just "ubuntu-20.04"
+              , runsOn = Just (RunsOnString "ubuntu-20.04")
               , secrets = fromList []
               , services = fromList []
               , steps =
@@ -871,7 +871,7 @@
               , outputs = fromList []
               , permissions = Nothing
               , runIf = Nothing
-              , runsOn = Just "ubuntu-20.04"
+              , runsOn = Just (RunsOnString "ubuntu-20.04")
               , secrets = fromList []
               , services = fromList []
               , steps =
