Conventional Commits Cheat Sheet: Types, Examples, Rules

Every Conventional Commits type with examples, how to pick one, breaking changes, footers, version bumps and changelog sections, and how to enforce it.

By Rahul10 min read
A conventional commit message, feat(inbox): add send later for replies, with its type, scope and description labelled

The short answer

A conventional commit starts with a type, an optional scope in parentheses, an optional ! for a breaking change, a colon and a short description, like feat(inbox): add send later. feat adds a feature (a minor version), fix fixes a bug (a patch), and a ! or a BREAKING CHANGE: footer makes it a major version. Other types such as docs, refactor, test, build, ci and chore don't change the version.

Key takeaways

  • Only the type and description are required; scope, body and footers are optional.
  • feat is a minor release, fix and perf are patches, any breaking change is major.
  • Mark breaking changes with ! and explain the upgrade in a BREAKING CHANGE: footer.
  • If you squash-merge, the pull request title is the commit that counts: lint it.

Conventional Commits is a small set of rules for the first line of a commit message: a type, an optional scope, a colon, and a short description. That's most of it. The parts people look up are the rest: which type a change is, how to mark a breaking change, what goes in the footer, and what each type does to your next version number and your changelog.

This cheat sheet covers all of it on one page, with a type table you can keep open, a decision order for picking a type, worked examples from a made-up product called Acme, and the setup to enforce it in a repository.

The Conventional Commits format

Every commit message follows the same shape:

<type>[optional scope][!]: <description>

[optional body]

[optional footer(s)]

Only the first line is required. A real one:

feat(inbox): add send later for replies

The Conventional Commits 1.0.0 specification sets the rules. The ones that matter day to day:

  • The type comes first, followed by an optional scope in parentheses, an optional !, then a colon and a space.
  • feat must be used for a new feature and fix must be used for a bug fix. Every other type is allowed but not defined by the spec.
  • The description follows the colon and space straight away.
  • The body starts one blank line after the description and can run to several paragraphs.
  • Footers start one blank line after the body. Each is a token, then : or #, then a value, like Refs: #123.
  • A breaking change is marked with ! before the colon, or a BREAKING CHANGE: footer, or both. BREAKING CHANGE must be uppercase; everything else is case-insensitive.
Anatomy of a conventional commit: type, scope, breaking-change marker, description, body and footers, each labelled
The parts of a conventional commit. Only the type and description are required.

Conventional Commits types

The spec defines only feat and fix. The other types most teams use come from the Angular convention, through @commitlint/config-conventional, which allows exactly these eleven: build, chore, ci, docs, feat, fix, perf, refactor, revert, style and test.

TypeUse it when the commitExampleDefault version bump*Angular changelog section*
featAdds a feature users can see or callfeat(inbox): add send later for repliesMinorFeatures
fixFixes a bug users could hitfix(upload): show an error for files over 10 MBPatchBug Fixes
perfMakes something faster without changing behaviourperf(search): cache results by workspacePatchPerformance Improvements
revertReverts an earlier commitrevert: feat(inbox): add send later for repliesPatchReverts
refactorRestructures code without changing behaviourrefactor(auth): split token refresh into its own moduleNoneHidden
docsChanges documentation onlydocs(api): document the scheduled_at fieldNoneHidden
testAdds or fixes tests onlytest(inbox): cover replies scheduled across DSTNoneHidden
styleChanges formatting only (white space, semicolons)style: run prettier on src/inboxNoneHidden
buildChanges the build system or dependenciesbuild(deps): bump pg from 8.11 to 8.12NoneHidden
ciChanges CI configuration or scriptsci: cache node_modules between jobsNoneHidden
choreAnything else that doesn't touch source or testschore: update .gitignoreNoneHidden

\*Bumps are semantic-release's default release rules (feat minor; fix, perf and reverts patch; any breaking change major). Sections are the conventional-changelog Angular preset: types marked hidden are left out of the changelog unless the commit carries a breaking change. Both checked October 4, 2026. Other tools can be configured differently.

Two things in that table catch people out:

  • Only feat, fix, perf and reverts cut a release by default. A pull request that only has refactor, docs and chore commits produces no new version with semantic-release.
  • A breaking change is major whatever its type. refactor!: drop the v1 webhook payload is a major release, even though refactor alone is nothing.

How to choose the right commit type

Most arguments about types are about changes that fit two of them. Ask these questions in order and stop at the first yes:

  1. Does it break something that worked before? Use the type below, and add ! (and a BREAKING CHANGE: footer explaining the upgrade).
  2. Does it fix a bug a user could hit? fix.
  3. Does it add or change something users can see or call? feat.
  4. Is it faster with the same behaviour? perf.
  5. Is it the same behaviour, restructured? refactor.
  6. Only docs? docs. Only tests? test. Only formatting? style.
  7. Only the build or dependencies? build. Only CI? ci.
  8. None of these? chore.

If a commit honestly fits two types, the spec's advice is to split it into two commits. A fix that also adds a feature is two changes, and the changelog reads better when it says so.

