What types of extension can a k6 binary carry, and how is each one invoked?
answer
- four types, four different entry points
- import, flag, flag, command
- only JS extensions use k6/x/
- subcommands live under k6 x
basics
~10 sk6 v2 recognises four extension types: JavaScript extensions imported as k6/x/ modules, output extensions selected with --out, secret source extensions selected with --secret-source, and subcommand extensions run as k6 x <name>.
solid answer
~30 sk6 has one extension registry with four types. A **JavaScript extension** registers a module name that must begin with `k6/x/` and is reached by importing it in the script. An **output extension** registers a name that becomes valid for `k6 run --out <name>` and is never imported. A **secret source extension** registers a type for the `--secret-source <type>=<config>` flag. A **subcommand extension** adds a CLI command under the reserved `k6 x` namespace, so you run `k6 x mytool`. Only the JavaScript type carries the `k6/x/` prefix; the other three register plain names.
code
bash · 4 linesk6 run script.js # imports k6/x/mqtt
k6 run --out mysink script.js # output extension
k6 --secret-source file=./secrets.txt run script.js
k6 x mytool --help # subcommand extensiongo deeper
Learn the four names and their entry points: import for JavaScript, --out for output, --secret-source for secrets, k6 x for subcommands. Being able to list them is most of what is asked here.
Explain that all four register into one registry during Go package initialisation, that only JavaScript names carry the k6/x/ prefix, and that a duplicate registered name is a startup panic rather than a silent override.
Use the type to predict behaviour: an import can trigger automatic provisioning while an --out value cannot, and a subcommand extension gets read-only access to k6's global state, which bounds what such a tool may safely do.
Decide which surface a capability belongs on. The same feature exposed as a subcommand, an output or an importable module lands on different people and different review paths, and that choice outlives the code.
## The four kinds of extension Every k6 extension registers itself into one process-wide registry, and that registry recognises **exactly four extension types**. The type decides which surface of k6 the extension widens and, crucially, **how a user reaches it** — none of the four is reached the same way as another. | type | registered with | how a user invokes it | naming rule | |---|---|---|---| | **JavaScript** | `modules.Register` | `import x from 'k6/x/name'` in the script | name **must** start with `k6/x/` | | **output** | `output.RegisterExtension` | `k6 run --out name script.js` | plain name, no prefix | | **secret source** | `secretsource.RegisterExtension` | `k6 --secret-source name=<config>` | plain name, no prefix | | **subcommand** | `subcommand.RegisterExtension` | `k6 x name [args…]` | plain name, must match the command's own name | All four registration calls funnel into the same underlying `ext.Register`, which is why the guarantees are identical across types: registration happens during Go package initialisation, and a **duplicate name within one type panics** rather than letting the last one win. ## JavaScript extensions These are the ones people mean by default. They add importable modules — a client for a protocol k6 does not speak, a data generator, a faster implementation of something awkward in JS. The `k6/x/` prefix is not a convention: `modules.Register` **panics** if the name lacks it, so a mis-named extension takes the binary down at startup instead of failing mysteriously later. The prefix also tells a reader of the script instantly that the import cannot be satisfied by a stock k6 binary. ## Output extensions An output extension registers a name that becomes valid for the `--out` flag. It is **not** imported by the script, which surprises people: nothing in the JavaScript changes when you add one. A name that collides with a built-in output is rejected outright when k6 assembles its output list, so an extension cannot quietly shadow a built-in. ## Secret source extensions A secret source registers a type name that becomes valid for the `--secret-source` flag, whose value has the shape `type=config`. k6 resolves the named source at startup and the values it hands back are cached and redacted, so the extension's only job is retrieval. ## Subcommand extensions A subcommand extension adds a **CLI command, not test behaviour**. It lives under the reserved `k6 x` namespace — `k6 x mytool` — and never becomes a top-level `k6` command, which keeps the core command set stable. Its constructor receives k6's global state read-only, so it can see configuration, the logger and the filesystem without being able to disturb the runtime. ## How registration actually happens All four registration calls are made from a Go `init()` function, so they run when the binary starts, before any command is parsed. That has three consequences worth stating: - **The set of extensions is fixed for the life of the process.** Nothing is discovered, scanned for, or loaded later; `k6 --version` can therefore print a complete `Extensions:` block naming each entry's module path, version, registered name and type. - **Collisions are fatal, not resolved.** Registering a name twice for the same type panics during startup. That is deliberate — a silent winner would make the behaviour of a binary depend on package initialisation order. - **k6's own components use the same door.** The built-in `file` and `url` secret sources register through `secretsource.RegisterExtension` exactly as a third-party one would, which is why `--secret-source file=./secrets.txt` and a secret source you added look identical on the command line. ## Why the distinction matters in practice 1. **It tells you what to change.** Needing a protocol means a JavaScript extension and an edited script; needing a metrics destination means an output extension and an edited command line. 2. **It tells you what automatic resolution will do for you.** In k6 v2, importing a `k6/x/` module or invoking `k6 x <name>` can trigger provisioning of a suitable binary; selecting an output with `--out` cannot, because a flag value is never treated as a script dependency. 3. **It tells you what to look for in `k6 --version`.** The `Extensions:` block prints each entry's type, so you can see at a glance whether the binary carries the *kind* of extension you need. A useful sanity check when someone describes "a k6 extension": ask how it is invoked. If the answer is an `import`, it is a JavaScript extension; if it is a flag, it is an output or a secret source; if it is a command, it is a subcommand. There is no fifth answer.
- Why does adding an output extension require no change to the k6 script?An output extension registers a name for the `--out` flag, not a module name. Nothing in the JavaScript refers to it, so the script stays byte-identical and the choice of destination lives entirely on the command line. That also means k6 cannot infer the need for one from the script's imports.
- What happens if a k6 output extension registers a name that a built-in output already uses?k6 refuses to assemble its output list and reports that a built-in output with that type already exists. The extension cannot shadow a built-in, so `--out json` always means k6's own JSON output. The fix is to rename the extension's registered output.
saying these in an interview costs you the question
- Thinks k6 extensions are only importable JavaScript modules
- Believes an output extension must be imported in the script
- Expects a subcommand extension as a top-level k6 command
- Assumes every extension type needs the k6/x/ prefix
- Says a secret source is configured in the options object