From 9888fb3c8cb17c680465c5e4b6b597daa6bb4c1f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 12 Jun 2026 23:07:51 +0000
Subject: [PATCH 1/4] Initial plan


From efe51f8e2a2da44e8277a8fcea20f6b00844f5bd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 12 Jun 2026 23:28:43 +0000
Subject: [PATCH 2/4] Support runs-on-slim runner label lists

Co-authored-by: zarenner <13670625+zarenner@users.noreply.github.com>
---
 .github/aw/workflow-constraints.md            |  2 +-
 docs/public/editor/autocomplete-data.json     | 16 +++++++-
 .../src/content/docs/reference/frontmatter.md |  2 +-
 .../docs/reference/self-hosted-runners.md     |  1 +
 pkg/parser/schema_location_test.go            | 27 +++++++++++++
 pkg/parser/schemas/main_workflow_schema.json  | 15 +++++--
 .../central_slash_command_workflow.go         | 22 ++++++++++-
 .../central_slash_command_workflow_test.go    | 35 +++++++++++++++++
 pkg/workflow/compiler_types.go                |  2 +-
 pkg/workflow/frontmatter_parsing.go           |  3 ++
 pkg/workflow/frontmatter_serialization.go     |  2 +-
 pkg/workflow/frontmatter_types.go             |  2 +-
 pkg/workflow/safe_outputs_runs_on_test.go     | 39 ++++++++++++++++++-
 pkg/workflow/safe_outputs_runtime.go          |  2 +-
 pkg/workflow/workflow_builder.go              |  5 +--
 15 files changed, 157 insertions(+), 18 deletions(-)

diff --git a/.github/aw/workflow-constraints.md b/.github/aw/workflow-constraints.md
index 526a6532be1..78e409c4220 100644
--- a/.github/aw/workflow-constraints.md
+++ b/.github/aw/workflow-constraints.md
@@ -69,7 +69,7 @@ When a requested feature increases risk:
 
 When a workflow targets a self-hosted runner (any `runs-on` value other than GitHub-hosted labels such as `ubuntu-latest`, `ubuntu-slim`, `windows-latest`, or `macos-latest`), keep the generated workflow compatible with self-hosted constraints:
 
-- Set `runs-on` explicitly (it is not inherited from imports) to the runner the user's setup provides; `runs-on` accepts a string, array, or runner-group object. Framework/generated jobs (activation, safe-outputs, unlock, etc.) default to the hosted `ubuntu-slim`, so also set `runs-on-slim` to route them to the self-hosted runner, otherwise they try to run on a hosted runner. `runs-on-slim` takes a single string label, so give it a self-hosted label the runner answers to (it cannot mirror an array or object value).
+- Set `runs-on` explicitly (it is not inherited from imports) to the runner the user's setup provides; `runs-on` accepts a string, array, or runner-group object. Framework/generated jobs (activation, safe-outputs, unlock, etc.) default to the hosted `ubuntu-slim`, so also set `runs-on-slim` to route them to the self-hosted runner, otherwise they try to run on a hosted runner. `runs-on-slim` accepts the same string, array, or runner-group object forms as `runs-on`.
 - Write transient state, tool downloads, and intermediate outputs under `$RUNNER_TEMP`, not `/tmp`, which can persist across jobs on shared runners.
 - The agent job's own steps run as the runner user, not root — don't write steps that assume root (for example, installing to system-wide paths). Separately, the egress firewall needs host-level privileges (sudo) on the runner; if the host cannot provide that, the firewall can be disabled, which removes egress filtering. Surface that trade-off to the user rather than encoding it in the workflow.
 - Declare every outbound domain the workflow contacts in `network.allowed` (keep `defaults` for the core GitHub/Copilot/registry endpoints). When the egress firewall is enabled (the default once network permissions are set), any domain that is not allow-listed is blocked.
