How to Write a Changelog: Format, Examples and Keep a Changelog
A changelog is a curated list of notable changes, newest first, grouped by kind. The Keep a Changelog format, how Semantic Versioning fits, what generated changelogs get right and wrong, and a good and a bad example side by side.
7 min read
A good changelog is a single file, usually CHANGELOG.md, that lists the notable changes in each version of your project, newest version first, each with its release date, and with changes grouped under a small set of headings: Added, Changed, Deprecated, Removed, Fixed and Security. It is written by a person for people, describes each change in a sentence someone outside the team can follow, and keeps an Unreleased section at the top for what is merged but not yet shipped. That is the Keep a Changelog convention, and it is the one most projects follow. The rest of this article covers the format in detail, how version numbers fit, where automation helps and where it does not, and a good and a bad example.
Changelog or release notes?
They overlap, but they are written for different readers. A changelog is the complete running record, read mostly by developers and people who integrate with you, and it includes every notable change, deprecations and removals included. Release notes are selective and written for a particular audience at a particular release, usually customers, in terms of what they can now do. Many teams keep both, writing the release notes from the changelog. If you need the customer-facing version, the release notes template covers it; this article is about the changelog.
The Keep a Changelog format
Keep a Changelog (opens in a new tab) defines a changelog as a file containing a curated, chronologically ordered list of notable changes for each version of a project, and states plainly that changelogs are for humans, not machines. Its guiding principles are short:
- There is an entry for every single version.
- The same types of changes are grouped together.
- Versions and sections can be linked to.
- The latest version comes first.
- Each version shows its release date.
- The changelog says whether the project follows Semantic Versioning.
Within each version, changes go under one of six headings:
Addedfor new features.Changedfor changes in existing functionality.Deprecatedfor features that will be removed soon.Removedfor features removed in this version.Fixedfor bug fixes.Securityfor fixes to vulnerabilities.
Two details make the file more useful than a list. Dates are written in ISO 8601 form, 2026-09-28, year then month then day, so nobody has to guess whether 03/04 is March or April. And an Unreleased section at the top collects changes as they are merged, so that on release day you rename it to the new version and date rather than writing the entry from memory. A release you withdraw because of a serious problem stays in the file, marked [YANKED], so nobody wonders where it went.
# Changelog All notable changes to this project are documented in this file. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning. ## [Unreleased] ### Added - Export any report as CSV from the report menu. ## [2.4.0] - 2026-09-28 ### Added - Invoices can be sent in the customer's own currency. ### Changed - Search results now show 50 items per page instead of 20. ### Deprecated - The `customer_name` field; use `customer.display_name`. It will be removed in 3.0.0. ### Fixed - Invoices with more than 50 lines no longer lose their last line when exported to PDF. ## [2.3.1] - 2026-09-12 ### Security - Password reset links now expire after one hour.
Version numbers: Semantic Versioning
The changelog records what changed; the version number tells readers how much it matters to them. Semantic Versioning (opens in a new tab) uses three numbers, MAJOR.MINOR.PATCH: increase MAJOR when you make incompatible API changes, MINOR when you add functionality in a backward-compatible way, and PATCH for backward-compatible bug fixes. Versions below 1.0.0 are for initial development, when anything may change; 1.0.0 is the point at which you declare a public API that the numbering then protects.
The headings and the numbers line up usefully. An entry under Removed, or a breaking change under Changed, means a major version. Added without breaking anything means a minor one. A release with only Fixed and Security entries is a patch. If your changelog and your version number disagree, one of them is wrong. And if you do ship a breaking change in a minor release by mistake, the specification’s advice is to release a new minor version that restores backward compatibility, and to document the version that broke it, which is exactly what a changelog entry is for.
Generated changelogs, and their limits
The most common way to automate a changelog is to write commit messages in a fixed shape. Conventional Commits (opens in a new tab) is a specification for that shape: type(scope): description, where fix corresponds to a patch release, feat to a minor one, and a ! after the type or a BREAKING CHANGE: footer to a major one. Other types such as docs, refactor and chore are allowed. Because the shape is fixed, tools can read the history, choose the next version number and write a changelog draft; the specification’s own site lists several such tools.
feat(invoices): send invoices in the customer's currency fix(export): keep the last line of long invoices in PDFs feat(api)!: return 50 orders per page instead of 100 BREAKING CHANGE: clients must follow the next link for more results.
Hosting platforms offer a lighter version. GitHub’s automatically generated release notes (opens in a new tab) list the merged pull requests, the contributors and a link to the full changelog, and a .github/release.yml file can sort pull requests into categories by label and exclude some altogether.
Both are good at completeness and bad at judgement. A commit message is written for a reviewer at the moment of the change, not for someone reading the release six months later, and Keep a Changelog names commit log diffs as the first bad practice for that reason: they are full of noise. Generated output also misses what no commit says, such as a deprecation that was decided in a meeting, or a fix that took five commits to land. Use generation for the first draft, then edit: merge related lines, cut internal changes nobody outside the team will notice, rewrite each remaining line from the reader’s side, and check the version number against the headings.
A good and a bad changelog entry
The same release, written twice. The bad version is roughly what a raw commit log produces:
## v2.4 - fix bug - Merge branch 'feature/INV-332' - refactor currency service - update deps - WIP pdf export - fixed the thing Sam mentioned - change page size
## [2.4.0] - 2026-09-28 ### Added - Invoices can be sent in the customer's own currency. ### Changed - Search results show 50 items per page instead of 20. ### Fixed - Invoices with more than 50 lines no longer lose their last line when exported to PDF.
The bad version has no date and an ambiguous version number, mixes internal work with changes users will notice, uses words only the team understands, and gives no way to tell whether upgrading is safe. The good version has a date, groups the changes, drops the refactor and the dependency update because nobody outside will notice them, and says each change in terms of what is different for the reader.
Changelog best practices, in one list
- Keep it in the repository, next to the code, as
CHANGELOG.md, and update the Unreleased section in the same change as the code, not on release day. - Write one line per change, in plain words, from the point of view of someone using the project.
- Describe fixes by the symptom that no longer happens, not by the cause you found.
- Give every deprecation its replacement and the version in which the old thing will be removed.
- Never leave out a breaking change or a security fix. A changelog that mentions only some changes is worse than none, because readers trust it.
- Leave out refactors, dependency bumps and build changes unless they change behaviour someone can see.
- Use ISO dates, newest version first, and link each version heading to the diff where your hosting allows it.
- Have someone other than the author read the entry before a release, the same way someone reviews the code.
Drafting a changelog from finished tasks
Commits are one raw material; finished tasks are often a better one, because a task title is written for whoever asked for the work. On a fenbs board every task is a feature, an enhancement or a bug, which sorts neatly under Added, Changed and Fixed. When a task reaches Completed the board records how it ended, as Completed, Won’t fix, Duplicate, Cannot reproduce or Obsolete, and only the first belongs in a changelog. Each task also carries a test status and test notes, so anything not tested is a question to answer before it is listed.
fenbs does not write a changelog for you and has no release or version field. What it has is a Copy as Markdown button below the board, which copies every lane as a Markdown list in the order each lane is showing, with each task’s reference, priority, title, kind and note. Sort Completed by newest, copy, keep the tasks finished since your last release, and you have a list to edit into the six headings. It does not include how a task ended, so strike out anything closed as Won’t fix or Duplicate by hand. If an AI assistant is connected to the board, it can do that drafting for you and a person checks every line; the method and the prompt are in AI release notes from finished tasks.
Related
The customer-facing version: release notes template. Sorting work so it maps onto a changelog: features, enhancements and bugs. What counts as finished before it is listed: definition of done examples.