Custom Formatters

May 4, 2026 · View on GitHub

You can write your own formatter to get exactly the kind of report you want.

Writing a formatter

A formatter is a slimmed-down kind of Plugin, sharing much of the same lifecycle and plumbing with them.

A formatter in its simplest form is an object identifying itself as a formatter plugin, with a "formatter" function to be run. It should be the default export of your formatter package or module. Here's a minimal formatter skeleton:

export default {
  type: 'formatter',
  formatter: (context) => {
    // do stuff here
  }
}

(That example is an ES Module, but CommonJS works just as well.)

Lifecycle

See Plugins → Lifecycle.

Cucumber manages the lifecycle of the stream, and will not finish it until your cleanup function has returned or settled.

Context

A formatter plugin function accepts a single argument which provides the context for your formatter. It has:

  • on(event: 'message', handler: (envelope: Envelope) => void) - function for registering an message handler
  • options - options provided by the user in their configuration
  • logger - a minimal logger, directed to stderr
  • write(buffer: string | Uint8Array) - fire-and-forget function to write data to the output stream
  • stream - writable stream the output is being written to (ideally you should use this only for feature detection and/or advanced TTY functionality; for just writing output, use write)

Messages

You can register a handler for Cucumber Messages via the on function.

Here's a minimal example that prints the overall result when the test run finishes:

export default {
  type: 'formatter',
  formatter: ({
    on,
    write
  }) => {
    on('message', envelope => {
      if (envelope.testRunFinished) {
        write(envelope.testRunFinished.success ? 'Success' : 'Failure')
      }
    })
  }
}

If you're new to Cucumber Messages, we have some resources and tools to help you work with them:

Options

Users provide a single formatOptions object to Cucumber via their configuration. If you want to target a specific key within this object, you can do that with the optionsKey prop on your formatter object, and Cucumber will only give you that block:

export default {
  type: 'formatter',
  formatter: ({ options, on, logger }) => {
    on('message', (message) => {
      if (message.testRunFinished) {
        logger.info(options.bar) // yields `2` if the options are `{foo: {bar: 2}}`
      }
    })
  },
  optionsKey: 'foo'
}

Logging

See Plugins → Logging.

Error handling

See Plugins → Error Handling.

TypeScript

If you're using TypeScript, you can get full type checking and completion using the FormatterPlugin type:

import type { FormatterPlugin } from '@cucumber/cucumber/api'

type MyFormatterOptions = {
  foo: {
    bar: number
  }
}

const myFormatter: FormatterPlugin<MyFormatterOptions> = {
  type: 'formatter',
  formatter: (context) => {...},
  optionsKey: 'foo'
}

export default myFormatter

Distribution

If you want to share your formatter with other users, publish it as an npm package and make sure your formatter plugin object is the default export of the entry point defined in package.json - that way users will be able to just reference it by the package name when running cucumber-js, once they've added it as a dependency.

Legacy architecture

The previous generation of formatter architecture is still supported (see the previous version of this doc), though we'd recommend switching to the new architecture and Cucumber Messages.