diff --git a/docs/public/editor/autocomplete-data.json b/docs/public/editor/autocomplete-data.json
index b24a883f0d1..c1b1597ca35 100644
--- a/docs/public/editor/autocomplete-data.json
+++ b/docs/public/editor/autocomplete-data.json
@@ -484,9 +484,21 @@
       "array": true
     },
     "runs-on-slim": {
-      "type": "string",
+      "type": "string|array|object",
       "desc": "Runner for all framework/generated jobs (activation, pre-activation, safe-outputs, unlock, APM, etc.).",
-      "leaf": true
+      "children": {
+        "group": {
+          "type": "string",
+          "desc": "Runner group name for self-hosted runners or GitHub-hosted runner groups",
+          "leaf": true
+        },
+        "labels": {
+          "type": "array",
+          "desc": "List of runner labels for self-hosted runners or GitHub-hosted runner selection",
+          "array": true
+        }
+      },
+      "array": true
     },
     "timeout-minutes": {
       "type": "integer|string",
diff --git a/docs/src/content/docs/reference/frontmatter.md b/docs/src/content/docs/reference/frontmatter.md
index adbb8833fd7..798347d4402 100644
--- a/docs/src/content/docs/reference/frontmatter.md
+++ b/docs/src/content/docs/reference/frontmatter.md
@@ -195,7 +195,7 @@ runs-on-slim: ubuntu-slim            # Defaults to ubuntu-slim (framework jobs o
 timeout-minutes: 30                  # Defaults to 20 minutes
 ```
 
-`runs-on` applies to the main agent job only. `runs-on-slim` applies to all framework/generated jobs (activation, safe-outputs, unlock, etc.) and defaults to `ubuntu-slim`. `safe-outputs.runs-on` takes precedence over `runs-on-slim` for safe-output jobs specifically.
+`runs-on` applies to the main agent job only. `runs-on-slim` applies to all framework/generated jobs (activation, safe-outputs, unlock, etc.), accepts the same string, array, or runner-group object forms as `runs-on`, and defaults to `ubuntu-slim`. `safe-outputs.runs-on` takes precedence over `runs-on-slim` for safe-output jobs specifically.
 
 `timeout-minutes` accepts an integer or a GitHub Actions expression string (e.g. `${{ inputs.timeout }}`), letting a reusable `workflow_call` workflow parameterize its own timeout from caller inputs. It applies to the workflow being compiled, **not** to plain caller jobs that invoke a reusable workflow with job-level `uses:` — GitHub rejects `timeout-minutes` there.
 
diff --git a/docs/src/content/docs/reference/self-hosted-runners.md b/docs/src/content/docs/reference/self-hosted-runners.md
index 8b36f9ec9ee..e84234f9878 100644
--- a/docs/src/content/docs/reference/self-hosted-runners.md
+++ b/docs/src/content/docs/reference/self-hosted-runners.md
@@ -113,6 +113,7 @@ safe-outputs:
 
 > [!NOTE]
 > `runs-on` controls only the main agent job. `runs-on-slim` controls all framework/generated jobs. `safe-outputs.runs-on` still takes precedence over `runs-on-slim` for safe-output jobs specifically.
+> `runs-on-slim` accepts the same string, array, or runner-group object forms as `runs-on`.
 
 ## Configuring the maintenance workflow runner
 
diff --git a/pkg/parser/schema_location_test.go b/pkg/parser/schema_location_test.go
index 64e67559eec..4a67cffb0ad 100644
--- a/pkg/parser/schema_location_test.go
+++ b/pkg/parser/schema_location_test.go
@@ -415,6 +415,33 @@ func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsJobRunsOnAr
 	}
 }
 
+func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsRunsOnSlimArrayForm(t *testing.T) {
+	frontmatter := map[string]any{
+		"on":           "workflow_dispatch",
+		"runs-on-slim": []any{"self-hosted", "linux"},
+	}
+
+	err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/test/workflow.md")
+	if err != nil {
+		t.Fatalf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() unexpected error = %v", err)
+	}
+}
+
+func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsRunsOnSlimObjectForm(t *testing.T) {
+	frontmatter := map[string]any{
+		"on": "workflow_dispatch",
+		"runs-on-slim": map[string]any{
+			"group":  "arc-custom",
+			"labels": []any{"ubuntu2404", "x64"},
+		},
+	}
+
+	err := ValidateMainWorkflowFrontmatterWithSchemaAndLocation(frontmatter, "/test/workflow.md")
+	if err != nil {
+		t.Fatalf("ValidateMainWorkflowFrontmatterWithSchemaAndLocation() unexpected error = %v", err)
+	}
+}
+
 func TestValidateMainWorkflowFrontmatterWithSchemaAndLocation_AcceptsAllowedBaseBranchesInCreatePullRequest(t *testing.T) {
 	frontmatter := map[string]any{
 		"on": map[string]any{
diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json
index 60ad3a3d2a9..623352e85c1 100644
--- a/pkg/parser/schemas/main_workflow_schema.json
+++ b/pkg/parser/schemas/main_workflow_schema.json
@@ -2634,9 +2634,18 @@
       ]
     },
     "runs-on-slim": {
-      "type": "string",
-      "description": "Runner for all framework/generated jobs (activation, pre-activation, safe-outputs, unlock, APM, etc.). Provides a compile-stable override for generated job runners without requiring a safe-outputs section. Overridden by safe-outputs.runs-on when both are set. Defaults to 'ubuntu-slim'. Use this when your infrastructure does not provide the default runner or when you need consistent runner selection across all jobs.",
-      "examples": ["self-hosted", "ubuntu-latest", "ubuntu-22.04"]
+      "$ref": "#/$defs/github_actions_runs_on",
+      "description": "Runner for all framework/generated jobs (activation, pre-activation, safe-outputs, unlock, APM, etc.). Provides a compile-stable override for generated job runners without requiring a safe-outputs section. Supports the same string, array, and runner-group object forms as runs-on. Overridden by safe-outputs.runs-on when both are set. Defaults to 'ubuntu-slim'. Use this when your infrastructure does not provide the default runner or when you need consistent runner selection across all jobs.",
+      "examples": [
+        "self-hosted",
+        "ubuntu-latest",
+        "ubuntu-22.04",
+        ["self-hosted", "ubuntu2404", "x64", "host"],
+        {
+          "group": "larger-runners",
+          "labels": ["ubuntu-latest-8-cores"]
+        }
+      ]
     },
     "timeout-minutes": {
       "$ref": "#/$defs/templatable_integer",
diff --git a/pkg/workflow/central_slash_command_workflow.go b/pkg/workflow/central_slash_command_workflow.go
index c2061425f8e..f0f74e251b1 100644
--- a/pkg/workflow/central_slash_command_workflow.go
+++ b/pkg/workflow/central_slash_command_workflow.go
@@ -487,7 +487,7 @@ func resolveCentralSlashRunsOn(workflowDataList []*WorkflowData) string {
 		if wd.SafeOutputs != nil && strings.TrimSpace(wd.SafeOutputs.RunsOn) != "" {
 			resolved = strings.TrimSpace(wd.SafeOutputs.RunsOn)
 		} else if strings.TrimSpace(wd.RunsOnSlim) != "" {
-			resolved = strings.TrimSpace(wd.RunsOnSlim)
+			resolved = formatRunsOnSnippetForInlineValue(wd.RunsOnSlim)
 		}
 		counts[resolved]++
 	}
@@ -503,6 +503,26 @@ func resolveCentralSlashRunsOn(workflowDataList []*WorkflowData) string {
 	return best
 }
 
+func formatRunsOnSnippetForInlineValue(runsOn string) string {
+	runsOn = strings.TrimSpace(runsOn)
+	if !strings.HasPrefix(runsOn, "runs-on:") {
+		return runsOn
+	}
+
+	value := strings.TrimPrefix(runsOn, "runs-on:")
+	if !strings.HasPrefix(value, "\n") {
+		return strings.TrimSpace(value)
+	}
+
+	value = strings.TrimPrefix(value, "\n")
+	lines := strings.Split(value, "\n")
+	for i, line := range lines {
+		line = strings.TrimPrefix(line, "  ")
+		lines[i] = "      " + line
+	}
+	return "\n" + strings.Join(lines, "\n")
+}
+
 func writeCentralSlashEventsYAML(b *strings.Builder, mergedEvents map[string]map[string]bool) {
 	eventOrder := []string{
 		"issues",
diff --git a/pkg/workflow/central_slash_command_workflow_test.go b/pkg/workflow/central_slash_command_workflow_test.go
index e82dc56ff42..78969d2bb00 100644
--- a/pkg/workflow/central_slash_command_workflow_test.go
+++ b/pkg/workflow/central_slash_command_workflow_test.go
@@ -327,6 +327,41 @@ func TestGenerateCentralSlashCommandWorkflow_UsesCentralizedRunsOnResolution(t *
 	require.Contains(t, string(content), "runs-on: self-hosted")
 }
 
+func TestFormatRunsOnSnippetForInlineValue(t *testing.T) {
+	tests := []struct {
+		name   string
+		runsOn string
+		want   string
+	}{
+		{
+			name:   "plain label",
+			runsOn: "ubuntu-latest",
+			want:   "ubuntu-latest",
+		},
+		{
+			name:   "rendered string snippet",
+			runsOn: "runs-on: self-hosted",
+			want:   "self-hosted",
+		},
+		{
+			name:   "rendered array snippet",
+			runsOn: "runs-on:\n- self-hosted\n- linux",
+			want:   "\n      - self-hosted\n      - linux",
+		},
+		{
+			name:   "rendered object snippet",
+			runsOn: "runs-on:\n  group: runner-group\n  labels:\n  - linux",
+			want:   "\n      group: runner-group\n      labels:\n      - linux",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			require.Equal(t, tt.want, formatRunsOnSnippetForInlineValue(tt.runsOn))
+		})
+	}
+}
+
 func TestBuildCommandsHeaderMetadata_UsesReleaseVersionOnlyForReleaseBuilds(t *testing.T) {
 	originalVersion := compilerVersion
 	originalIsRelease := isReleaseBuild
diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go
index 3594f48c26c..774c130e6b4 100644
--- a/pkg/workflow/compiler_types.go
+++ b/pkg/workflow/compiler_types.go
@@ -488,7 +488,7 @@ type WorkflowData struct {
 	PreAgentSteps                  string // steps to run immediately before the agent execution step
 	PostSteps                      string // steps to run after AI execution
 	RunsOn                         string
-	RunsOnSlim                     string // runner override for all framework/generated jobs (activation, safe-outputs, unlock, etc.)
+	RunsOnSlim                     string // rendered runs-on snippet for framework/generated jobs (activation, safe-outputs, unlock, etc.)
 	Environment                    string // environment setting for the main job
 	Container                      string // container setting for the main job
 	Services                       string // services setting for the main job
diff --git a/pkg/workflow/frontmatter_parsing.go b/pkg/workflow/frontmatter_parsing.go
index 95a4663b79e..ba49622afeb 100644
--- a/pkg/workflow/frontmatter_parsing.go
+++ b/pkg/workflow/frontmatter_parsing.go
@@ -31,6 +31,9 @@ func ParseFrontmatterConfig(frontmatter map[string]any) (*FrontmatterConfig, err
 	if err := validateRunsOnValue(config.RunsOn); err != nil {
 		return nil, err
 	}
+	if err := validateRunsOnValue(config.RunsOnSlim); err != nil {
+		return nil, err
+	}
 
 	// Parse typed Runtimes field if runtimes exist
 	if len(config.Runtimes) > 0 {
diff --git a/pkg/workflow/frontmatter_serialization.go b/pkg/workflow/frontmatter_serialization.go
index 2b7c13a1d1d..cf7129b9365 100644
--- a/pkg/workflow/frontmatter_serialization.go
+++ b/pkg/workflow/frontmatter_serialization.go
@@ -182,7 +182,7 @@ func (fc *FrontmatterConfig) ToMap() map[string]any {
 	if !isNilValue(fc.RunsOn) {
 		result["runs-on"] = fc.RunsOn
 	}
-	if fc.RunsOnSlim != "" {
+	if !isNilValue(fc.RunsOnSlim) {
 		result["runs-on-slim"] = fc.RunsOnSlim
 	}
 	if fc.RunName != "" {
diff --git a/pkg/workflow/frontmatter_types.go b/pkg/workflow/frontmatter_types.go
index eca08ea0490..5fb9d4e4d74 100644
--- a/pkg/workflow/frontmatter_types.go
+++ b/pkg/workflow/frontmatter_types.go
@@ -327,7 +327,7 @@ type FrontmatterConfig struct {
 
 	// Workflow execution settings
 	RunsOn        any            `json:"runs-on,omitempty"`      // Supports string, array, or object GitHub Actions runner forms
-	RunsOnSlim    string         `json:"runs-on-slim,omitempty"` // Runner for all framework/generated jobs (activation, safe-outputs, unlock, etc.)
+	RunsOnSlim    any            `json:"runs-on-slim,omitempty"` // Runner for all framework/generated jobs; supports the same forms as runs-on
 	RunName       string         `json:"run-name,omitempty"`
 	PreSteps      []any          `json:"pre-steps,omitempty"`       // Pre-workflow steps (run before checkout)
 	Steps         []any          `json:"steps,omitempty"`           // Custom workflow steps
diff --git a/pkg/workflow/safe_outputs_runs_on_test.go b/pkg/workflow/safe_outputs_runs_on_test.go
index 542651028e0..54fe52a7dc4 100644
--- a/pkg/workflow/safe_outputs_runs_on_test.go
+++ b/pkg/workflow/safe_outputs_runs_on_test.go
@@ -258,6 +258,34 @@ This is a test workflow.`,
 			expectedRunsOn:   "runs-on: self-hosted",
 			checkJobPatterns: []string{"\n  activation:", "\n  safe_outputs:"},
 		},
