Clean Up GitHub Workflow Runs From the Terminal

Delete all GitHub workflow runs in terminal using gh cli command

Velu S Gautam2 min readTechnology

Why

Renaming or deleting a workflow file doesn't clear its run history — the old runs stay listed under Actions → All workflows with no active workflow behind them, and the GitHub UI has no bulk-delete button for them. Deleting the runs through the API with gh is the only way to clear that list.

Before you start

  • Install and authenticate the GitHub CLI: gh auth login
  • Run the commands from inside the repo, or add --repo owner/repo to every gh run list / gh run delete call
  • Use the workflow's filename (e.g. deploy.yml), not its display name, for --workflow

How it works

repeat until list is empty

gh run list --workflow deploy.yml

extract run IDs
(--json databaseId --jq '.[].databaseId')

loop over each ID

gh run delete <id>

macOS / Linux (bash, zsh)

gh run list only returns 20 runs by default. Leave that alone and the loop silently stops after the first page, so anything past run #20 never gets deleted. Add --limit set comfortably above your run count — 1000 is a safe ceiling — to actually clear everything.

BASH
WORKFLOW="deploy.yml"
 
for id in $(gh run list --workflow "$WORKFLOW" --limit 1000 --json databaseId --jq '.[].databaseId'); do
  gh run delete "$id"
done

Works unchanged in bash and zsh — the default shell on modern macOS and most Linux distros.

Windows (PowerShell)

POWERSHELL
$workflow = "deploy.yml"
 
gh run list --workflow $workflow --limit 1000 --json databaseId --jq ".[].databaseId" |
    ForEach-Object { gh run delete $_ }

gh's --jq flag runs on a built-in JSON engine, you don't need a separate jq install on either OS. Prefer the bash version? It runs unchanged in Git Bash or WSL.

A safer version (confirmation, dry run, progress)

The scripts above delete everything immediately, with no preview and no way back. This version counts the runs first, can list them without deleting (--dry-run), and asks for confirmation before touching anything.

bash / zsh

BASH
#!/usr/bin/env bash
set -euo pipefail
 
WORKFLOW="${1:?Usage: $0 <workflow-file> [--dry-run]}"
DRY_RUN=false
[[ "${2:-}" == "--dry-run" ]] && DRY_RUN=true
 
ids=()
while IFS= read -r id; do
  ids+=("$id")
done < <(gh run list --workflow "$WORKFLOW" --limit 1000 --json databaseId --jq '.[].databaseId')
 
if [[ ${#ids[@]} -eq 0 ]]; then
  echo "No runs found for $WORKFLOW"
  exit 0
fi
 
echo "Found ${#ids[@]} run(s) for $WORKFLOW"
 
if $DRY_RUN; then
  printf '%s\n' "${ids[@]}"
  exit 0
fi
 
read -rp "Delete all ${#ids[@]} runs? [y/N] " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || exit 0
 
count=0
for id in "${ids[@]}"; do
  gh run delete "$id"
  count=$((count+1))
  echo "Deleted $count/${#ids[@]}"
done

Usage: ./cleanup-runs.sh deploy.yml to run, or ./cleanup-runs.sh deploy.yml --dry-run to preview. Uses a plain while read loop instead of mapfile so it also runs on the old bash (3.2) that macOS ships by default.

PowerShell

POWERSHELL
param(
    [Parameter(Mandatory)][string]$Workflow,
    [switch]$DryRun
)
 
$ids = @(gh run list --workflow $Workflow --limit 1000 --json databaseId --jq ".[].databaseId") | Where-Object { $_ -ne "" }
 
if ($ids.Count -eq 0) {
    Write-Host "No runs found for $Workflow"
    exit 0
}
 
Write-Host "Found $($ids.Count) run(s) for $Workflow"
 
if ($DryRun) {
    $ids | ForEach-Object { Write-Host $_ }
    exit 0
}
 
$confirm = Read-Host "Delete all $($ids.Count) runs? [y/N]"
if ($confirm -notmatch '^[Yy]$') { exit 0 }
 
$count = 0
foreach ($id in $ids) {
    gh run delete $id
    $count++
    Write-Host "Deleted $count/$($ids.Count)"
}

Usage: .\cleanup-runs.ps1 -Workflow deploy.yml, add -DryRun to preview first. The @(...) wrapper matters — without it, PowerShell collapses a single-line result into a plain string instead of a one-item array, and .Count breaks.

Variant: keep the last N runs

For trimming history instead of wiping it — useful once a workflow's been running for a while and only the recent runs matter.

BASH
#!/usr/bin/env bash
set -euo pipefail
 
WORKFLOW="${1:?Usage: $0 <workflow-file> <keep-count>}"
KEEP="${2:?Usage: $0 <workflow-file> <keep-count>}"
 
ids=()
while IFS= read -r id; do
  ids+=("$id")
done < <(gh run list --workflow "$WORKFLOW" --limit 1000 --json databaseId --jq '.[].databaseId')
 
to_delete=("${ids[@]:$KEEP}")
 
if [[ ${#to_delete[@]} -eq 0 ]]; then
  echo "Nothing to delete — ${#ids[@]} run(s), keeping $KEEP"
  exit 0
fi
 
echo "Deleting ${#to_delete[@]} run(s), keeping the newest $KEEP"
for id in "${to_delete[@]}"; do
  gh run delete "$id"
done

Usage: ./keep-last-n.sh deploy.yml 10 deletes everything except the 10 most recent runs gh run list returns runs newest-first, so the slice after the first KEEP entries is everything older.

Variant: only failed or cancelled runs

Leaves successful runs alone and clears out the noise.

BASH
gh run list --workflow deploy.yml --limit 1000 --json databaseId,conclusion \
  --jq '.[] | select(.conclusion=="failure" or .conclusion=="cancelled") | .databaseId' \
  | xargs -n1 gh run delete

Comments