A few borderline cases, decided:

ChangeTypeWhy
A dependency bump that fixes a security issue users were exposed tofix(deps)Users get a fix; that's what the changelog should say
A routine dependency bumpbuild(deps)No user-visible effect
Changing a default value users rely onfeat! or fix!It changes behaviour for existing users: breaking
Fixing a typo in UI copyfixUsers see it
Fixing a typo in a code commentstyle or docsUsers don't
Removing a deprecated optionfeat! with a footerBreaking, even if it was announced
Adding logging or metricschore (or feat if users can see them)Depends who reads them

The free conventional commit checker flags messages that break the format; it can't tell you whether you picked the right type, so this is the part worth agreeing on as a team.

Scopes

A scope is a noun in parentheses that names the part of the codebase a commit touches: feat(inbox):, fix(api):, build(deps):. The spec makes it optional and leaves the list to you.

Scopes earn their keep when a changelog or a reviewer needs to group changes, for example by package in a monorepo. Three habits help:

  • Keep a short, fixed list (the product areas or packages) and write it down. inbox, Inbox and inbox-ui scattered across a history make grouping useless.
  • Leave the scope out when a change is truly cross-cutting, rather than inventing all or misc.
  • Don't put issue numbers in the scope. fix(#123): is hard to read and harder to group. Issue references go in the footer.

Breaking changes

There are two ways to mark a breaking change, and you can use both:

feat(api)!: remove the v1 webhook payload
feat(api): send webhooks in the v2 payload

BREAKING CHANGE: webhooks no longer include the v1 `ticket_id` field.
Read `ticket.id` instead. See the migration guide for a full mapping.

The ! is visible in one-line logs and pull request titles, so reviewers notice it. The footer is where the upgrade instructions go. Using both is the clearest: ! to flag it, the footer to explain it. BREAKING-CHANGE: with a hyphen means the same thing as BREAKING CHANGE:.

Write the footer for the person upgrading, not the person who made the change: what stopped working, and the one thing to do about it. Tools like semantic-release and release-please copy that text into the release notes.

Writing the description

The description is the line people read in git log --oneline, in pull request lists and in generated changelogs. The spec only says it's a short summary; these conventions are what config-conventional and most teams add:

  • Imperative mood: "add send later", not "added" or "adds". Read it as "this commit will … add send later".
  • Lowercase first letter, no full stop. config-conventional rejects sentence case, start case, pascal case and upper case, and a trailing ..
  • Keep the whole header short. config-conventional errors over 100 characters; around 72 keeps it readable in most tools.
  • Say what changes, not how. "show an error for files over 10 MB" beats "add size check in upload handler".

Body and footers

The body is free text. Use it for why the change was made and what it replaces, wrapped at a sensible width (config-conventional errors on body lines over 100 characters).

Footers follow the git trailer format: a token, : or #, and a value. Tokens use hyphens instead of spaces, which is how parsers tell a footer from a paragraph. Common ones:

fix(upload): show an error for files over 10 MB

Uploads over the limit used to fail without a message, so people
retried the same file. The limit itself is unchanged.

Fixes #482
Reviewed-by: Dana Lee
Co-authored-by: Sam Ortiz <sam@acme.example>

Refs, Fixes, Closes, Reviewed-by and Co-authored-by are all fine. BREAKING CHANGE is the one token with a space, and the one that changes your version.

Conventional commit examples

Twelve commits from Acme, a made-up help desk, written well and less well:

Weak and strong conventional commit messages compared for a feature, a fix, a breaking change, a dependency bump and a refactor
The same changes, before and after. The strong version names the effect and uses the type the change really is.
feat(inbox): add send later for replies
feat(search): find tickets by customer email domain
fix(upload): show an error for files over 10 MB
fix(inbox): keep drafts when switching tickets
perf(search): cache results by workspace
feat(api)!: remove the v1 webhook payload
revert: feat(search): find tickets by customer email domain
refactor(auth): split token refresh into its own module
docs(api): document the scheduled_at field
test(inbox): cover replies scheduled across DST
build(deps): bump pg from 8.11 to 8.12
ci: run the e2e suite on pull requests only

For a revert, the spec doesn't set the rules. A common pattern is the revert type with the reverted commit's header as the description, and a Refs: footer with the SHA.

From commits to a version number and a changelog

This is what the convention is for. Say Acme's last release was 2.3.0, and these five commits landed since:

feat(inbox): add send later for replies
fix(upload): show an error for files over 10 MB
perf(search): cache results by workspace
refactor(auth): split token refresh into its own module
docs(api): document the scheduled_at field

The highest bump wins: one feat makes it a minor release, so the next version is 2.4.0. Add feat(api)!: remove the v1 webhook payload and it becomes 3.0.0. Remove the feat and it's 2.3.1. You can check a set of commits in the semver calculator.

With the Angular preset, the generated changelog for 2.4.0 looks roughly like this (commit and compare links trimmed):

## 2.4.0 (2026-10-04)

### Features
* **inbox:** add send later for replies