+		{
+			name: "runs-on-slim supports array labels",
+			frontmatter: `---
+on: push
+runs-on-slim: [self-hosted, ubuntu2404, x64, host]
+---
+
+# Test Workflow
+
+This is a test workflow.`,
+			expectedRunsOn:   "runs-on:\n    - self-hosted\n    - ubuntu2404\n    - x64\n    - host",
+			checkJobPatterns: []string{"\n  activation:"},
+		},
+		{
+			name: "runs-on-slim supports group and labels object",
+			frontmatter: `---
+on: push
+runs-on-slim:
+  group: runner-group
+  labels: [ubuntu2404, x64]
+---
+
+# Test Workflow
+
+This is a test workflow.`,
+			expectedRunsOn:   "runs-on:\n      group: runner-group\n      labels:\n      - ubuntu2404\n      - x64",
+			checkJobPatterns: []string{"\n  activation:"},
+		},
 		{
 			name: "default used when neither runs-on-slim nor safe-outputs.runs-on is set",
 			frontmatter: `---
@@ -330,14 +358,14 @@ func TestFormatFrameworkJobRunsOn(t *testing.T) {
 		{
 			name: "runs-on-slim used when safe-outputs.runs-on is empty",
 			data: &WorkflowData{
-				RunsOnSlim: "self-hosted",
+				RunsOnSlim: "runs-on: self-hosted",
 			},
 			expectedRunsOn: "runs-on: self-hosted",
 		},
 		{
 			name: "safe-outputs.runs-on takes precedence over runs-on-slim",
 			data: &WorkflowData{
-				RunsOnSlim:  "ubuntu-22.04",
+				RunsOnSlim:  "runs-on: ubuntu-22.04",
 				SafeOutputs: &SafeOutputsConfig{RunsOn: "self-hosted"},
 			},
 			expectedRunsOn: "runs-on: self-hosted",
@@ -357,6 +385,13 @@ func TestFormatFrameworkJobRunsOn(t *testing.T) {
 			},
 			expectedRunsOn: "runs-on: " + constants.DefaultActivationJobRunnerImage,
 		},
+		{
+			name: "runs-on-slim array snippet returned as-is",
+			data: &WorkflowData{
+				RunsOnSlim: "runs-on:\n- self-hosted\n- ubuntu2404",
+			},
+			expectedRunsOn: "runs-on:\n    - self-hosted\n    - ubuntu2404",
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/pkg/workflow/safe_outputs_runtime.go b/pkg/workflow/safe_outputs_runtime.go
index 04155866f94..d28d1120951 100644
--- a/pkg/workflow/safe_outputs_runtime.go
+++ b/pkg/workflow/safe_outputs_runtime.go
@@ -29,7 +29,7 @@ func (c *Compiler) formatFrameworkJobRunsOn(data *WorkflowData) string {
 	}
 	if data != nil && data.RunsOnSlim != "" {
 		safeOutputsRuntimeLog.Printf("Framework job runs-on from runs-on-slim: %s", data.RunsOnSlim)
-		return "runs-on: " + data.RunsOnSlim
+		return c.indentYAMLLines(data.RunsOnSlim, "    ")
 	}
 	safeOutputsRuntimeLog.Printf("Framework job runs-on using default: %s", constants.DefaultActivationJobRunnerImage)
 	return "runs-on: " + constants.DefaultActivationJobRunnerImage
diff --git a/pkg/workflow/workflow_builder.go b/pkg/workflow/workflow_builder.go
index 2d789151e13..4b293805297 100644
--- a/pkg/workflow/workflow_builder.go
+++ b/pkg/workflow/workflow_builder.go
@@ -277,11 +277,8 @@ func (c *Compiler) extractYAMLSections(frontmatter map[string]any, workflowData
 	workflowData.TimeoutMinutes = c.extractTopLevelYAMLSection(frontmatter, "timeout-minutes")
 
 	workflowData.RunsOn = c.extractTopLevelYAMLSection(frontmatter, "runs-on")
-	// Extract runs-on-slim as a plain string (no YAML formatting needed)
 	if v, ok := frontmatter["runs-on-slim"]; ok {
-		if s, ok := v.(string); ok {
-			workflowData.RunsOnSlim = s
-		}
+		workflowData.RunsOnSlim = c.extractTopLevelYAMLSection(map[string]any{"runs-on": v}, "runs-on")
 	}
 	workflowData.Environment = c.extractTopLevelYAMLSection(frontmatter, "environment")
 	workflowData.Container = c.extractTopLevelYAMLSection(frontmatter, "container")

From 63491a48fa1bcbde97fc3a22f58f851094caa9ec Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
 <41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 13 Jun 2026 10:25:36 +0000
Subject: [PATCH 3/4] Add draft ADR-38965 for runs-on-slim schema reuse

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 ...5-reuse-runs-on-schema-for-runs-on-slim.md | 39 +++++++++++++++++++
 1 file changed, 39 insertions(+)
 create mode 100644 docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md

diff --git a/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md b/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md
new file mode 100644
index 00000000000..0908661b4ea
--- /dev/null
+++ b/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md
@@ -0,0 +1,39 @@
+# ADR-38965: Reuse the runs-on schema and rendering pipeline for runs-on-slim
+
+**Date**: 2026-06-13
+**Status**: Draft
+
+## Context
+
+`runs-on-slim` selects the runner for all framework/generated jobs (activation, safe-outputs, unlock, APM, etc.), while `runs-on` selects the runner for the main agent job. `runs-on` already accepts the full set of GitHub Actions runner forms — a plain string label, an array of labels, or a `{ group, labels }` runner-group object — but `runs-on-slim` was schema-validated and parsed as a string only. Self-hosted users who select runners by label array or runner group therefore could not route framework jobs to the same runner they use for `runs-on`, and any such value failed to compile. The two fields configure the same concept (runner selection) and should accept the same syntax.
+
+## Decision
+
+We will treat `runs-on-slim` as the same kind of value as `runs-on` rather than as a distinct string field. Concretely: the JSON schema entry for `runs-on-slim` now `$ref`s the shared `#/$defs/github_actions_runs_on` definition; the in-memory `FrontmatterConfig.RunsOnSlim` field changes from `string` to `any` and is validated through the existing `validateRunsOnValue`; and `WorkflowData.RunsOnSlim` holds a **rendered `runs-on:` YAML snippet** (produced by the same extraction path as `runs-on`) instead of a bare label. Downstream consumers re-indent that snippet for the framework job context via helpers (`formatRunsOnSnippetForInlineValue`, `indentYAMLLines`). This guarantees parity with `runs-on` and avoids a second, drift-prone validation/rendering path.
+
+## Alternatives Considered
+
+### Alternative 1: Keep `runs-on-slim` as a string and document the limitation
+Leave the type as `string` and tell users that `runs-on-slim` cannot mirror an array or runner-group value. Rejected because it permanently blocks legitimate self-hosted configurations and forces an inconsistent mental model where two runner-selection fields accept different syntax.
+
+### Alternative 2: Add a separate schema and parser for `runs-on-slim`'s array/object forms
+Duplicate the array/runner-group validation and YAML-rendering logic specifically for `runs-on-slim`. Rejected because it duplicates non-trivial logic already maintained for `runs-on`, inviting divergence over time as one path gains features or fixes the other misses.
+
+## Consequences
+
+### Positive
+- `runs-on-slim` reaches full parity with `runs-on`, accepting string, label-array, and `{ group, labels }` forms.
+- Validation and rendering reuse the existing shared `runs-on` schema and code paths, so future changes apply to both fields automatically.
+- Self-hosted setups that select runners by label array or runner group can now route framework jobs correctly.
+
+### Negative
+- The internal contract of `RunsOnSlim` changes: `FrontmatterConfig.RunsOnSlim` becomes `any` and `WorkflowData.RunsOnSlim` now carries a rendered `runs-on:` snippet rather than a bare label, requiring every consumer (serialization, framework-job formatting, central slash-command resolution) to be updated and re-tested.
+- Indentation handling adds complexity: snippets must be re-indented for differing YAML contexts, introducing helper functions whose correctness depends on the exact upstream rendering format.
+
+### Neutral
+- Existing tests were updated to expect rendered `runs-on:` snippets instead of bare labels, and new tests cover the array and runner-group forms.
+- Reference docs, self-hosted-runner guidance, workflow constraints, and editor autocomplete metadata were updated to describe the expanded syntax.
+
+---
+
+*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/27456719584) workflow. The PR author must review, complete, and finalize this document before the PR can merge.*

