Managing Stale Issues and Pull Requests with GitHub Actions¶
Every open-source project eventually faces the same problem: issues and pull requests that were once active go quiet. A bug report with no updates in a year. A pull request that was never finished. A feature request that the contributor lost interest in. These threads accumulate over time, and before long a repository's issue tracker becomes a graveyard of items that no one knows are still relevant.
The actions/stale GitHub Action gives maintainers a way to address this automatically. It scans issues and pull requests on a schedule, labels anything that has gone too long without activity, warns contributors that it will be closed soon, and closes it if no activity follows. The whole process is configurable and runs without manual intervention.
This post covers what the action does, how to configure it, a real-world example from one of my own projects, and an honest look at the tradeoffs so you can decide whether it makes sense for yours.
What actions/stale Does¶
actions/stale is a GitHub Action maintained by GitHub itself. It runs on a schedule (or on-demand), iterates through open issues and pull requests, and applies a configurable staleness policy.
The lifecycle it implements is straightforward:
- Mark as stale: If an issue or pull request has had no activity for a configurable number of days, the action adds a label (typically
stale) and posts a comment explaining that it has been marked stale. - Close if no response: If the stale item receives no activity for a second configurable period, the action closes it automatically.
- Unmark if updated: If any activity occurs after the stale label is applied but before the item is closed, the stale label is removed and the timer resets.
Maintainers can configure separate timers for issues and pull requests, exempt specific labels from the stale policy (so a bug label or a needs-review label keeps an item alive indefinitely), and customize both the stale label and the warning message.
The Debate: Is Stale Management a Good Idea?¶
Before setting this up, it's worth understanding the tradeoffs. There's genuine disagreement in the open-source community about whether automated stale management helps or hurts projects.
Arguments in Favor¶
- Reduces noise in the tracker. An issue tracker full of old, unresolved items is harder to navigate and prioritize. Closing stale items surfaces what is actually active.
- Prompts contributors to re-engage. The stale warning comment gives contributors a clear signal to provide an update or confirm the issue is still relevant before it's closed.
- Reduces maintainer cognitive load. A smaller set of active items is easier to triage, prioritize, and respond to. It also makes the health of the project easier to assess at a glance.
- Handles abandoned pull requests cleanly. Pull requests that were never finished, never responded to review feedback, or became stale after a long review cycle can be closed without requiring a maintainer to manually track and close each one.
- Encourages good triaging hygiene. When maintainers know items will eventually be closed without activity, they are more likely to respond promptly and keep the tracker clean.
Arguments Against¶
- A reported bug that's stale is still a bug. Closing a bug report because the original reporter went quiet doesn't fix the underlying problem. Users who later encounter the same issue will either open a duplicate or not bother, degrading the quality of your issue history.
- Discourages contributors. A contributor who invested time in a detailed bug report or a pull request and returns to find it automatically closed may feel dismissed, even if a comment was posted first.
- Creates false signals. A closed issue can imply the problem was resolved. If the issue was closed due to inactivity rather than a fix, new users may not realize the problem is still present.
- High-priority items can fall through. Without careful label exemptions, important issues that simply haven't been triaged yet can be swept up by the stale policy.
- Not a substitute for active maintenance. Stale management isn't a replacement for actually triaging and responding to issues. If a project is understaffed, stale management cleans up the symptom but not the cause.
Making the Decision¶
The right answer depends on the nature of your project and your goals as a maintainer.
A few questions to consider:
- What type of project is it? A tool with a fast release cadence and a small surface area has different needs than a large framework or a project that supports critical infrastructure. The latter may want to be more conservative about closing issues automatically.
- How active is the maintainer team? A solo-maintained project with limited bandwidth benefits more from stale management than one with a full-time team that triages daily.
- Do you have reliable label exemptions? If you can exempt
bugorconfirmedlabels from the stale policy, you can protect high-signal items while still cleaning up noise. - What does your contributor community expect? Some communities accept automated stale management as a normal part of open-source hygiene. Others react negatively to it.
A common middle ground is to use very long timers (360 days for stale, 30 days before close), exempt bug and confirmed labels, and use a stale message that clearly explains the policy and invites contributors to re-engage. This captures most of the cleanup benefit while minimizing the risk of losing genuinely relevant reports.
Setting Up the Workflow¶
Create a workflow file at .github/workflows/stale.yml in your repository:
name: Stale
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
stale:
name: Stale
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Manage Stale Issues and Pull Requests
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 360
days-before-close: 30
exempt-issue-labels: needs-triage
exempt-pr-labels: needs-review
remove-stale-when-updated: true
delete-branch: false
stale-issue-label: stale
stale-issue-message: >
Marking this issue as stale due to inactivity. This helps us focus
on the active issues. If this issue receives no comments in the next
30 days it will automatically be closed.
If this issue was automatically closed and you feel this issue
should be reopened, we encourage creating a new issue linking back
to this one for added context.
Thank you!
stale-pr-label: stale
stale-pr-message: >
Marking this pull request as stale due to inactivity. This helps us
focus on the active pull requests. If this pull request receives no
comments in the next 30 days it will automatically be closed.
If this pull request was automatically closed and you feel this pull
request should be reopened, we encourage creating a new pull request
linking back to this one for added context.
Thank you!
A few things worth noting about this workflow:
- Permissions: The workflow declares
permissions: contents: readat the top level, following least-privilege practice. The job itself elevates toissues: writeandpull-requests: write, which is the minimum required for the action to label and close items. - Pinned SHA: The action is pinned to a full commit SHA rather than a tag. See Why You Should Pin GitHub Actions to Commit Hashes for why this matters.
- Schedule:
0 0 * * *runs the action once daily at midnight UTC. You can adjust this to match your preferred cadence, but daily is sufficient for most projects.
Key Configuration Options¶
actions/stale exposes a large number of inputs. The ones most commonly used:
| Input | Default | Description |
|---|---|---|
days-before-stale | 60 | Days of inactivity before marking an item stale |
days-before-close | 7 | Days after the stale label is applied before closing |
days-before-issue-stale | — | Issue-specific override for days-before-stale |
days-before-pr-stale | — | PR-specific override for days-before-stale |
days-before-issue-close | — | Issue-specific override for days-before-close |
days-before-pr-close | — | PR-specific override for days-before-close |
stale-issue-label | "Stale" | Label applied to stale issues |
stale-pr-label | "Stale" | Label applied to stale pull requests |
stale-issue-message | — | Comment posted when an issue is marked stale |
stale-pr-message | — | Comment posted when a PR is marked stale |
close-issue-message | — | Comment posted when a stale issue is closed |
close-pr-message | — | Comment posted when a stale PR is closed |
exempt-issue-labels | — | Comma-separated labels that prevent an issue from being marked stale |
exempt-pr-labels | — | Comma-separated labels that prevent a PR from being marked stale |
remove-stale-when-updated | true | Remove the stale label when activity resumes |
delete-branch | false | Delete the branch when a stale PR is closed |
operations-per-run | 30 | Maximum number of operations per run (API rate limit control) |
For the full reference, see the actions/stale documentation.
Using Separate Timers for Issues and Pull Requests¶
Issues and pull requests often warrant different staleness policies. A bug report that went quiet for a year may still be worth investigating. A pull request that went quiet after review feedback was posted is much less likely to be resurrected.
You can use the issue- and PR-specific inputs to apply different timers to each:
- name: Manage Stale Issues and Pull Requests
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-issue-stale: 365
days-before-issue-close: 60
days-before-pr-stale: 180
days-before-pr-close: 30
exempt-issue-labels: bug,confirmed,needs-triage
exempt-pr-labels: needs-review
stale-issue-label: stale
stale-pr-label: stale
In this configuration, issues go stale after a year and close after another 60 days. Pull requests go stale after 180 days and close after 30. The bug and confirmed labels are both exempt from the issue stale policy, so confirmed bugs are never automatically closed regardless of activity.
Protecting High-Signal Issues¶
The most important configuration decision in stale management is which labels to exempt. Without exemptions, the action treats every issue the same way. With them, you can protect the items that matter most.
Common exemption strategies:
- Exempt
bugorconfirmed: Prevents confirmed bugs from being closed, even if the original reporter has gone quiet. - Exempt
help-wantedorgood-first-issue: Keeps contribution opportunities visible regardless of their activity level. - Exempt
needs-triage: Prevents untriaged items from being closed before a maintainer has reviewed them. This is particularly useful if triage is infrequent. - Exempt
pinned: Useful for tracking issues or roadmap items that should stay open indefinitely.
The exempt-issue-labels and exempt-pr-labels inputs accept comma-separated lists:
exempt-issue-labels: bug,confirmed,help-wanted,good-first-issue,needs-triage,pinned
exempt-pr-labels: needs-review,pinned
Writing Effective Stale Messages¶
The stale warning message is what contributors see before their issue or pull request is closed. It's worth writing thoughtfully. A good stale message:
- Explains why the item is being marked stale (inactivity, not merit).
- Tells the contributor what will happen next and when.
- Gives a clear action for re-engaging (add a comment, provide an update).
- Acknowledges the contributor's time and effort.
Avoid messages that feel dismissive or robotic. "This issue has been automatically marked as stale" with no further context is technically correct but leaves contributors wondering whether their report was ever read. Adding a sentence acknowledging the time they took and inviting them to provide an update goes a long way.
Tip
Use the close-issue-message and close-pr-message inputs to post a final comment when an item is closed. This gives contributors a last chance to re-engage and makes the closure feel less abrupt. A good closing message also points contributors toward reopening or creating a new issue if the problem persists.
A Real-World Example¶
This is the configuration I use in a project. The timers are intentionally long: 360 days before stale, 30 days before close. This gives contributors ample time to respond and reduces the risk of closing issues that are genuinely still relevant.
The needs-triage label exemption for issues and needs-review label exemption for pull requests ensure that items flagged for maintainer attention are never swept up by the policy before someone has a chance to look at them.
name: Stale
on:
schedule:
- cron: "0 0 * * *"
permissions:
contents: read
jobs:
stale:
name: Stale
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Manage Stale Issues and Pull Requests
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 360
days-before-close: 30
exempt-issue-labels: needs-triage
exempt-pr-labels: needs-review
remove-stale-when-updated: true
delete-branch: false
stale-issue-label: stale
stale-issue-message: >
Marking this issue as stale due to inactivity. This helps us focus
on the active issues. If this issue receives no comments in the next
30 days it'll automatically be closed.
If this issue was automatically closed and you feel this issue
should be reopened, we encourage creating a new issue linking back
to this one for added context.
Thank you!
stale-pr-label: stale
stale-pr-message: >
Marking this pull request as stale due to inactivity. This helps us
focus on the active pull requests. If this pull request receives no
comments in the next 30 days it'll automatically be closed.
If this pull request was automatically closed and you feel this pull
request should be reopened, we encourage creating a new pull request
linking back to this one for added context.
Thank you!
Closing Thoughts¶
Stale management is a tool, not a policy. The right configuration depends on the size of your project, your capacity as a maintainer, and the expectations of your contributor community. Used thoughtfully, it keeps the issue tracker navigable and reduces the overhead of managing abandoned pull requests. Used carelessly, it closes legitimate bugs and discourages contributors.
The safest starting point is conservative: long timers, exemptions for bug labels and confirmed issues, and clear, considerate stale messages. You can always tighten the policy later if the tracker grows unmanageable. It's harder to rebuild contributor trust once it has been lost to an aggressive close policy.