devops #github-actions #workflow-runs #ci #pagination #coding-agents #change-control

A 2,500+ Actions Count Is Not an Exact Run Inventory

A GitHub Actions badge that says 2,500+ is a capped count, not a run inventory. Print the cap, the 1,000-item page, a date-range filter, and the named Actions owner before you file missing runs.

Standup hears “we have 2,500 runs.” Someone pasted the Actions tab. The coding agent pasted total_count from GET /repos/{owner}/{repo}/actions/runs. The junior files missing-runs: “the rest of the history vanished.”

I stop the run there. A 2,500+ Actions count is not an exact run inventory. Official GitHub changelog for 25 September 2026 is blunt: queries for workflow runs in the Actions API and UI still paginate up to 1,000 items, and if the number of found records exceeds 2,500, GitHub reports “2,500+” instead of an exact number. Larger queries often timed out and returned however many records had been counted before the timeout. The cap is the honest number. The exact inventory is a different ticket. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/]

I already refused to treat a timed-out Laravel job as a killed worker pool in A Timed-Out Job Is Not a Killed Worker. I already refused to treat ubuntu-latest as a runner image I already tested in Ubuntu-latest Is Not a Runner Image You Already Tested. I already refused to treat an auto-started Codex background server as compatible settings in An Auto-Started Codex Background Server Is Not Compatible Settings. This post is the same desk rule for Actions history. Print the cap. Print the page. Name who owns the workflow-run query.

The question is not whether a dashboard demos a big number. The question is whether the named owner can tell a capped count from a missing run before paging CI.

Three columns: 2,500+ capped count, 1,000 item page, named Actions owner

The ticket that looks like missing history

Juniors treat 2,500+ the way they treat a database COUNT(*). The badge is large. The list stops. They file “GitHub deleted our runs.”