From 277b0d83f6b80dbed0cccb71640ef3ac9df32dee Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 13 Jun 2026 11:02:58 +0000
Subject: [PATCH 4/4] Fix runs-on-slim empty handling and reviewer follow-ups

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
 ...5-reuse-runs-on-schema-for-runs-on-slim.md |  4 +-
 .../central_slash_command_workflow.go         |  3 ++
 .../central_slash_command_workflow_test.go    |  2 +-
 .../compiler_orchestrator_workflow_test.go    | 39 +++++++++++++++++++
 pkg/workflow/frontmatter_serialization.go     |  2 +-
 pkg/workflow/frontmatter_types_test.go        | 10 +++++
 pkg/workflow/runs_on_validation.go            | 24 ++++++++++++
 pkg/workflow/safe_outputs_runs_on_test.go     |  2 +-
 pkg/workflow/workflow_builder.go              |  2 +-
 9 files changed, 81 insertions(+), 7 deletions(-)

diff --git a/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md b/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md
index 0908661b4ea..2d4e67ad98b 100644
--- a/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md
+++ b/docs/adr/38965-reuse-runs-on-schema-for-runs-on-slim.md
@@ -1,7 +1,7 @@
 # ADR-38965: Reuse the runs-on schema and rendering pipeline for runs-on-slim
 
 **Date**: 2026-06-13
