GitHub Agentic Workflows

A/B Experiments

Use the experiments frontmatter section to compare workflow variants across repeated runs. Each experiment declares a name and a set of variants. On every run, the activation job picks one variant and exposes it to the prompt.

Experiments work best when you test one workflow choice at a time, such as:

  • prompt wording
  • model selection
  • whether to delegate to a sub-agent
  • which subskill (inline skill) to invoke

Add an experiments map to the workflow frontmatter. Each key names an experiment. The value is either a simple array of variants (bare-array form) or a rich object with additional metadata fields.

---
on:
issues:
types: [opened]
engine: copilot
experiments:
style: [concise, detailed]
---
Summarize this issue in a **${{ experiments.style }}** way.

Use the object form when you want built-in reporting and experiment metadata:

---
on:
schedule: daily on weekdays
engine: copilot
evals:
- id: focused
question: Is the change focused and actionable?
experiments:
prompt_style:
variants: [concise, detailed]
description: "Test whether a concise prompt reduces cost without quality loss"
hypothesis: "H0: no change in aic. H1: concise reduces AIC by >=15%"
metric: eval:focused
secondary_metrics: [duration_ms, discussion_word_count]
guardrail_metrics:
- name: success_rate
threshold: ">=0.95"
- name: empty_output_rate
direction: min
threshold: 0.0
weight: [50, 50]
min_samples: 25
start_date: "2026-05-05"
end_date: "2026-07-25"
issue: 1234
---
Summarize the findings in a **${{ experiments.prompt_style }}** way.

When evals are configured, metric can reference an eval question ID using eval:<id> (for example eval:focused) or evals.<id>.

When graders are configured, metric can reference a grader result using grader:<id> (for example grader:tool-success-rate) or graders.<id>.value.

gh aw experiments analyze <workflow> resolves the referenced eval question and, when eval result data is available, shows YES/NO/UNKNOWN totals for that eval-backed metric. It also joins per-run eval answers to persisted experiment assignments and analyzes the valid YES/NO observations by variant using the same statistical comparisons available for grader-backed metrics. For a grader-backed primary metric, it joins persisted experiment assignments to completed run artifacts and analyzes the valid grader values by variant.

Reference a variant with ${{ experiments.<name> }}. At runtime, gh-aw replaces the expression with the selected variant string, such as concise.

