# Issue Triage

> Build and run a system for handling incoming issues. Use when an issue backlog has become unmanageable, when setting up issue templates or a label taxonomy, when deciding what to close, when configuring stale bots or automation, or when the user asks "how do I deal with all these issues". Covers issue forms, a minimal label set, triage rotation, converting issues to discussions, writing closing messages that don't burn people, duplicate handling, and turning recurring issues into documentation or code changes.

- Skill: `the-open-agent/issue-triage` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add the-open-agent/issue-triage`
- Raw SKILL.md: https://api.skillmd.com/api/skills/the-open-agent/issue-triage/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Product & Planning
- Author: the-open-agent (https://skillmd.com/u/the-open-agent)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/the-open-agent/issue-triage

---


# 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".

```yaml
# .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:

```yaml
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:

1. **Duplicate?** Link to the original and close. Comment on the original so the
   reporter is subscribed to the real thread.
2. **A question?** Convert to a Discussion (GitHub does this natively, preserving
   the thread and the author).
3. **Bug without a reproduction?** Label `needs-repro`, ask for specifics. If nothing
   arrives in 30 days, close with an explicit invitation to reopen.
4. **Reproducible bug?** Label, add to a milestone if you will fix it, or `help wanted`
   if you will not.
5. **Feature request?** Decide: accept (milestone), decline (close, with the reason),
   or defer (`needs-decision` and say when you will decide). *Deciding slowly is the
   expensive option* — the reporter and any would-be implementer are both blocked.
6. **Already fixed on main?** Say which release will contain it, and close.

```bash
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 to `CONTRIBUTING.md`.
- **Auto-close `needs-repro` after 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.

```markdown
# 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:

1. Announce it in a pinned issue: what you are doing and why.
2. 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.
3. Set the criteria you will maintain going forward, and publish them.
4. **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.**
- **`p0` labels 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 issue` on something that needs architecture context.**

