Issue Triage
An untriaged backlog is not a to-do list — it is a public record of requests you are ignoring. Every issue is either actionable, waiting on someone, or should be closed. There is no fourth state, and "open forever" is the one that quietly costs you contributors.
Issue forms beat templates
.yml forms produce structured, complete reports. Markdown templates get deleted by
the reporter and produce "it doesn't work".
# .github/ISSUE_TEMPLATE/bug_report.yml
name: Bug report
description: Something isn't working
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for the report. Please check [existing issues](../issues?q=is%3Aissue) first.
- type: input
id: version
attributes: { label: Version, placeholder: "4.2.0" }
validations: { required: true }
- type: textarea
id: repro
attributes:
label: Reproduction
description: Minimal steps or a link to a minimal repo. This is the field that determines whether we can help.
validations: { required: true }
- type: textarea
id: expected
attributes: { label: Expected vs actual }
validations: { required: true }
- type: input
id: env
attributes: { label: OS and runtime, placeholder: "macOS 15.3, Node 22.11" }
Also add .github/ISSUE_TEMPLATE/config.yml to route non-bugs away from the tracker:
blank_issues_enabled: false
contact_links:
- name: Question or help
url: https://github.com/OWNER/REPO/discussions/categories/q-a
about: Ask in Discussions — questions here will be converted.
- name: Security vulnerability
url: https://github.com/OWNER/REPO/security/advisories/new
about: Report privately. Do not open a public issue.
Keep forms short. Every required field costs you some fraction of reports, and a bug you never hear about is worse than one reported imperfectly. Three fields that matter beat nine that produce abandonment.
A label set you will actually maintain
Most projects have 40 labels and use six. Start minimal and add only on demonstrated need.
| Group | Labels | Purpose |
|---|---|---|
| Type | bug feature docs question chore |
What kind of work |
| State | needs-triage needs-repro needs-decision blocked |
Whose turn it is |
| Priority | p0-critical p1 p2 |
Only if you honor it |
| Contributor | good first issue help wanted |
Discoverability — GitHub surfaces these |
| Resolution | wontfix duplicate stale |
Why it closed |
Rules: every open issue carries exactly one state label, so a query tells you
whose turn it is. Only use priority labels if you triage by them — a p0 sitting for
six months is a lie in public.
good first issue is the highest-value label in the set, and only if the issues under
it are real: a specific, self-contained task with a pointer to the relevant file and
an expected approach. A good first issue that turns out to require architectural
context is a broken promise to a newcomer (see contributor-experience).
The triage pass
Do this on a schedule — 20 minutes, twice a week, beats a four-hour purge once a
quarter. For each needs-triage issue:
- Duplicate? Link to the original and close. Comment on the original so the reporter is subscribed to the real thread.
- A question? Convert to a Discussion (GitHub does this natively, preserving the thread and the author).
- Bug without a reproduction? Label
needs-repro, ask for specifics. If nothing arrives in 30 days, close with an explicit invitation to reopen. - Reproducible bug? Label, add to a milestone if you will fix it, or
help wantedif you will not. - Feature request? Decide: accept (milestone), decline (close, with the reason),
or defer (
needs-decisionand say when you will decide). Deciding slowly is the expensive option — the reporter and any would-be implementer are both blocked. - Already fixed on main? Say which release will contain it, and close.
gh issue list --label needs-triage --limit 50
gh issue list --search "is:open no:label" --limit 50 # slipped through
gh issue list --search "is:open updated:<2025-08-01" --limit 50 # decayed
gh issue list --search "is:open label:\"good first issue\" no:assignee"
Automation worth having
- Auto-label from the form (
labels:in the form frontmatter). - Comment on first-time contributors (
actions/first-interaction) — a warm, short greeting with a link toCONTRIBUTING.md. - Auto-close
needs-reproafter 30 days of silence, with a message that clearly invites reopening. - Duplicate detection on similar titles — as a suggestion for the triager, not an auto-close.
Automation not worth having:
- A stale bot on everything. Closing valid bugs because nobody had time is a
message to your users: your report was noise. It also destroys the search value of
your tracker. If you must run one, exclude
bug,security,help wanted, and anything with a reproduction, and set the window to months not weeks. - Auto-closing PRs. Never. See
code-review. - Templates so strict that reporting a typo requires a version number and an OS.
Closing messages that don't burn people
Closing is a normal, healthy operation. How you do it determines whether that person files a second issue.
# Duplicate
Thanks — this is the same underlying issue as #412, so I'll close here and keep the
discussion there. I've linked your reproduction, which is more complete than the
original; it'll be useful when we fix it.
# Out of scope
Thanks for the suggestion. I'm going to pass on this one: it would mean owning
<X>, and keeping this project focused on <Y> is a deliberate constraint (noted in
the README under Scope). A plugin could do this using the `onParse` hook — happy to
help if you build one.
# Can't reproduce
I haven't been able to reproduce this on <versions tried> — steps I took: <...>.
Closing for now, but please reopen with a minimal repo or the exact input and I'll
take another look. Not a dismissal; I just don't have enough to work with yet.
# Won't fix, deliberate behavior
This is intentional: <reason>. I can see why it's surprising, so I've opened #520 to
document it. You can get the behavior you want with <workaround>.
Common structure: thank, give a specific reason, take responsibility for anything that was your project's fault, offer a next step. Never close silently, and never close with just a label.
Backlog bankruptcy
If the backlog is past a few hundred stale issues and triage feels impossible, a declared reset is legitimate — but do it honestly, once:
- Announce it in a pinned issue: what you are doing and why.
- Close everything untouched for over a year with a clear message: this is a capacity decision, not a judgment on the report, and reopening is one comment away.
- Set the criteria you will maintain going forward, and publish them.
- Then actually maintain them. A second bankruptcy declaration means the process is the problem, not the backlog.
The real point of triage
Triage is not queue management; it is detecting patterns. Five issues about the same confusing flag is not five issues — it is one design or documentation bug.
- Three reports of the same misunderstanding → a docs page (
docs-architecture) - Repeated confusion about an API → the API is wrong (
api-design) - The same error message misread repeatedly → rewrite the error message
- Frequent "how do I..." → a how-to guide, and a link from the error output
Every issue you prevent is worth ten you close.
Anti-patterns
- No triage. A backlog nobody reads is a signal that the project is unmaintained.
- Stale-botting valid bugs.
- Closing without a reason.
p0labels on issues that sit for months.- 40 labels, 6 in use.
- Answering the same question 20 times instead of writing the page once.
- Letting a feature request sit undecided for a year. Say no faster; it is kinder.
- Arguing with an angry reporter. Answer the technical content, ignore the tone,
and enforce the Code of Conduct if it crosses a line (see
governance-and-sustainability). good first issueon something that needs architecture context.