-**Status**: Draft
+**Status**: Accepted
 
 ## Context
 
@@ -35,5 +35,3 @@ Duplicate the array/runner-group validation and YAML-rendering logic specificall
 - Reference docs, self-hosted-runner guidance, workflow constraints, and editor autocomplete metadata were updated to describe the expanded syntax.
 
 ---
-
-*This is a DRAFT ADR generated by the [Design Decision Gate](https://github.com/github/gh-aw/actions/runs/27456719584) workflow. The PR author must review, complete, and finalize this document before the PR can merge.*
diff --git a/pkg/workflow/central_slash_command_workflow.go b/pkg/workflow/central_slash_command_workflow.go
index f0f74e251b1..028c95f4ff8 100644
--- a/pkg/workflow/central_slash_command_workflow.go
+++ b/pkg/workflow/central_slash_command_workflow.go
@@ -517,6 +517,9 @@ func formatRunsOnSnippetForInlineValue(runsOn string) string {
 	value = strings.TrimPrefix(value, "\n")
 	lines := strings.Split(value, "\n")
 	for i, line := range lines {
+		// The 2-space strip matches DefaultMarshalOptions map indentation.
+		// The 6-space re-indent aligns with the central slash command template,
+		// where runs-on: lives at 4-space job-level indent (4 + 2 = 6).
 		line = strings.TrimPrefix(line, "  ")
 		lines[i] = "      " + line
 	}
diff --git a/pkg/workflow/central_slash_command_workflow_test.go b/pkg/workflow/central_slash_command_workflow_test.go
index 78969d2bb00..ae97640ee85 100644
--- a/pkg/workflow/central_slash_command_workflow_test.go
+++ b/pkg/workflow/central_slash_command_workflow_test.go
@@ -299,7 +299,7 @@ func TestGenerateCentralSlashCommandWorkflow_UsesCentralizedRunsOnResolution(t *
 			Command:            []string{"one"},
 			CommandEvents:      []string{"issue_comment"},
 			CommandCentralized: true,
-			RunsOnSlim:         "ubuntu-latest",
+			RunsOnSlim:         "runs-on: ubuntu-latest",
 		},
 		{
 			WorkflowID:         "two",
diff --git a/pkg/workflow/compiler_orchestrator_workflow_test.go b/pkg/workflow/compiler_orchestrator_workflow_test.go
index e9a118af17e..6d7c4f7328e 100644
--- a/pkg/workflow/compiler_orchestrator_workflow_test.go
+++ b/pkg/workflow/compiler_orchestrator_workflow_test.go
@@ -212,6 +212,45 @@ func TestExtractYAMLSections_MissingSections(t *testing.T) {
 	assert.Empty(t, workflowData.Cache)
 }
 
+func TestExtractYAMLSections_EmptyRunsOnSlimTreatedAsUnset(t *testing.T) {
+	compiler := NewCompiler()
+
+	tests := []struct {
+		name  string
+		value any
+	}{
+		{
+			name:  "empty string",
+			value: "",
+		},
+		{
+			name:  "empty array",
+			value: []any{},
+		},
+		{
+			name:  "empty object",
+			value: map[string]any{},
+		},
+		{
+			name:  "object with empty group and labels",
+			value: map[string]any{"group": "", "labels": []any{}},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			workflowData := &WorkflowData{}
+			frontmatter := map[string]any{
+				"runs-on-slim": tt.value,
+			}
+
+			compiler.extractYAMLSections(frontmatter, workflowData)
+
+			assert.Empty(t, workflowData.RunsOnSlim)
+		})
+	}
+}
+
 func TestValidateWorkflowEngineSettings_PreservesLegacyErrorOrder(t *testing.T) {
 	compiler := NewCompiler()
 	compiler.strictMode = true
diff --git a/pkg/workflow/frontmatter_serialization.go b/pkg/workflow/frontmatter_serialization.go
index cf7129b9365..7996eb3a7c3 100644
--- a/pkg/workflow/frontmatter_serialization.go
+++ b/pkg/workflow/frontmatter_serialization.go
@@ -182,7 +182,7 @@ func (fc *FrontmatterConfig) ToMap() map[string]any {
 	if !isNilValue(fc.RunsOn) {
 		result["runs-on"] = fc.RunsOn
 	}
-	if !isNilValue(fc.RunsOnSlim) {
+	if !isEmptyRunsOnValue(fc.RunsOnSlim) {
 		result["runs-on-slim"] = fc.RunsOnSlim
 	}
 	if fc.RunName != "" {
diff --git a/pkg/workflow/frontmatter_types_test.go b/pkg/workflow/frontmatter_types_test.go
index 833229fe17a..efcee762cff 100644
--- a/pkg/workflow/frontmatter_types_test.go
+++ b/pkg/workflow/frontmatter_types_test.go
@@ -1075,6 +1075,16 @@ func TestFrontmatterConfigIntegration(t *testing.T) {
 		assert.Equal(t, true, networkMap["allowed-input"], "allowed-input should be preserved")
 		assert.Equal(t, []string{"defaults"}, networkMap["allowed"], "allowed list should be preserved")
 	})
+
+	t.Run("ToMap omits empty runs-on-slim", func(t *testing.T) {
+		config := &FrontmatterConfig{
+			RunsOnSlim: "",
+		}
+
+		reconstructed := config.ToMap()
+		_, ok := reconstructed["runs-on-slim"]
+		assert.False(t, ok, "empty runs-on-slim should be omitted")
+	})
 }
 
 // TestRuntimesConfigTyping tests the new typed RuntimesConfig field
diff --git a/pkg/workflow/runs_on_validation.go b/pkg/workflow/runs_on_validation.go
index eb388d1b450..9af09569cf0 100644
--- a/pkg/workflow/runs_on_validation.go
+++ b/pkg/workflow/runs_on_validation.go
@@ -107,6 +107,30 @@ func validateRunsOnValue(value any) error {
 	}
 }
 
+func isEmptyRunsOnValue(value any) bool {
+	switch v := value.(type) {
+	case nil:
+		return true
+	case string:
+		return strings.TrimSpace(v) == ""
+	case []any:
+		return len(v) == 0
+	case map[string]any:
+		if len(v) == 0 {
+			return true
+		}
+
+		group, hasGroup := v["group"].(string)
+		labels, hasLabels := v["labels"].([]any)
+		if !hasGroup && !hasLabels {
+			return false
+		}
+		return strings.TrimSpace(group) == "" && len(labels) == 0
+	default:
+		return false
+	}
+}
+
 // extractRunnerLabels extracts individual runner label strings from a runs-on value.
 // Handles all supported GitHub Actions runs-on forms:
 //   - string: "ubuntu-latest"
diff --git a/pkg/workflow/safe_outputs_runs_on_test.go b/pkg/workflow/safe_outputs_runs_on_test.go
index 54fe52a7dc4..964f0278060 100644
--- a/pkg/workflow/safe_outputs_runs_on_test.go
+++ b/pkg/workflow/safe_outputs_runs_on_test.go
@@ -386,7 +386,7 @@ func TestFormatFrameworkJobRunsOn(t *testing.T) {
 			expectedRunsOn: "runs-on: " + constants.DefaultActivationJobRunnerImage,
 		},
 		{
-			name: "runs-on-slim array snippet returned as-is",
+			name: "runs-on-slim array snippet indents continuation lines by 4 spaces",
 			data: &WorkflowData{
 				RunsOnSlim: "runs-on:\n- self-hosted\n- ubuntu2404",
 			},
diff --git a/pkg/workflow/workflow_builder.go b/pkg/workflow/workflow_builder.go
index 4b293805297..86bd89b2905 100644
--- a/pkg/workflow/workflow_builder.go
+++ b/pkg/workflow/workflow_builder.go
@@ -277,7 +277,7 @@ func (c *Compiler) extractYAMLSections(frontmatter map[string]any, workflowData
 	workflowData.TimeoutMinutes = c.extractTopLevelYAMLSection(frontmatter, "timeout-minutes")
 
 	workflowData.RunsOn = c.extractTopLevelYAMLSection(frontmatter, "runs-on")
-	if v, ok := frontmatter["runs-on-slim"]; ok {
+	if v, ok := frontmatter["runs-on-slim"]; ok && !isEmptyRunsOnValue(v) {
 		workflowData.RunsOnSlim = c.extractTopLevelYAMLSection(map[string]any{"runs-on": v}, "runs-on")
 	}
 	workflowData.Environment = c.extractTopLevelYAMLSection(frontmatter, "environment")