Use the {{#if experiments.<name> }} block syntax for conditional prompt sections. A variant value of no is treated as falsy, which makes yes/no experiments easy to express:

---
experiments:
caveman: [yes, no]
---
{{#if experiments.caveman }}
Talk like a caveman in all your responses. Me test. You run.
{{/if}}
Address the issue described above.

Most experiments compare a single decision in the workflow. The examples below show common patterns.

---
experiments:
style: [concise, detailed]
---
Summarize this issue in a **${{ experiments.style }}** way.

Model experiments are useful when you want to compare speed, cost, and output quality. gh-aw model aliases such as small and large are often a good place to start. See Model Aliases.

---
engine:
id: copilot
model: ${{ experiments.model }}
experiments:
model: [small, large]
---
Review the issue and recommend the next action.

This pattern compares a direct prompt with a delegated sub-agent flow.

---
experiments:
use_summarizer: [yes, no]
---
{{#if experiments.use_summarizer }}
Use the `file-summarizer` sub-agent to summarize `README.md`, then continue.
{{/if}}
Write a short project overview for maintainers.
## agent: `file-summarizer`
---
model: small
description: Summarizes a file in a few sentences
---
Read the given file and return a concise summary.

See Inline Sub-Agents for the full syntax.

This pattern compares two reusable instruction blocks, sometimes called subskills, without changing the main workflow prompt.

---
experiments:
triage_skill: [triage-fast, triage-deep]
---
Use the `${{ experiments.triage_skill }}` skill to classify this issue.
## skill: `triage-fast`
---
description: Fast issue triage
---
Classify the issue and suggest the smallest next step.
## skill: `triage-deep`
---
description: Detailed issue triage
---
Classify the issue, identify missing context, and recommend a fuller follow-up
plan.

The activation job tracks how often each variant has been selected. The counter is stored using the storage setting in the experiments: block. By default, gh-aw chooses the least-used variant on each run. If multiple variants are tied, including on the first run, one of them is chosen at random. Over time, this keeps usage roughly balanced across variants.

When you provide a weight array, gh-aw uses weighted random selection instead of least-used selection. For example, [70, 30] gives the first variant a 70% selection probability. If start_date or end_date is set and the current date falls outside that range, gh-aw returns the control variant (the first entry) without incrementing any counter.

The storage key inside the experiments: map controls where experiment state is persisted:

experiments:
storage: repo # or: cache (default: repo)
prompt_style: [concise, detailed]
ValueBehavior
repo (default)Commits state to a git branch named experiments/{sanitizedWorkflowID} (workflow ID lowercased with hyphens removed, e.g. my-workflowexperiments/myworkflow). Durable — survives cache evictions. Requires contents: write permission (added automatically by the compiler).
cacheUses GitHub Actions cache (legacy). State may be evicted after 7 days of inactivity.

When storage: repo, the compiler adds a push_experiments_state job after the activation job and commits the updated state.json to the experiments branch.

Each experiment exposes its selected variant as an activation job output:

ExpressionDescription
needs.activation.outputs.<name>Selected variant for experiment <name>
needs.activation.outputs.experimentsAll assignments as a JSON object

Use these expressions in downstream jobs defined in the jobs: frontmatter section.

The activation job uploads the counter state as an experiment artifact. Download and inspect it with the gh aw CLI:

Terminal window
# Download the experiment artifact for a specific run
gh aw audit <run-id> --artifacts experiment
# Display experiment assignments in the audit report
gh aw audit <run-id>

The A/B Experiments section of the audit report shows the variant chosen on the most recent run and the cumulative counts across all runs:

A/B Experiments
• caveman = yes (cumulative: no:4, yes:5)
• style = concise (cumulative: concise:5, detailed:4)

Use a deterministic grader as the primary outcome metric:

---
graders:
trajectory-efficiency:
direction: higher_is_better
tool-failure-count:
direction: lower_is_better
experiments:
prompt_v2:
variants: [control, candidate]
metric: grader:trajectory-efficiency
guardrail_metrics:
- name: grader:tool-failure-count
threshold: "<=0"
min_samples: 20
analysis_type: mann_whitney
decision:
minimum_effect: 0.05
confidence: 0.95
---
Follow the **${{ experiments.prompt_v2 }}** instructions.

After assigned runs complete, analyze their grader artifacts:

Terminal window
gh aw experiments analyze <workflow>

The assignment ledger remains the source of variant attribution. The command downloads each assigned run’s unified agent artifact on demand, reads grader_results.json, and reports assigned, usable, and excluded observations per variant. Missing artifacts, missing graders, failed graders, and invalid values are excluded rather than counted as zero. Only usable observations count toward min_samples.

This analysis does not replay traces or invoke an evaluator model. A deterministic grader measures only the behavior encoded by that grader and is not a universal correctness metric. The decision layer treats grader observations like any other resolved numeric metric.

Use a BinEval question as the primary outcome metric:

---
evals:
questions:
- id: focused
question: Does the response stay on topic?
experiments:
prompt_v2:
variants: [control, candidate]
metric: eval:focused
min_samples: 20
---
Follow the **${{ experiments.prompt_v2 }}** instructions.

gh aw experiments analyze <workflow> joins each assigned run’s recorded eval answer (from evals.jsonl) to the assignment ledger by run ID and reports assigned, usable, and excluded YES/NO observations per variant. Runs missing an eval answer, or with an UNKNOWN/unrecognized answer, are excluded rather than counted as zero. Only usable observations count toward min_samples, and the same t-test, Mann–Whitney, proportion, or Bayesian comparisons available for grader-backed metrics are applied to the YES/NO outcomes. UNKNOWN answers remain excluded. They are not treated as NO.

gh aw experiments analyze <workflow> keeps each experiment stage distinct:

StageQuestion answered
AssignmentWhich variant ran?
ObservationWhat happened during the assigned run?
AnalysisWhat treatment effect and evidence were estimated?
ReadinessAre there enough usable observations for normal analysis?
DecisionShould collection extend, or should the candidate be promoted, rejected, or remain inconclusive?
PromotionWhat future automation changes the workflow or traffic?

The command performs the stages through decision. It does not promote a variant, edit the workflow, or change traffic. readiness is COLLECTING or READY; the legacy recommendation field remains EXTEND or READY_FOR_ANALYSIS for compatibility. A ready experiment can still have an INCONCLUSIVE decision.

The complete flow is:

variant assignment
→ run observation (for example, a grader value)
→ statistical analysis
→ readiness
→ deterministic decision

Assignment identifies the treatment (control or candidate); it is not an outcome. Observation records what happened during that assigned run. Analysis estimates the effect and evidence. Readiness only answers whether every variant has reached min_samples: usable primary-metric observations for observation-backed metrics, or assignment counts when no supported observation source is resolved. The decision then applies policy and mandatory guardrails to the existing analysis.

For two-variant experiments, the command emits one of these decisions:

DecisionMeaning
EXTENDMore valid primary or guardrail observations are required, or analysis is not yet computable.
PROMOTEThe candidate has sufficient statistical evidence, exceeds the practical-effect threshold, and passes all guardrails.
REJECTThe candidate materially regresses or fails a mandatory guardrail.
INCONCLUSIVEMinimum samples exist, but the evidence or practical effect does not establish a winner.

In particular, EXTEND means that evidence collection or computation is incomplete. INCONCLUSIVE means that the engine could adjudicate the available evidence, but it did not establish a decision-quality improvement or regression. A candidate that fails to prove an improvement is not automatically harmful; REJECT requires a failed guardrail or sufficient evidence of a material regression.

The decision configuration uses absolute primary-metric units. minimum_effect defaults to 0. regression_tolerance defaults to minimum_effect. confidence defaults to 0.95. Frequentist methods require p <= 1-confidence; bayesian_ab uses the analyzer’s probability of superiority directly. Statistical significance alone does not override minimum_effect.

Metric direction is normalized so a positive effect is always better for the candidate. Grader direction supplies this metadata, while eval metrics default to higher-is-better. Native metric names without resolved per-run observations return EXTEND rather than guessing. Automatic decisions for experiments with more than two variants return INCONCLUSIVE.

For example, with direction: max, a grader increase from 0.80 to 0.84 has an absolute effect of +0.04. If minimum_effect is 0.05, even a statistically significant result is INCONCLUSIVE with effect_below_threshold. With direction: min, a duration decrease from 10 seconds to 8 seconds has a raw absolute effect of -2, but a normalized effect of +2; users do not reverse lower-is-better metrics themselves.

Mandatory guardrails take precedence over the primary metric. A primary-metric improvement cannot override a failed guardrail:

Primary metric: +12%
Tool-failure guardrail: FAIL
Decision: REJECT (guardrail_failed)

Missing or undersampled guardrail observations do not pass. They produce EXTEND with insufficient_observations; an unsupported guardrail metric produces EXTEND with guardrail_unsupported.

The decision portion of the default CLI output uses these fields:

Readiness : READY
READY FOR ANALYSIS — all 2 variants have reached min_samples (20); outcome metric analysis is available
Decision : PROMOTE candidate
Reason : candidate materially improves the primary metric with sufficient evidence and all guardrails pass (candidate_improved)

An adequately sampled result without sufficient evidence remains distinct:

Readiness : READY
READY FOR ANALYSIS — all 2 variants have reached min_samples (20); outcome metric analysis is available
Decision : INCONCLUSIVE
Reason : minimum samples are available but the configured evidence threshold is not satisfied (evidence_insufficient)

While observations are still accumulating, output instead includes:

Readiness : COLLECTING
EXTEND — 1 of 2 variant(s) below min_samples threshold (min observed: 19 / 20)
Decision : EXTEND
Reason : one or more variants have fewer usable observations than min_samples (insufficient_samples)

reason_code is the stable machine-readable explanation for the core decision:

Reason codeInterpretation
insufficient_samplesAt least one variant has fewer than min_samples usable observations.
insufficient_observationsA required comparison or mandatory guardrail lacks usable observations.
candidate_improvedThe candidate has sufficient evidence of an improvement meeting minimum_effect.
candidate_regressedThe candidate has sufficient evidence of a regression exceeding regression_tolerance.
guardrail_failedA mandatory guardrail failed; this overrides a primary-metric win.
guardrail_unsupportedA mandatory guardrail is not backed by a supported observation source.
effect_below_thresholdEvidence is sufficient, but the practical effect does not meet the configured threshold.
evidence_insufficientSamples are ready, but the configured frequentist or Bayesian evidence threshold is not met.
unsupported_multi_variantAutomatic decisions currently require exactly two variants.
analysis_unavailableThe configured analysis cannot produce the required effect or evidence.

interaction_underpowered is not a core reason code. The daily experiment report uses it only for a presentation-level safety hold when simultaneous-experiment cells are sparse. It preserves the core decision and reports report_action: EXTEND.

Use --json for the stable automation boundary:

Terminal window
gh aw experiments analyze <workflow> --json

Each entry in analyses includes decision, reason_code, samples, decision_guardrails, and decision_policy, alongside the separate readiness field. control, candidate, effect, and evidence are emitted when those values are available for the decision path (for example two-variant statistical comparisons); early EXTEND and multi-variant INCONCLUSIVE results may omit them. Future promotion automation can consume these fields without rerunning statistics or interpreting grader artifacts.

A ready two-variant result can contain:

{
"decision": "PROMOTE",
"reason_code": "candidate_improved",
"reason": "candidate materially improves the primary metric with sufficient evidence and all guardrails pass",
"control": "control",
"candidate": "candidate",
"direction": "max",
"samples": {
"control": 20,
"candidate": 20
},
"effect": {
"absolute": 0.08,
"relative": 0.1,
"normalized_absolute": 0.08
},
"evidence": {
"analysis_type": "mann_whitney",
"significant": true,
"p_value": 0.01
},
"decision_guardrails": {
"configured": true,
"passed": true
},
"decision_policy": {
"minimum_effect": 0.05,
"regression_tolerance": 0.05,
"confidence": 0.95
},
"experiment_name": "prompt_v2",
"analysis_type": "mann_whitney",
"metric": "grader:trajectory-efficiency",
"min_samples": 20,
"readiness": "READY",
"recommendation": "READY_FOR_ANALYSIS"
}

The stable automation boundary is analyses[].readiness, analyses[].decision, and analyses[].reason_code. Reporting workflows may add a presentation or interaction-safety hold, but must preserve the core decision rather than recomputing it.

The core CLI supports all four analysis methods (t_test, mann_whitney, proportion_test, and bayesian_ab). Frequentist methods provide p-value evidence. Bayesian analysis provides probability_superiority; it is significant when that probability is at least confidence or at most 1-confidence, and it is never described as a p-value.

The daily experiment report consumes this structured core decision. It does not independently recompute the decision policy. Its only additional outcome handling is the interaction safety hold described above. Older reporting terminology such as ABANDON, and guardrail statuses such as GUARDRAIL_FAILED, are not core decision values.

Existing experiment configurations remain compatible. Analysis still defaults to 20 samples per variant and the configured or inferred statistical method. A deterministic decision is emitted even when no decision: block exists, using minimum_effect: 0, regression_tolerance: 0, and confidence: 0.95. The legacy recommendation remains in JSON. No migration is required.

Decisions are only as meaningful as their configured observations. A grader measures the behavior it encodes, not universal task correctness. Low-frequency or heterogeneous tasks may need larger samples, simultaneous experiments can complicate attribution, and no observed regression proves only the configured evidence—not universal non-regression.

Use --experiment and --variant to filter audit runs to a specific variant:

Terminal window
gh aw audit <run-id> --experiment prompt_style --variant concise

Each activation job writes a Markdown step summary that shows the selected variants, cumulative counts, and, when you use the object form, progress toward min_samples:

## A/B Experiment Assignments
| Experiment | Selected Variant | All Variants | Cumulative Counts |
| --- | --- | --- | --- |
| prompt_style | concise | concise, detailed | concise: 8, detailed: 7|
### Sampling Progress
prompt_style (target: 25 per variant)
concise: ████████░░░░░░░░░░░░ 8/25 (32%)
detailed: ███████░░░░░░░░░░░░░ 7/25 (28%)
### Experiment Details
**prompt_style**
> Test whether a concise prompt reduces cost without quality loss
**Hypothesis:** H0: no change in aic. H1: concise reduces AIC by >=15%
**Guardrail metrics:**
- `success_rate` >=0.95
- `empty_output_rate` ==0
Tracking issue: [#1234](https://github.com/owner/repo/issues/1234)
FieldTypeDescription
experimentsobjectMap of experiment name → variant array or config object
experiments.<name>string[]Array of two or more variant strings for one experiment
FieldTypeRequiredDescription
variantsstring[]Array of two or more variant strings
descriptionstringHuman-readable explanation of what the experiment tests
hypothesisstringNull and alternative hypothesis (e.g. "H0: no change. H1: concise reduces AIC by >=15%")
metricstringPrimary metric to observe (e.g. aic, duration_ms)
secondary_metricsstring[]Additional metrics to track alongside the primary metric
guardrail_metricsobject[]List of guardrail objects with name (string), threshold (comparison string like >=0.95 or bare number like 0.0), and optional direction ("min" or "max"). When threshold is a bare number, direction governs the pass condition (≤ for min, ≥ for max). See experiments-specification §4.4 for full semantics.
min_samplesintegerMinimum runs per variant required before statistical analysis is considered reliable. The step summary shows a progress bar toward this target.
analysis_typestringStatistical method: t_test, mann_whitney, proportion_test, or bayesian_ab.
decisionobjectDeterministic policy with optional non-negative minimum_effect, non-negative regression_tolerance, and confidence between 0 and 1. Effects use absolute primary-metric units.
weightinteger[]Per-variant probability weights (same length as variants). Enables weighted-random selection; values are relative and need not sum to 100.
issueintegerGitHub issue number that tracks this experiment’s lifecycle
start_datestringISO-8601 date (YYYY-MM-DD) before which the experiment is inactive. The control variant is returned before this date without incrementing any counter.
end_datestringISO-8601 date (YYYY-MM-DD) after which the experiment is inactive. The control variant is returned after this date without incrementing any counter.
continualobjectExperimental deterministic control/candidate assignment with automatic traffic ramping.

A continual experiment assigns a candidate to a bounded share of future executions. The first variant is the control and the second is the candidate.

experiments:
optimize_tool_use:
variants: [control, candidate]
metric: eval:quality
min_samples: 20
continual:
seed: tool-use-v1
ramp: [10, 25, 50]

Assignment happens in the activation job before agent execution. A SHA-256 hash of the seed, experiment name, repository, workflow, and run ID selects the variant. The activation job advances the ramp after each min_samples candidate assignments and stores the current stage on the experiment branch, leaving the workflow source immutable. The logs show the stage, assignment counts, weights, and selected variant.

The ramp does not evaluate outcomes or promote a winner. Use the existing experiment analysis commands and configured metrics to decide whether to stop the experiment or make a variant permanent.