actions/stale: the inactivity clock behind GitHub issue cleanup, and where it trips up
Marks issues and pull requests that have not had recent interaction
At a glance
- What is it?
- actions/stale is GitHub's own TypeScript action that labels, comments on and closes issues and pull requests once they go quiet, with a two-stage timer and a cache-backed cursor. It works well for high-traffic repositories, but its statefulness and its permission needs are easy to misread.
- Who is it for?
- Adopt it on a public repository that accumulates many idle issues and where the team accepts automatic closing with not_planned as the reason. Skip it on small repositories, on private work where a wrong close destroys review context, or on anything that cannot spare issues: write and pull-requests permissions.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 6 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 21, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two timers, not one: 60 days to label, 7 more to close
The default behaviour is a staged process rather than a single cutoff. After 60 days with no activity an issue or pull request gets the label Stale and a comment. The clock then restarts, and after another 7 days of silence the item is closed. Any update or comment on a stale item removes the label and resets the timer from scratch, which is the mechanism that keeps active discussions out of the queue.
Each stage has its own set of overrides, and the split is finer than most users expect. days-before-stale sets the idle threshold for both item types, while days-before-issue-stale and days-before-pr-stale override it for one type only. The close side works the same way, with days-before-close at 7 by default and days-before-issue-close and days-before-pr-close as the per-type overrides. Every one of these inputs is optional.
The four message inputs let you say something different at each moment: stale-issue-message and stale-pr-message post when the label goes on, close-issue-message and close-pr-message post when the item is closed. The label inputs split the same way, with stale-issue-label and stale-pr-label defaulting to Stale and close-issue-label and close-pr-label left empty, so nothing extra is applied at close time unless you ask for it.
The close reason defaults to not_planned through close-issue-reason, which is worth pausing on. That is the GitHub API value for an issue closed as not planned, and it is the reason field an issue carries into the closed view.
Why close-issue-reason defaults to not_planned and not another value
The docs give you the knob but not much argument for it, so teams that want completed work to show as completed have to set the reason themselves rather than accept what arrives.
The message inputs are the more useful lever. An empty message means silent treatment apart from the label, which is the quietest configuration and also the one most likely to surprise contributors who expected an explanation. Filling in stale-pr-message costs nothing extra and tells an author exactly how to revive their pull request.
The label filters decide what the action is allowed to touch
Four inputs narrow the working set. exempt-issue-labels and exempt-pr-labels list labels that exempt an item from ever going stale. only-labels restricts processing to items carrying all of the listed labels, with only-issue-labels and only-pr-labels overriding it per type. any-of-labels does the opposite, accepting items that carry any listed label, again with any-of-issue-labels and any-of-pr-labels available for the split.
Getting these wrong is the most common way to get surprising behaviour. A misspelled label name in exempt-issue-labels does not fail loudly; it simply exempts nothing. A stale only-labels filter can leave the action with an empty set and nothing to do.
Repository access also has to be declared. The action must be able to read every issue and pull request in the repository, and whatever the configuration asks for on top of that.
permissions block: write scopes you cannot always get
When a workflow's permissions are restricted, the action needs explicit scopes. The block in the README is the baseline set, and note that one line is conditional:
permissions:
actions: write
contents: write # only for delete-branch option
issues: write
pull-requests: writecontents: write appears only because the delete-branch option needs it. The action authenticates with ${{ github.token }} by default, and repo-token accepts a PAT instead if you need wider reach.
Where to put the action and what it does once it runs
The configuration has to live on the default branch.
At run time the action walks the repository's issues and pull requests through Octokit, applying the label, posting the comment and closing at the thresholds above. The dependencies in package.json back this up, including @octokit/core, @octokit/plugin-retry and @actions/github. The build step compiles TypeScript to lib/main.js and bundles it with ncc into dist, which is what a workflow actually runs.
Operations-per-run and the cache that remembers where it stopped
The action is stateful, and this is the part that changes how you schedule it. When a run stops because it hit operations-per-run, the next run resumes at the first unprocessed issue, skipping everything handled by earlier runs. The saved state disappears once every item has been processed.
The state is stored where GitHub Actions cache entries live, and the README points out that its lifetime matches the cache policy configured for the repository. A repository with aggressive cache eviction can lose the cursor, and the next run starts over rather than continuing.
The practical consequence is about scheduling, not correctness. A daily cron on a large backlog is a reasonable starting point; a workflow that is expected to sweep everything in one pass is not, because it will hit the operations ceiling and hand the rest to later runs.
Runner requirements after v10 and the ESM move in v11
Two release notes matter for anyone pinning a version. V10 upgraded the runtime from node20 to node 24, and the README says to make sure the runner is on version v2.327.1 or later for compatibility.
V11 changed the packaging rather than the contract: the action migrated to ESM internally to support the latest @actions/* package versions, with no changes to inputs, outputs or behavior. The package metadata reflects that, with type set to module and engines requiring node 24 and npm >=7.0.0. For most users V11 is a drop-in replacement for V10.
The last push was on 2026-10-14 and v11.0.0 was released on 2026-07-28, with v10.4.0 on 2026-07-10 and v10.3.0 on 2026-05-21. The repository is not archived. The licence is MIT.
Where a hand-rolled script beats it
A small repository may not need this. Writing a scheduled workflow with a small script that queries the API and closes a fixed set of abandoned items gives you exact control over which ones and why, without the label churn that actions/stale creates on the default path.
The difference in approach is configurability versus precision. actions/stale exposes many inputs and applies the same policy across every item that matches the filters, which is useful at volume and can be a blunt instrument at small scale. It also has the stronger claim to long-term care, being the action GitHub maintains itself.
What it will not do is judge content. There is no similarity search, no superseded detection, no sense of whether a duplicate still matters. Its only input is elapsed time.
Editorial conclusion
Adopt it on a public repository that accumulates many idle issues and where the team accepts automatic closing with not_planned as the reason. Skip it on small repositories, on private work where a wrong close destroys review context, or on anything that cannot spare issues: write and pull-requests permissions. Before the first scheduled run, check the exempt-issue-labels and only-labels filters against real labels in the repository, and confirm the workflow file sits on the default branch, because the action reads its configuration from there and nowhere else.
Frequently asked questions
What permissions does the actions/stale action need in a workflow?
It must be able to fetch all issues and pull requests, plus whatever the configuration requires for labelling, commenting and closing. The README shows a block with actions: write, issues: write, pull-requests: write, and contents: write only when the delete-branch option is used.
What is the default number of days before actions/stale closes an issue?
The Stale label and a comment are applied after 60 days of inactivity, and the item is closed after a further 7 days of inactivity. Any comment or update removes the label and restarts the timer.
Does actions/stale need to run on the default branch?
The configuration must be on the default branch, according to the README. That covers both the workflow file that schedules the action and the configuration it reads.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/actions-stale)Was this useful?