A pull request description template, with filled examples
A copy-paste PR description template with filled feature, bug fix and refactor examples, plus how to set it up in GitHub, including multiple templates.
The KnowWhyy team · · 7 min read
Most pull request descriptions are written in the thirty seconds between pushing a branch and asking for review. "Fixes the thing." "See ticket." "Refactor." The reviewer opens the diff with no idea what problem it solves, and a year later someone runs git blame, lands on the PR and learns nothing they couldn't see in the code.
A good template fixes both problems at once. This post gives you one you can copy, three filled examples (a feature, a bug fix and a refactor), and the exact GitHub setup, including multiple templates and making sure the description ends up in your commit history.
Who reads a PR description
A PR description has two audiences, and they need slightly different things.
The reviewer, today. They need to know what you were trying to do before they can judge whether the code does it. Google's code review guide says a reviewer should start with the description and ask whether the change makes sense at all before reading any files. Without a description, they reverse-engineer your intent from the diff, which is slow and error prone.
The future reader, months or years from now. Google's guide to writing good CL descriptions calls the description "a public record of change." It becomes part of version control history, people will search for it, and the code alone won't tell them why the change exists. The same guide points out that links can rot or sit behind access restrictions, so the description should carry enough context on its own.
We covered the second reader in why was this code written?, which suggests adding a short "why" line to your PR template. This post goes further: a full template, what each section is for, and examples of it filled in properly.
Ask your first question in minutes.
Create a workspace, connect a tool and choose what gets indexed. The free plan includes GitHub, Slack and Notion.
The "Why" and "Alternatives considered" sections matter most for the future reader, because they hold information the code can't. A reviewer can see how the code works by reading it. Nobody can see what you decided not to do.
The template
Copy this into .github/pull_request_template.md. The HTML comments are prompts for the author and don't show in the rendered description.
## Summary
<!-- One or two sentences: what this PR changes. -->
## Why
<!-- The problem, constraint or decision this change follows from.
If a reader only reads one section, it should be this one. -->
## Alternatives considered
<!-- What you didn't do, and why. Write "None" if it was obvious. -->
## How it works
<!-- Optional for small PRs. Where should the reviewer start?
Anything non-obvious about the approach? -->
## Testing
<!-- How you verified it: tests added, manual steps, screenshots. -->
## Risk and rollout
<!-- What could break? Feature flag, migration, config change?
How do we roll back? -->
## Links
<!-- Issue, Slack thread, design doc, incident. Summarize, don't just link. -->
Closes #
Keep it about this size. Every section you add is one more that people will delete or fill with "N/A". If a section doesn't apply, a one-word answer like "None" is fine. An empty heading is not, because the reader can't tell whether you skipped it or thought about it.
Example 1: a feature
## Summary
Add CSV export to the invoices list, limited to the current filter.
## Why
Finance exports invoices every month for reconciliation and currently
copies them out of the UI page by page. Support gets a request for this
roughly every week (see #412). We scoped it to the current filter so the
export matches what the user sees.
## Alternatives considered
- Scheduled email reports: more work, and finance asked for on-demand.
- Exporting all invoices regardless of filter: large accounts have
enough invoices that the request would time out. Filtered export keeps
the query bounded by the same limits as the list view.
## How it works
Start with `InvoiceExportController`. It reuses the list query builder,
streams rows to the response, and caps exports at the list view's
existing maximum. The button is behind the `invoice_csv_export` flag.
## Testing
- Unit tests for column formatting (currency, dates, empty fields).
- Integration test: filtered export returns only matching rows.
- Manually exported 2,000 rows on staging and opened the file in
Excel and Google Sheets.
## Risk and rollout
Read-only. Ships behind a flag; we'll enable it for internal accounts
first. Rollback is turning the flag off.
## Links
Closes #412. Discussion with finance in #billing (summary above).
Notice what the "Why" does. It names who needs this, what they do today and why the scope is what it is. A reviewer who thinks "why not export everything?" finds the answer before asking.
Example 2: a bug fix
For bugs, the most useful thing you can write down is the root cause. "Fix bug" is the classic bad description, and Google's guide uses it as its first example of an inadequate one.
## Summary
Stop sending duplicate welcome emails when signup is retried.
## Why
Users on slow connections sometimes double-submit the signup form. Both
requests create the account inside a transaction, but the welcome email
is queued before the transaction commits, so the second request's job
is queued even though its insert fails on the unique email constraint.
Root cause: email job enqueued inside the transaction instead of after
commit.
## Alternatives considered
- Disabling the submit button on click: worth doing too (separate PR),
but it doesn't cover API clients or network retries.
- Deduplicating in the email worker: hides the problem and would need
its own state.
## How it works
Moves the enqueue into an after-commit hook, so a rolled-back signup
never queues an email.
## Testing
- New test: two concurrent signups with the same email produce one
account and one queued email. Fails on main, passes here.
## Risk and rollout
Low. If the commit succeeds but enqueueing fails, the user gets no
welcome email; that is logged and the existing resend flow covers it.
## Links
Fixes #988. Reported in #support with three affected accounts.
The bug fix template earns its keep when the same bug comes back. Whoever investigates next can search the history for "welcome email", read the root cause and see which approaches were already ruled out.
Example 3: a refactor
Refactors are the PRs most likely to get a one-word description, and the ones future readers most often wonder about. Say why now, and state plainly that behavior shouldn't change. Google's guide recommends keeping refactors in separate changes from features and bug fixes, which makes this section easy to write.
## Summary
Extract the pricing rules from OrderService into a PricingPolicy class.
No behavior change.
## Why
OrderService mixes persistence, pricing and notification logic. The
regional pricing work in #1045 needs to swap pricing rules per region,
which is hard while they're spread through OrderService. This PR only
moves code; the regional change will follow in its own PR.
## Alternatives considered
- Adding region checks inside OrderService: faster today, but every
pricing change would keep touching a large class with no direct tests.
## How it works
Pure move plus a constructor parameter. Review commit by commit: the
first commit moves code unchanged, the second wires it in.
## Testing
- Existing OrderService tests pass unchanged.
- Added direct tests for PricingPolicy covering the current rules.
## Risk and rollout
Low. No schema, API or config changes.
## Links
Prepares for #1045.
Setting up the template in GitHub
GitHub fills new pull requests with the template automatically. The details from GitHub's docs:
Location. Name the file pull_request_template.md and put it in the repository root, in docs/, or in the hidden .github/ folder. .github/pull_request_template.md keeps the repository root clean.
Default branch. Templates only work once they're merged into the default branch. A template on a feature branch does nothing.
Filename. The filename isn't case sensitive and can use .md or .txt.
If features, bug fixes and releases really need different sections, create a PULL_REQUEST_TEMPLATE folder (in .github/, the root or docs/) with one file per template:
Issue templates get a chooser page. For pull requests, GitHub's docs describe a different mechanism: you pick a template with the templatequery parameter on the compare URL:
The same URL can also set title, labels, assignees and milestone. In practice, most teams put these links in CONTRIBUTING.md or a shell alias. Because using a specific template takes an extra step, a single default template that covers most PRs usually works better than several that people forget to select.
One template for every repository
You can set a default for every repository in an organization by creating a public repository named .github and adding the template there. GitHub uses it for any repository that doesn't have its own. A repository can still override it with its own file.
Make the description outlive the PR
A great description helps your reviewer. To help the future reader, it needs to end up somewhere they'll look.
Squash-merge with the PR description. In your repository's settings, under "Pull Requests", you can choose the default squash commit message. One option is "pull request title and description". With it, your Why and Alternatives sections land in the commit itself, so git log and git blame show the reasoning even if nobody opens GitHub. See how to write good commit messages for how that fits with commit conventions.
Use closing keywords. Writing Closes #412 or Fixes #988 in the description links the PR to the issue and closes it on merge. GitHub only does this when the PR targets the default branch; on any other branch the keywords are ignored.
Update it before merging. PRs change during review. Google's guide suggests re-reading the description before submitting so it still matches what the change does. If review settled a question, like "we kept the retry at 3 because of the provider's limit", add it to the description. Comments buried in a long review thread are much harder to find later. Our code review checklist covers how to record decisions made in review.
Summarize your links. "See Slack" is a dead end when the channel is archived or the reader isn't in it. Write the one-line conclusion, then link.
Common mistakes
The description repeats the diff. "Changed foo() to call bar()" adds nothing. Write what the diff can't show.
"See ticket" as the whole description. Tickets get moved, closed or migrated to another tool. Copy the essential context in.
The template is too long. If people routinely delete half of it, cut those sections.
Screenshots without words. A before/after screenshot is great for UI changes, but say what changed too, so the PR is searchable.
Huge PRs. No template rescues a 3,000-line PR. Split it, and the description gets easier to write.
Where KnowWhyy fits
Good PR descriptions only help if someone can find the right one later. KnowWhyy indexes the GitHub repositories you choose (commits, pull requests, issues and their discussions, not the source code) along with Slack and Notion. You ask in the web app, for example "why do we enqueue welcome emails after commit?", and the answer cites the PRs and threads it came from. If the sources don't say, it tells you so. See GitHub history search, or start free.
The short version
A PR description serves two readers: the reviewer today and someone running git blame next year.
Use a short template: Summary, Why, Alternatives considered, How it works, Testing, Risk and rollout, Links.
For bugs, write the root cause. For refactors, say why now and that behavior shouldn't change.
Put the template at .github/pull_request_template.md on the default branch; use a PULL_REQUEST_TEMPLATE/ folder and ?template= for multiple templates.
Squash-merge with "pull request title and description" so the reasoning lands in the commit.
Update the description with decisions made in review before you merge.