How to Generate a Changelog With AI From Your Merged Changes
The changelog gets written last, from a list of PR titles someone else wrote for a reviewer who has moved on. Here is how to generate a changelog with AI that records what shipped for the people upgrading — and parks what nobody can stand behind yet.
Build a Changelog PromptThirty-one titles, and none of them written for the reader
The release goes out Thursday. The changelog is due Wednesday, and it lands on whoever has the afternoon. You open the merge list and there are thirty-one pull request titles, most of them written by someone else, all of them written months ago for a reviewer who read them once and moved on.
So the changelog gets written from the titles, because the titles are what is there. And a title is not a description of a change — it is a note one engineer left another about a diff they were both looking at. "Fix nesting" meant something to the two people in that thread. It means nothing to the customer who is about to upgrade and find their export parser broken.
That is the failure this guide is about, and it is not a writing failure. The changelog that ships is usually accurate about the merge list and wrong about the release, because those are different objects and only one of them was in front of the person writing.
What the AI can see, and what it will make up if you let it
Start with the boundary, because this topic invites a specific fantasy: that you point something at the repository and a changelog comes out. Nothing here does that. The AI reads the change records you paste and nothing else. NewPrompt connects to no repository, no Git provider, no issue tracker, no build pipeline. It discovers no commits, no pull requests, no tags, no releases. It cannot tell you what merged, what reached production, or what the version number should be, and NewPrompt publishes nothing — the file in your repo is one you commit yourself.
This matters more than it sounds, because a changelog is a factual claim about the world. Every entry says: this changed, in this release, for you. Hand a model eight records and it will produce a confident, well-formatted, publishable-looking document about all eight — including the one nobody has verified and the one that should never have left the commit history — and it will phrase both in exactly the same register as the facts. The draft is a candidate. The person who can confirm what actually merged is you.
So the useful question is not how to get a changelog out of AI. It is what to feed it, what to cut before you do, and what to refuse to let it write.
Step 1: Decide who reads it, and which window it covers
Two decisions up front, and everything downstream falls out of them. Who is this for, and what does it cover?
The audience is not "users" — it is a specific person with a specific problem. Someone upgrading a dependency and wondering what will break. An integrator whose parser stopped. A support rep working out whether a customer's complaint is a change or a bug. A teammate looking for when a behaviour changed. Those readers want different things, and the changelog can serve the first three at once if you decide that on purpose. It cannot also serve the person deciding whether to buy — that reader gets the announcement, and the moment you write for them, the entries start selling.
The window is the release boundary: which changes belong to this version and which belong to the last one or the next one. Not "everything merged since I last wrote one" — that is a calendar, not a release. If some of the merged work is behind a flag that is off, it did not ship, whatever the merge date says.
- Name the reader in one sentence before you write an entry. "Someone upgrading from 2.5 who integrates with our export" produces different entries than "our users."
- Fix the boundary as a pair of points, not a date range — the last published version and this one.
- Merged is not shipped. Anything behind an off flag, or reverted before the cut, is not in this release.
- If you cannot say which release a change is in, that is the question to answer first — not something to decide while writing.
Step 2: Collect the records, and mark what each one actually proves
Now gather the source material, and gather more of it than the merge list: merged PR summaries, the outcomes of the issues they closed, migration notes, deprecation decisions, security-relevant fixes, and any behaviour change someone flagged in review. The merge list is the skeleton. The issue an entry closed is usually where the user-visible symptom is actually described, because that is where the person who hit it wrote it down.
Then do the part that gets skipped: mark each record with what it proves. A PR title is a claim. The diff is the fact. The issue is a report of a symptom. A review comment saying "this changes the response shape" is a warning nobody has confirmed. These are different grades of evidence and a changelog treats them identically once they are entries, so the grading has to happen before.
Where a title and the change disagree, the title is wrong until someone who touched it says otherwise. This is common and it is not carelessness — a PR grows after it is named. "Fix nesting" ends up also changing a default. The title never got updated because the title had already done its job.
- Pull the closed issue, not just the PR. The symptom in the reader's words is usually already written there.
- Grade each record: what shipped (fact), what the title says (claim), what someone predicted in review (unconfirmed).
- A PR that closes an issue is one change with two records — you will merge them in Step 5, so keep them together now.
- When the record is thin, the answer is a question to its author, not a better guess.
Step 3: Cut everything the reader cannot observe
Most of what merged is invisible from outside, and it belongs in the commit history — where it already is, permanently, at no cost to anyone. The refactor, the dependency bump, the test helper, the queue library swap: none of it goes in unless a reader can observe something different.
The test is observability, not importance. A library swap that took three weeks and prevented a class of outage is enormous, and it still does not go in if nothing a user can see changed. Meanwhile a one-character default change goes in, because someone's Tuesday depends on it.
The interesting cases are the ones in between, and there is only one honest way to resolve them: ask the person who wrote it what a user would notice. If the answer is "nothing", it is out. If the answer is "well, jobs might come back in a different order now" — that is a Changed entry, and you nearly shipped a release that quietly reordered someone's pipeline.
- Ask what a reader could observe: an output, a shape, a timing, an error, a default, a limit.
- Effort is not the test. Big internal work with no observable effect stays internal.
- "Might" is an answer that needs its author, not a hedge you can write into an entry.
- Internal work does not need a changelog to be recognised — that is what the commit history and the team already are.
Step 4: Build the categories from what shipped
Added, Changed, Fixed, Deprecated, Removed, Security — the conventional set, and it is a menu rather than a form. The categories are supposed to describe this release. If nothing was removed, Removed is not an empty heading to be honest about; it is a heading that does not belong, and an empty one teaches readers to skim past headings.
This is where the Markdown Output Builder does the real work, and it is worth knowing what it does and does not hand you. Its changelog document type suggests five sections — Added, Changed, Fixed, Removed, Security — and enforces two rules that are the whole discipline in one line each: write each entry as a user-visible change rather than a commit message, and never bundle unrelated changes into one bullet. What it does not suggest is Deprecated. It is not one of the chips; if your release deprecates something, you add that section yourself and describe what belongs in it. That is not a workaround — the section list is yours to build, which is the point of the thing.
One detail worth understanding rather than fighting: the changelog preset ships with strict consistency, and strict does not mean "always five categories". It means the model may not drift from the skeleton you defined — no renaming your headings, no merging your sections, no dropping one because the content got thin. Strict is enforcement of your decisions, so make the decisions first and let it hold the line. Drop Removed in the builder and strict holds you to the four you kept.
- Decide the sections from the change list you just cut down, not from the convention.
- Add Deprecated by hand when the release deprecates something — the suggestions stop at five.
- An empty category is a lie of omission in reverse: it implies you checked and found nothing, when usually nobody looked.
- Security earns its place when something security-relevant shipped. A pinned empty Security section is a defensible house style — the packaged prompt takes that position deliberately — but it is a choice you should make on purpose rather than inherit.
Step 5: Write the entry as before, after, and what to do about it
An entry that says what changed is half an entry. The reader is not reading out of curiosity — they are working out whether this lands on them and what it costs. So the shape is: what it did before, what it does now, who that reaches, and what they have to do. Most entries end at "nothing to do", and saying so is the service.
Breaking changes carry the action or they are not finished. Not a warning — the action. "The export shape changed" is a warning. "`annotations` was an object keyed by id, it is now an array with the id inside each item, so build that map on your side; there is no compatibility flag" is an entry someone can act on at 9am without opening your source.
Granularity is decided by the reader, not by the merge list. Two PRs that produced one observable change are one entry. One PR that changed three unrelated things is three. A PR and the issue it closed are one entry with two references — and if you skip this merge, the same change appears twice in different words and the reader assumes two things broke.
Then reference it. Every entry gets a link to the PR or issue behind it, so the person who needs the detail can get it and the entry can stay short. Where you do not have the URL yet, a placeholder is fine — an unfilled placeholder is visible, and an omitted link is not.
- Before, after, who, and what to do. If "what to do" is nothing, write nothing — do not leave the reader to infer it.
- Give the action, not the alarm. A breaking change without its migration step is an announcement of a problem.
- One observable change is one entry, however many PRs produced it.
- Link every entry to its record. The entry is the summary; the link is the evidence.
Step 6: Park what nobody can stand behind
Some records will not survive the previous steps, and the temptation is to write a vague entry rather than an empty one. That is how "improved performance" gets published — a sentence with no measurement, no audience, and no way to be wrong, sitting next to entries that are all three.
Park it instead, visibly, with the reason and the person: excluded until its author says what got faster and how anyone knows. A parked entry costs one line in a draft nobody outside the team reads. A published unmeasured claim costs you the reader's trust in the entries that were true.
The date is the same discipline in a place people forget it is a claim. A changelog entry has a version and a date, and neither is yours to invent. Until the release publishes, the entries live under Unreleased — which is not a placeholder, it is an accurate statement about the world. When it publishes, Unreleased becomes the version and gets the date it actually happened. A date written on Wednesday for a Thursday release is a small fiction that ages into a real one when the release slips to the following week and nobody edits it back.
- Park with a reason and an owner: what is missing, and who can settle it.
- Unreleased is the honest home for anything not published yet. Promote it on the day, not before.
- Version numbers are a judgement about impact — whether the export break is major or minor is a decision your team owns, and it is not one to delegate to a draft.
- A deprecation without a removal date is a note nobody will ever schedule. If the date is unknown, that is the finding.
Step 7: Publish it the same way every time, and leave the old entries alone
A changelog earns its value by being in the same place, in the same shape, every release. Its usefulness is cumulative — someone scanning for when a behaviour changed is reading across versions, and every inconsistency between them is a place their eye stops. Same file, same categories where they apply, same entry shape, same level of detail.
Which leaves the rule people break with the best intentions: a published entry is a record, and records are not edited to become correct. If a v2.4.0 entry was wrong, the fix is a new entry in the current release saying what is actually true, not a quiet rewrite of history that leaves everyone who read the original holding a version of the past that no longer exists anywhere. The one exception is repair with a footprint — a correction noted as a correction, dated, so the change to the record is itself part of the record.
That is the difference between a changelog and a description of your software. A description should always be current. A ledger should be append-only, and a ledger silently rewritten is just a document.
- Same file, same shape, every release. The value is in the comparison across versions.
- Wrong published entry: correct it in the current release, do not rewrite the old one.
- If you must touch history, leave the correction visible and dated.
- Consistency beats completeness. A thorough changelog nobody can scan is worse than a plain one they can.
Common mistakes
The ways a changelog goes wrong that are not about writing quality:
- Publishing the merge list. One entry per PR is the commit history with worse formatting — and it reads as thorough, which is why it survives review.
- Filling a category because the convention has it. An empty Removed section implies somebody checked. Usually nobody did.
- Deprecating with no removal date. "Deprecated" alone is a mood; the date is what makes anyone plan for it, and the release where you announce it is the only time you have everyone's attention.
- Writing "improved performance". If it was measured, the numbers make the entry. If it was not, the entry is a feeling with a version number on it.
- Explaining the vulnerability in the Security entry. The reader needs what changed and what to do — the mechanism is a different document with a different audience, and the changelog is a public file.
- Quietly fixing an old entry. The correction is right and the silence is the problem: a ledger you edit is a ledger nobody can cite.
A worked example: Fieldnote v2.6.0
Fieldnote is a team file-annotation app. Eight source notes came out of the merge window, and every one of the interesting decisions happens before a single entry is written.
Read the shape of it rather than the prose: six of the eight notes become five entries, one is cut, one is parked. Two records collapse into one entry because they are one change seen twice. The category set that ships is not the one the builder suggested — Removed goes, because nothing was removed, and Deprecated arrives, because something was. And the date stays empty, because on Wednesday nobody knows it.
Eight source notes in: six become five entries, one is cut as internal, one is parked as unverifiable — and the release date stays blank until it publishesTHE SOURCE NOTES (what you were handed, eight of them):
PR #418 threaded replies
PR #421 swap queue lib to v3
PR #423 export: return annotations array not object
PR #427 deprecate /v1/export
PR #431 harden share link session handling
PR #412 fix annotation search in shared files
ISS #402 "search finds nothing in files shared with me" (closed by #412)
PR #435 perf improvements
WHAT EACH ONE TURNS OUT TO BE:
#418 a user-visible feature -> Added
#423 a breaking change to a live API -> Changed, with the action
#412 + #402 ONE change, two records -> Fixed (merged)
#427 a deprecation, no removal date -> Deprecated (date TBD)
#431 a security-relevant change -> Security
#421 internal only, nothing observable -> cut
#435 "perf improvements" -> parked, unverifiable
THE CATEGORIES THIS RELEASE ACTUALLY NEEDS:
the builder suggests: Added Changed Fixed Removed Security
this release removed nothing -> drop Removed
this release deprecated something -> add Deprecated (type it in;
it is not one of the suggestions)
THE DRAFT:
## [Unreleased]
## [2.6.0] - TBD
the date goes in when it publishes. Nobody has set it yet, so
nobody gets to write one here.
### Added
- Replies on an annotation now nest under the comment they answer,
instead of sitting in one flat list. Affects: everyone, on every
file -- existing replies stay where they are, at the top level.
Nothing to do. ([#418](<repo>/pull/418))
### Changed
- **Breaking - /v2/export.** `annotations` was an object keyed by
annotation id. It is now an array, and the id moved inside each item.
Affects: anything that reads /v2/export -- most integrations index
by id. You have to: build that id map on your side. There is no
compatibility flag on this endpoint. ([#423](<repo>/pull/423))
### Fixed
- Annotation search missed files shared with you -- it searched only
files you own. Affects: anyone who searched a shared file and read
the empty result as an answer. You have to: run those searches
again. They were wrong, not empty. ([#412](<repo>/pull/412),
[#402](<repo>/issues/402))
### Deprecated
- `/v1/export` is deprecated. It still works and still returns its
original shape. Removal: TBD - needs owner confirmation before this
publishes. Affects: anyone still on /v1. You have to: plan the move
to /v2 -- and read the /v2 export entry above first, because the
endpoint you are being moved to changed shape in this same release.
([#427](<repo>/pull/427))
### Security
- A share link now opens a session scoped to that link, which ends when
the link's access ends, instead of reusing the account session of
whoever opened it. Affects: people who open a share link and later
go to the workspace directly. You have to: sign in again when you
get there -- nothing else changes, and no existing link stops
working. ([#431](<repo>/pull/431))
NOT IN THE DRAFT, ON PURPOSE:
#421 internal. The queue library changed; nothing a user can observe
did. It stays in the commit history, which already has it.
#435 parked. "Perf improvements" names no measurement and no user-
visible claim. Excluded until its author says what got faster,
for whom, and how anyone knows.
THE SAME RELEASE, ANNOUNCED:
"Fieldnote 2.6 brings threaded replies to every annotation."
True, and it is one of five. The announcement gets to pick the change
worth selling. The entry above does not get to pick: the export break
reaches that reader on Tuesday whether or not it is interesting.
Where this fits in NewPrompt
The Markdown Output Builder is where the document gets its shape: you pick the changelog type, build the section list this release actually needs, and it builds the prompt — in your browser, running nothing. What comes out is prompt text. You paste it into your own AI assistant with your change records underneath it, and you read what comes back with the merge list open beside you. One honest wrinkle worth knowing before it confuses you: the generated prompt carries a line saying there is no source text to summarize, which is true of most documents this tool builds and false of a changelog. Replace it with your notes. It is your clipboard.
The Changelog Prompt is that same contract already built — the five-category version, ready to copy, and its own instructions tell you to paste the raw release notes underneath. Take it when your release fits five categories and you want to skip the building. Take the builder when it does not, which is any release with a deprecation in it.
The neighbours are worth naming, because half of changelog trouble is a document doing another document's job. The README Prompt Template shapes the page that says what the project is and how to start — always current, no history. The API Documentation Prompt shapes the reference that says what the interface does now — also always current. A changelog is neither: it is the only one of the three that is about time, and the only one where being out of date is not a defect but the entire point. A README that describes last year is broken. A changelog entry that describes last year is doing its job.
Release readiness is the decision to ship and lives before this; a runbook is how the thing gets deployed and lives beside it; neither belongs in a file whose reader has already got the new version. And an announcement is the release with a case made for it, which is a real document with a real audience — just not this reader, who is not deciding whether to care. They are upgrading on Tuesday, and the export break reaches them either way.