Skip to content

Introduction

conventional-changelog-writer renders already-parsed commits into changelog text. It’s the final stage of the pipeline — parserfilterwriter — and the engine behind conventional-changelog. What the output looks like is controlled entirely by the options you pass; presets are ready-made option sets.

  1. Install conventional-changelog-writer:

    pnpm add conventional-changelog-writer
  2. Provide parsed commits. The writer consumes parsed commit objects, like those produced by conventional-commits-parser:

    const commits = [
    {
    type: 'feat',
    scope: 'api',
    subject: 'add async write() generator',
    header: 'feat(api): add async write() generator',
    hash: '0f7e2c1a9d3e4b5c6f7089abcdef0123456789ab',
    notes: [],
    references: []
    },
    {
    type: 'fix',
    scope: 'cli',
    subject: 'resolve config path relative to cwd',
    header: 'fix(cli): resolve config path relative to cwd',
    hash: 'a3b9d8472e1f0c9b8a7d6e5f4c3b2a1908f7e6d5',
    notes: [],
    references: []
    }
    ]
  3. Render the changelog. writeChangelogString(commits, context?, options?) returns it as a string; the context supplies the version and repository:

    import { writeChangelogString } from 'conventional-changelog-writer'
    const changelog = await writeChangelogString(commits, {
    version: '1.2.0',
    repoUrl: 'https://github.com/acme/app'
    })
    console.log(changelog)

    Out of the box the writer groups commits by their raw type and prints each commit’s full header — deliberately minimal, because formatting is meant to come from options:

    CHANGELOG.md
    ## 1.2.0 (2026-07-01)
    ### feat
    * feat(api): add async write() generator ([0f7e2c1](https://github.com/acme/app/commits/0f7e2c1))
    ### fix
    * fix(cli): resolve config path relative to cwd ([a3b9d84](https://github.com/acme/app/commits/a3b9d84))
  4. Format it with a preset. A preset’s writer options map types to sections, format links, and choose a template. Pass them as the third argument:

    import { writeChangelogString } from 'conventional-changelog-writer'
    import createPreset from 'conventional-changelog-conventionalcommits'
    const preset = createPreset()
    const changelog = await writeChangelogString(commits, {
    version: '1.2.0',
    repoUrl: 'https://github.com/acme/app'
    }, preset.writer)
    CHANGELOG.md
    ## 1.2.0 (2026-07-01)
    ### Features
    * **api:** add async write() generator ([0f7e2c1](https://github.com/acme/app/commit/0f7e2c1a9d3e4b5c6f7089abcdef0123456789ab))
    ### Bug Fixes
    * **cli:** resolve config path relative to cwd ([a3b9d84](https://github.com/acme/app/commit/a3b9d8472e1f0c9b8a7d6e5f4c3b2a1908f7e6d5))

See the JS API for every option — template, groupBy, transform, sorting, and more — when you want to shape the output yourself.

For large histories you can stream commits instead of buffering them. writeChangelog returns an async-generator function, and writeChangelogStream a Node.js Transform:

import { writeChangelog, writeChangelogStream } from 'conventional-changelog-writer'
import { pipeline } from 'node:stream/promises'
// Async iterable
await pipeline(
commitsStream,
writeChangelog(context, options),
async function* (changelog) {
for await (const chunk of changelog) {
process.stdout.write(chunk)
}
}
)
// Node.js stream
commitsStream
.pipe(writeChangelogStream(context, options))
.pipe(process.stdout)

The CLI wraps these to render a changelog from a stream of commits on the command line.