Two jobs collide on that screen.

  1. Stop a query that used to lie with a partial count. GitHub’s own reason: queries that retrieve more than 2,500 records frequently timeout and return the number of records found before the timeout rather than the true count. The new badge is less precise and more accurate. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/]
  2. Keep listing the page you can actually fetch. The same note says paginated results still go up to 1,000 items. Official REST docs for list-workflow-runs already said that endpoint returns up to 1,000 results for each search when you use actor, branch, check_suite_id, created, event, head_sha, or status. [Source: https://docs.github.com/en/rest/actions/workflow-runs]

If you only screenshot “2,500+,” you will file missing history. You will not file the query.

I do not invent a fake overnight wipe. I use the public contract. The changelog page is the ticket, not a version pin in the title.

Three tickets, one owner

Laravel plus Vue work on this desk still sits next to GitHub Actions: a workflow under .github/workflows/, a coding agent that treats total_count as COUNT(*), a junior who writes a scraper because the badge stopped climbing. Mixing the cap, the page, and the date window into one “Actions is broken” thread hides the owner.

TicketWhat it meansWhat you doOwner
UI or API reports 2,500+Found records exceeded 2,500Do not call it 2,500 exact runsNamed human who owns the workflow-run query
List stops at 1,000 itemsSearch pagination cap on that queryPage inside the cap. Do not scrape past itSame named human
You need a specific weekExact inventory for that windowAdd created (date range). Narrow workflow, event, status, branch, or actorSame named human

Do not paste one badge and call the history complete. If the screen says 2,500+, you are on a cap ticket. If workflow_runs.length is 1,000 and the next page is empty, you are on a page ticket. If you need last Tuesday’s failed deploy, you are on a date-range ticket.

Dates and caps are evidence, not the hook
GitHub published the query-results note on 25 September 2026. The change is rolling out on github.com and GitHub Enterprise Cloud. Filters named in that note: workflow, event, status, branch, or actor. Pagination named in that note: up to 1,000 items. Cap named in that note: report “2,500+” when found records exceed 2,500. REST docs still list per_page max 100 and page for the list-runs endpoint. Print those strings on the ticket. Do not put 2,500+ in a social title as a version hook. Do not treat Enterprise Server as covered unless a later note says so. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/] [Source: https://docs.github.com/en/rest/actions/workflow-runs]

Three tickets: 2,500+ cap, 1,000 page, date range

What the list endpoint actually returns

I do not invent a fake JSON field. I use the public contract.

Official REST: GET /repos/{owner}/{repo}/actions/runs lists workflow runs for a repository. Anyone with read access can use it. The example response includes total_count as a number next to a workflow_runs array. Query parameters include actor, branch, event, status, per_page (max 100), page, and created for a date-time range. [Source: https://docs.github.com/en/rest/actions/workflow-runs]

Official REST also states the 1,000-result search cap when those filter parameters are in use. That sentence was already in the docs before this week’s badge. Octokit maintainers confirmed the same hard cap in a public discussion: paging past entry 1,000 returns empty lists even when an earlier total_count looked higher. [Source: https://docs.github.com/en/rest/actions/workflow-runs] [Source: https://github.com/orgs/octokit/discussions/81]

This week’s changelog adds the count behavior. Do not mix the two.

  1. Page of runs. Up to 1,000 items for a filtered search. per_page still max 100, so ten pages is the search window, not infinity. [Source: https://docs.github.com/en/rest/actions/workflow-runs] [Source: https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api]
  2. Reported total. If found records exceed 2,500, GitHub reports “2,500+” instead of attempting the exact number. That is the new honesty. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/]

Copy the docs example as a probe, not as a bypass recipe.

 1# Probe the list endpoint. Do not page past the documented search cap.
 2export ACTIONS_RUNS_OWNER="shinjae"
 3export GH_REPO="OWNER/REPO"
 4export CREATED_RANGE="2026-09-22T00:00:00+07:00..2026-09-29T00:00:00+07:00"
 5
 6gh api \
 7  -H "Accept: application/vnd.github+json" \
 8  -H "X-GitHub-Api-Version: 2026-03-10" \
 9  "/repos/${GH_REPO}/actions/runs?per_page=100&page=1&created=${CREATED_RANGE}" \
10  --jq '{owner: env.ACTIONS_RUNS_OWNER, total_count, run_len: (.workflow_runs|length), first_id: .workflow_runs[0].id}'

[Source: https://docs.github.com/en/rest/actions/workflow-runs]

created is the filter GitHub asked you to add when a script used to pull more than 2,500 matching runs from one query. Narrow the window. Do not write a second loop that walks page=11 hoping the 1,000 cap moved. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/]

A coding agent will still print total_count and stop. That is how the wrong ticket gets filed.

Treat three strings as different fields.

  1. Exact integer under 2,500. The query is small enough that GitHub still attempts an exact count. Still not an inventory of the whole repo forever. It is the count for that filter.
  2. Badge or report of 2,500+. Found records exceeded 2,500. Stop calling it 2,500. Stop calling it 3,000. File “query hit the cap.”
  3. Array length 1,000. You hit the search page cap. The next page is not a secret inventory. Narrow created, branch, event, or status.

I keep a small probe in the app that actually ships. It does not scrape. It refuses to treat a cap as a total.

 1#!/usr/bin/env python3
 2"""Probe GitHub Actions run queries. A cap is not an inventory."""
 3from __future__ import annotations
 4
 5import json
 6import os
 7import sys
 8from pathlib import Path
 9
10CAP_TOTAL = 2500
11CAP_PAGE = 1000
12
13
14def main() -> int:
15    owner = os.environ.get("ACTIONS_RUNS_OWNER", "").strip()
16    payload = Path(os.environ.get("ACTIONS_RUNS_JSON", "actions-runs.json"))
17    if not owner:
18        print("VERDICT=NO_OWNER")
19        return 2
20    if not payload.is_file():
21        print("VERDICT=NO_PAYLOAD")
22        return 2
23
24    data = json.loads(payload.read_text(encoding="utf-8"))
25    raw_total = data.get("total_count")
26    runs = data.get("workflow_runs") or []
27    run_len = len(runs)
28
29    total_text = str(raw_total).strip()
30    print(f"OWNER={owner}")
31    print(f"TOTAL_FIELD={total_text}")
32    print(f"RUN_LEN={run_len}")
33
34    if total_text in {"2500+", "2,500+", "2500"} and run_len >= min(CAP_PAGE, run_len):
35        if total_text in {"2500+", "2,500+"} or (
36            isinstance(raw_total, int) and raw_total >= CAP_TOTAL
37        ):
38            print("VERDICT=CAPPED_COUNT_NOT_INVENTORY")
39            return 0
40
41    if run_len >= CAP_PAGE:
42        print("VERDICT=PAGE_CAP_NOT_INVENTORY")
43        return 0
44
45    if isinstance(raw_total, int) and raw_total < CAP_TOTAL:
46        print("VERDICT=FILTERED_COUNT_UNDER_CAP")
47        return 0
48
49    print("VERDICT=INSUFFICIENT_QUERY")
50    return 2
51
52
53if __name__ == "__main__":
54    raise SystemExit(main())

Save one API page to actions-runs.json. Run the probe in the repo that ships.

1export ACTIONS_RUNS_OWNER="shinjae"
2export ACTIONS_RUNS_JSON="actions-runs.json"
3python3 scripts/probe_actions_run_cap.py

VERDICT=CAPPED_COUNT_NOT_INVENTORY means you do not have a wipe. Then open the Actions tab. Copy the filters. Copy the badge. Put those four lines on the ticket: owner, badge, run_len, verdict.

A second probe is the date window itself. Print created, the workflow file name, event, status, and branch. If the unfiltered query is 2,500+ and last Tuesday’s window is 37, you are looking at the changelog, not a deletion.

Probe checklist: owner, badge 2,500+, run_len, verdict

What a date range is for

GitHub’s own remediation is one sentence: if your integrations or scripts rely on retrieving more than 2,500 matching workflow runs from a single query, narrow your filters, for example by adding a date range, to retrieve the specific runs you need. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/]

That is the Monday move. It is not a scrape.

Official REST created uses GitHub’s date-range search syntax. Put a start and an end. Keep the window small enough that found records stay under the cap you care about for that ticket. [Source: https://docs.github.com/en/rest/actions/workflow-runs]

 1# .github/workflows/ci.yml — evidence only. Do not treat this file as a run inventory.
 2name: CI
 3on:
 4  push:
 5    branches: [main]
 6  pull_request:
 7jobs:
 8  phpunit:
 9    runs-on: ubuntu-24.04
10    steps:
11      - uses: actions/checkout@v4
12      - name: Run PHPUnit
13        run: php artisan test

The workflow file tells you which job ran. It does not tell you how many historical runs exist. For last week’s failed deploy you still query with created and status=failure. For “how busy is this repo this year” you accept 2,500+ and you stop.

I do not write a pager that walks until GitHub 404s. Pagination docs say most endpoints cap per_page at 100. The Actions search cap is 1,000 results per filtered search. Walking page=11 after a 1,000 cap is how juniors burn an hour and still file the wrong ticket. [Source: https://docs.github.com/rest/using-the-rest-api/using-pagination-in-the-rest-api] [Source: https://docs.github.com/en/rest/actions/workflow-runs]

What you must not mix into this ticket

This is not last week’s queue timeout. A job clock that kills one queue:work process is a Laravel worker ticket. A Timed-Out Job Is Not a Killed Worker already owns that field.

This is not the runner-label ticket. ubuntu-latest moving later this year is a pin-and-test ticket. Ubuntu-latest Is Not a Runner Image You Already Tested already owns that field.

This is not the Codex auto-start ticket. A background server that is not compatible settings is a change-control ticket. An Auto-Started Codex Background Server Is Not Compatible Settings already owns that field.

GSC this week still has no striking-distance query on those URLs. I am not refreshing them. This is a new field: a capped Actions count is not an exact run inventory.

If you need the broader habit, start at /ai-agent-operations/. Tooling notes live under /developer-tools/. Laravel plus Vue notes live under /laravel-vue-saas/. A first-week map is at /start-here/.

Do-not-mix: queue timeout, ubuntu-latest, Actions 2,500+

What you must not do

Forbidden:

  1. File a “run history vanished” ticket without printing the query filters, the 2,500+ cap, the 1,000-item page, a created range, and one human name on Actions.
  2. Put a GitHub API version, a runner image, or 2,500+ as a version-style hook in the title or the first line beyond the field name this post already uses.
  3. Mix this field with a Laravel job timeout, an ubuntu-latest migration, or a Codex auto-start settings ticket. Those are other posts.
  4. Treat total_count: 2500 or a 2,500+ badge as COUNT(*).
  5. Page past the documented 1,000-result search cap to “finish the inventory.”
  6. Write a scraper, a headless clicker, or an export-all script because the badge stopped climbing.
  7. Recommend buying Actions minutes, a Copilot plan, or a seat because the count capped.
  8. Claim GitHub Enterprise Server already flipped unless a later official note says so. The 25 September note names github.com and GitHub Enterprise Cloud. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/]
  9. Treat an empty eleventh page as proof of deletion. The search cap already explains an empty page after 1,000. [Source: https://docs.github.com/en/rest/actions/workflow-runs]
  10. Ignore GitHub’s own fix: narrow filters, for example a date range, to retrieve the specific runs you need. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/]

Allowed:

  1. Print the badge, total_count, len(workflow_runs), per_page, and page.
  2. Add created, branch, event, status, actor, or workflow filters until the window fits the ticket.
  3. Name one human as ACTIONS_RUNS_OWNER.
  4. Keep last week’s failed deploy as a dated query, not as a full-repo count.
  5. Leave the unfiltered tab at 2,500+ and stop.

A changelog bullet about a more accurate count is not permission to skip the owner.

What you should do Monday morning

  1. Open the repo that actually ships. Export ACTIONS_RUNS_OWNER to a human name. Save one GET /repos/{owner}/{repo}/actions/runs page to actions-runs.json. Run probe_actions_run_cap.py. Write the verdict on the ticket next to that name.
  2. Open the Actions tab from the incident. Copy the badge. Copy the filters. If it says 2,500+, file “query hit the cap,” not “GitHub deleted runs.”
  3. If you need last week’s failed deploy, add created for that week and status=failure. Do not reuse the unfiltered total. [Source: https://github.blog/changelog/2026-09-25-changes-to-query-results-in-the-github-actions-api-and-ui/] [Source: https://docs.github.com/en/rest/actions/workflow-runs]
  4. If someone pasted a pager that walks page=11 after 1,000 items, reject it. Point at the REST search cap. [Source: https://docs.github.com/en/rest/actions/workflow-runs]
  5. Confirm coding-agent instructions on this desk name the same owner and forbid “the run history vanished” without the four lines: owner, badge, run_len, verdict.
  6. Do not refresh the timeout post, the ubuntu-latest post, or the auto-start post. Those URLs already exist. This URL is the unused Actions-count field.

The question is not whether a 2,500+ badge demos well in a screenshot. The question is whether the named owner can still tell a capped count from a missing run after handoff.

Further reading

Source GitHub Changelog — Changes to query results in the GitHub Actions API and UI

Source GitHub Docs — REST API endpoints for workflow runs

Source GitHub Docs — Using pagination in the REST API