### Bug Fixes
* **upload:** show an error for files over 10 MB

### Performance Improvements
* **search:** cache results by workspace

The refactor and docs commits are dropped, which is right: users can't see them. Our guide to automating your changelog from GitHub compares the tools that do this (release-please, semantic-release, Changesets and git-cliff).

Squash merges and pull request titles

On teams that squash-merge, the commit that lands on main is the one that matters, not the dozen inside the pull request. GitHub's default squash message is the commit's own title and message when a pull request has one commit, and the pull request title plus a list of commits when it has two or more. You can make it always start with the pull request title: in the repository's Settings, under Pull Requests, open the dropdown under Allow squash merging and pick one of the pull request title options.

That gives you a simpler rule: the pull request title is the conventional commit. Contributors can write whatever they like on their branch; the maintainer checks one line before merging. release-please recommends squash merges, and it reads a BEGIN_COMMIT_OVERRIDE block in a merged pull request's body if you need to fix a message after the fact.

How to enforce Conventional Commits

Lint the message where it's written. With commitlint and Husky, from commitlint's own setup guides:

npm install -D @commitlint/cli @commitlint/config-conventional
echo "export default { extends: ['@commitlint/config-conventional'] };" > commitlint.config.js

npm install --save-dev husky
npx husky init
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg

Now a commit like Fixed stuff fails before it's created. If you squash-merge, lint the pull request title in CI as well, since that's the message that lands. To check a message without installing anything, paste it into the conventional commit checker; to draft one from a diff, use the commit message generator.

Common mistakes

  • Everything is chore. Then nothing cuts a release and the changelog is empty. If users can see it, it's feat or fix.
  • Breaking changes without ! or a footer. The tools release it as a minor or patch, and people upgrade into a break.
  • feat for internal work. A new admin script or an internal endpoint isn't a feature for your users, and it bumps the minor version for nothing.
  • Past tense, capitals and full stops. Fixed upload bug. has no type, and fix: Fixed upload bug. still fails config-conventional on the capital letter and the full stop.
  • A different scope per person. Agree on the list once.
  • Treating commits as release notes. That's the next section.

Commit messages aren't release notes

Conventional Commits are written by developers, for the history and the tools. fix(upload): show an error for files over 10 MB is a good commit. It isn't yet a good line for your customers, who want "Uploads over 10 MB now tell you the size limit instead of failing without a message", grouped with the other changes that shipped, and nothing about the auth refactor.

So the generated changelog is a starting point. For a library, it may be enough. For a product with users who never open GitHub, someone still has to turn it into release notes, a changelog people read, and an announcement. The commit types do the sorting; the writing is still a separate job.

How DevRelay uses your commits

DevRelay watches your GitHub repositories and uses commit conventions and labels as one signal for what kind of change each pull request is: a new feature, a fix, a speed-up or a breaking change. When a commit doesn't say, it works the kind out from the pull request and the diff. It waits for the release, since a merge alone isn't treated as shipped, then drafts the changelog entry in New, Improved, Fixed and Changed, an announcement for the release, and X and LinkedIn posts, written for your product's users rather than in commit-speak. Internal work like refactors and dependency bumps is left out.

Every claim in a draft is checked against the pull request and the release before you see it, and nothing goes out until you approve it. You can also ask what shipped and review drafts from Claude, ChatGPT or Cursor through the DevRelay MCP server.

DevRelay inbox showing a draft with each claim checked against the evidence from the release
A draft in the inbox, with each claim traced to the release.

To try it on your own history first, paste your commits into the free release notes generator.

Frequently asked questions

What are the Conventional Commits types?

The specification defines feat and fix. The common full set, from @commitlint/config-conventional, is build, chore, ci, docs, feat, fix, perf, refactor, revert, style and test.

How do you write a breaking change in Conventional Commits?

Add ! before the colon, as in feat(api)!: remove the v1 webhook payload, or add a footer that starts with BREAKING CHANGE: followed by what changed and what to do. Using both is clearest. A breaking change means a major version whatever the commit type.

Is the scope required in a conventional commit?

No. The scope is optional. When you use one, it's a noun in parentheses naming the part of the codebase, like fix(upload):, chosen from a short list your team agrees on.

Should conventional commit descriptions be lowercase?

The specification allows any casing as long as you're consistent. The popular @commitlint/config-conventional rules reject a capitalised first letter and a trailing full stop, so most teams write lowercase, imperative descriptions.

What is the difference between chore and refactor?

refactor restructures source code without changing its behaviour. chore covers other maintenance that doesn't touch source or tests, like editing .gitignore. Neither changes the version on its own.

How do Conventional Commits generate a changelog?

Tools like semantic-release, release-please and conventional-changelog read the types since the last release, pick the next version from the highest bump, and group feat, fix and perf commits into sections. Types users can't see, like refactor and docs, are usually left out.

Your next release deserves more than a merge commit.

Connect GitHub and your website. The next time you ship, the post, the changelog entry and the docs update will be waiting for your yes.

No credit card · Nothing posts without your approval