An Issue is a numbered, stateful record attached to a repository. It has a title, a body, a state of open or closed, and whatever metadata you attach to it.
The important word is stateful. An Issue is designed to close. That single property is what distinguishes it from every other conversation surface on GitHub, and it is the basis of every good decision about when to use one.
The test for whether something is an Issue
Section titled “The test for whether something is an Issue”Ask: what would have to be true for this to close?
If you can answer — a bug stops reproducing, a feature ships, a document is written — it is an Issue. If you cannot, it is a conversation, and belongs in Discussions.
This matters because an Issue tracker is only useful as a signal if the open count means something. A repository with 400 open Issues where half are open-ended discussions has no usable signal: nobody can tell whether the project has 400 outstanding problems or 200. The tracker stops being a work queue and becomes an archive.
Anatomy of an Issue
Section titled “Anatomy of an Issue”Number. Assigned per repository, shared with pull requests — a repository does not have both an Issue #12 and a pull request #12. This is a hint about how GitHub models them internally, which matters when you use the API.
Title. Written for someone scanning a list of 200. “Crash” is useless; “Client crashes on reconnect when the server closes mid-handshake” is scannable.
Body. Markdown, with task lists, code blocks and references. For bugs, the body should let someone reproduce without asking you anything.
State. Open or closed. Closing also records how — completed, or not planned — which is a small distinction that makes reporting much more honest, because “we fixed 40 Issues” and “we closed 40 Issues” are different claims.
Assignees. Who is doing it. Assigning more than one person usually means nobody is doing it.
Labels. Free-form tags. The main axis of triage.
Milestone. A named grouping with an optional due date, typically a release.
Labels that earn their place
Section titled “Labels that earn their place”Labels are the most-used and most-abused Issue feature. A repository with sixty labels has no labelling scheme; it has sixty synonyms that nobody applies consistently.
A workable scheme uses a small number of orthogonal axes:
| Axis | Example labels | Answers |
|---|---|---|
| Type | bug, feature, docs, chore | What kind of work is this? |
| Priority | p0, p1, p2 | When does it need doing? |
| Status | needs-repro, blocked, ready | What is stopping it? |
| Area | area/api, area/ui | Which part of the codebase? |
Orthogonal matters: every Issue should take at most one label from each axis. When labels overlap in meaning, people apply them inconsistently and filtering stops working.
Two special labels are worth adopting deliberately. good first issue and help wanted are
recognised by GitHub and surfaced in contributor-facing discovery. Applying them accurately is one of
the highest-leverage things a maintainer can do for a project’s contributor pipeline — and applying
them to something that is neither is a good way to waste a newcomer’s evening.
Manage them from the terminal:
gh label listgh label create "needs-repro" --description "Cannot reproduce from the report as written" --color FBCA04gh label clone owner/other-repogh label clone is genuinely useful: it copies a label scheme you have refined into a new
repository, so every project does not start from GitHub’s defaults.
Linking Issues to code
Section titled “Linking Issues to code”This is where Issues stop being a to-do list and become part of the engineering record.
Closing keywords. A pull request whose description contains Closes #42 — or Fixes, or
Resolves — automatically closes Issue 42 when the pull request merges, and links the two
permanently.
Closes #42Fixes #17, closes #18Two details people get wrong. The keyword must be in the pull request description or a commit
message, not in a review comment. And to close an Issue in a different repository the reference
must be fully qualified: Closes owner/repo#42.
Plain references. Mentioning #42 anywhere creates a visible cross-link without closing
anything. Useful for “related to” without asserting that this work finishes it.
Commit references. Mentioning an Issue number in a commit message links the commit to the Issue. This is how, months later, you find the commit that fixed a bug by starting from the bug report.
Templates and forms
Section titled “Templates and forms”Left to themselves, bug reports arrive as “it doesn’t work”. Templates fix this by supplying structure at creation time.
Markdown templates live in .github/ISSUE_TEMPLATE/ and pre-fill the body. Simple, and easy for
a reporter to delete.
Issue forms are YAML in the same directory and render actual form fields — dropdowns, checkboxes, required inputs. Because fields can be required, forms produce far more consistent reports.
name: Bug reportdescription: Something is not working as documentedlabels: ["bug", "needs-repro"]body: - type: textarea id: what-happened attributes: label: What happened? description: What did you expect, and what happened instead? validations: required: true - type: input id: version attributes: label: Version placeholder: "1.4.2" validations: required: true - type: dropdown id: os attributes: label: Operating system options: [Linux, macOS, Windows] validations: required: trueA config.yml in the same directory can add links to other destinations — a Discussions category
for questions, a security policy for vulnerabilities — and optionally disable blank Issues entirely.
Routing questions away from the tracker before they are filed is more effective than closing them
afterwards.
Searching and filtering
Section titled “Searching and filtering”The search syntax is the difference between a tracker you can use and a list you scroll.
is:issue is:open label:bug no:assigneeis:issue is:open assignee:@me sort:updated-descis:issue is:closed closed:>2026-08-01is:issue is:open -label:needs-repro created:<2026-01-01The same qualifiers work from the CLI, which is where they become scriptable:
gh issue list --state open --label bug --search "no:assignee" \ --json number,title,createdAt --limit 50What it doesLists open Issues labelled `bug` with no assignee, as JSON, showing number, title and age.
Why we run itThis is the triage query — unowned bugs are the ones that quietly rot. Structured output means it can feed a report rather than being read by eye.
Expected resultA JSON array of Issue objects with the three requested fields.
Three queries worth running regularly on any repository you maintain: open bugs with no assignee,
Issues not updated in ninety days, and Issues labelled needs-repro older than a month. Each
identifies a different kind of decay.
Working with Issues from the terminal
Section titled “Working with Issues from the terminal”The full command family is covered in gh issue; the essentials:
gh issue create --title "Retry logic drops the final attempt" --body-file report.md --label buggh issue list --state open --limit 20gh issue view 42 --commentsgh issue edit 42 --add-label p1 --add-assignee @me --milestone "v2.1"gh issue close 42 --reason completedgh issue develop 42 --checkoutgh issue develop is the least-known and most useful of these: it creates a branch linked to the
Issue and checks it out, so the connection between the work and its record exists from the first
commit rather than being remembered at pull request time.
Issues and Projects
Section titled “Issues and Projects”Issues can be added to Projects, which provide boards, custom fields, roadmaps and cross-repository views.
The division of responsibility is worth stating clearly, because conflating them is common: the Issue is the record of the work and lives with the repository. The Project is a view over many Issues, used for planning. An Issue is complete without a Project; a Project is just a lens.
Planning tooling is a subject of its own and is not covered further here — this lesson deliberately stops at the boundary, because “how should we run our sprint” is not a question about GitHub Issues.
Writing an Issue someone can act on
Section titled “Writing an Issue someone can act on”Most Issues that stall do so because they cannot be acted on, not because nobody cares.
A bug report needs four things, and it is worth stating them explicitly because reporters routinely supply two:
What you did. Precise enough to repeat. “Called the client with a 4KB payload against a server that closes the connection after the headers” — not “used the client”.
What you expected. Often omitted because it feels obvious to the reporter and is not to anyone else.
What happened instead. The actual output, error or behaviour. Paste it; do not describe it.
Your environment. Version, operating system, and anything unusual about the setup. Most irreproducible bugs are environment differences nobody wrote down.
A feature request has a different shape. The useful version describes the problem, not the solution:
I need to run the client behind a proxy that requires authentication. Currently there is no way to supply credentials, so I am patching
session.pylocally.
That invites a design conversation. “Add a proxy_auth parameter” pre-commits to one answer and
often the wrong one — the maintainer may know a better mechanism, or that one already exists.
Reading an Issue tracker’s health
Section titled “Reading an Issue tracker’s health”A few numbers tell you a great deal about how a project is run, and all are visible without asking anyone.
Open-to-closed ratio over time. Steadily rising open count means intake exceeds capacity. That is not a criticism — it is the normal state of a popular unpaid project — but it tells you how long your report is likely to wait.
Age of the oldest open Issue. Something open for four years is either genuinely hard or the tracker is an archive.
Time to first response. More predictive of a good experience than time to close. A project that replies within a day and fixes in a month is more pleasant than one that fixes in a week after three weeks of silence.
Proportion with no label. Unlabelled Issues are untriaged. A tracker that is mostly unlabelled is one nobody is reading.
gh issue list --state open --search "no:label" --json number --jq 'length'gh issue list --state closed --limit 100 --json closedAt,createdAt \ --jq '[.[] | ((.closedAt | fromdateiso8601) - (.createdAt | fromdateiso8601)) / 86400] | add / length | floor'The second computes mean days-to-close over the last hundred. It is crude — it ignores the ones still open, which are the slow ones — and it is still more informative than the raw open count.
Closing well
Section titled “Closing well”Closing is a communication act, and doing it carelessly is how projects acquire a reputation for being unwelcoming.
Say why. “Fixed in #57” or “not planned — this conflicts with the design in #12” takes ten seconds and leaves a record.
Use the reason field. completed and not_planned are different claims, and reporting that
conflates them overstates how much was fixed.
Do not close silently. An Issue that disappears with no comment reads as dismissal, whatever was intended.
Closing is not rejection. A tracker where everything stays open to avoid seeming unwelcoming becomes useless, and an honest “not planned” respects the reporter’s time more than five years of ambiguity.
Common mistakes
Section titled “Common mistakes”Using Issues for open-ended discussion. They never close, and the open count stops meaning anything.
Titles written for the author. “Bug in the thing” is not findable in six months.
Sixty labels. A scheme nobody can apply consistently is not a scheme.
Not linking pull requests to Issues. Loses the archaeology when it is most needed.
Assigning three people. Diffuses responsibility; nobody owns it.
Closing without a reason. “Completed” and “not planned” tell very different stories later.
Leaving Issues open forever out of politeness. An honest “not planned” respects the reporter more than five years of silence.
Exercise
Section titled “Exercise”Use the repository you created in the cluster exercise.
- Create three labels forming one axis —
bug,feature,docs. - Open an Issue with
gh issue create, describing something genuinely fixable. - Run
gh issue develop <number> --checkoutand confirm you are on a new linked branch. - Make a commit and push, then open a pull request whose body contains
Closes #<number>. - Merge the pull request and confirm the Issue closed automatically.
- Run
gh issue list --state closed --json number,title,closedAtand observe the record.
Step 5 is the one to watch closely — seeing the Issue close by itself makes the link between the two systems concrete in a way that reading about it does not.
What you learned
Section titled “What you learned”- An Issue is a stateful record designed to close; if it cannot close, it is a Discussion.
- Issues and pull requests share one number sequence per repository.
- A small orthogonal label scheme is usable; a large one is not.
- Closing keywords in a pull request body link and close automatically, and cross-repository references must be fully qualified.
- Issue forms enforce required fields, which produces reports you can act on.
- Search qualifiers work identically in the web interface and the CLI, which makes triage scriptable.