This is documentation for the next version of Grafana k6 documentation. For the latest stable release, go to the latest version.
Subcommand extensions
k6 provides a rich set of built-in commands, but some use cases require custom CLI tools that integrate with k6’s runtime and state. Subcommand extensions allow you to register custom commands under the k6 x namespace, providing a standardized way to extend k6’s CLI functionality.
Subcommand extensions are useful for:
- Setup and configuration tools (for example, verifying system requirements)
- Custom validation and testing utilities
- Integration tools that interact with k6’s runtime state
- Helper commands specific to your testing infrastructure
Like other k6 extensions, subcommand extensions are built as Go modules that implement specific APIs and are compiled into custom k6 binaries using xk6.
Before you begin
To run this tutorial, you’ll need the following applications installed:
You also need to install xk6:
go install go.k6.io/xk6/cmd/xk6@latestWrite a simple extension
Set up a directory to work in.
mkdir xk6-subcommand-mytool; cd xk6-subcommand-mytool; go mod init xk6-subcommand-mytoolThe core of a subcommand extension is a constructor function that creates a Cobra command. The constructor receives k6’s
GlobalStatefor read-only access to runtime configuration.Create an example command named
mytool:package mytool import ( "github.com/spf13/cobra" "go.k6.io/k6/cmd/state" "go.k6.io/k6/subcommand" ) func init() { subcommand.RegisterExtension("mytool", newCommand) } func newCommand(gs *state.GlobalState) *cobra.Command { return &cobra.Command{ Use: "mytool", Short: "My custom tool", Long: "A custom tool that integrates with k6", Run: func(cmd *cobra.Command, args []string) { gs.Logger.Info("Running mytool") // Custom logic here }, } }The extension uses the
subcommand.RegisterExtensionfunction to register itself during initialization. The first argument is the command name (which must match the command’sUsefield), and the second is the constructor function.
Caution
The
GlobalStateprovided to your command is read-only. Do not modify it, as this can cause core k6 instability.
Build k6 with the extension
Once you’ve written your extension, build a custom k6 binary with it by using xk6:
xk6 build --with xk6-subcommand-mytool=.This creates a k6 binary in your current directory that includes your extension.
Use the extension
After building, your subcommand is available under the k6 x namespace:
./k6 x mytoolTo see all available extension subcommands:
./k6 x helpConstructor requirements
The constructor function passed to RegisterExtension must:
- Accept a single
*state.GlobalStateparameter - Return a
*cobra.Command - Create a command whose
Usefield matches the registered extension name
Violating these requirements causes the extension to panic at startup, ensuring configuration errors are caught early.
Example: Complete validation tool
Here’s a more complete example that checks system requirements:
package validate
import (
"fmt"
"os"
"runtime"
"github.com/spf13/cobra"
"go.k6.io/k6/cmd/state"
"go.k6.io/k6/subcommand"
)
func init() {
subcommand.RegisterExtension("validate", newValidateCommand)
}
func newValidateCommand(gs *state.GlobalState) *cobra.Command {
cmd := &cobra.Command{
Use: "validate",
Short: "Verify system requirements",
Long: "Check if the system meets requirements for running tests",
RunE: func(cmd *cobra.Command, args []string) error {
gs.Logger.Info("Checking system requirements...")
// Check Go version
gs.Logger.Infof("Go version: %s", runtime.Version())
// Check available memory
var m runtime.MemStats
runtime.ReadMemStats(&m)
gs.Logger.Infof("Available memory: %d MB", m.Sys/1024/1024)
// Check environment variables
if broker := os.Getenv("MQTT_BROKER"); broker != "" {
gs.Logger.Infof("MQTT broker configured: %s", broker)
} else {
gs.Logger.Warn("MQTT_BROKER not set")
}
gs.Logger.Info("Validation complete")
return nil
},
}
return cmd
}Usage:
./k6 x validateAccess k6 runtime state
The GlobalState provides read-only access to k6’s configuration:
func newCommand(gs *state.GlobalState) *cobra.Command {
return &cobra.Command{
Use: "validate",
Short: "Validate k6 configuration",
Run: func(cmd *cobra.Command, args []string) {
// Access logger
gs.Logger.Info("Validating k6 configuration...")
// Access flags and options
if gs.Flags.Verbose {
gs.Logger.Debug("Verbose mode enabled")
}
// Access environment variables
gs.Logger.Infof("Working directory: %s", gs.Getwd)
},
}
}Add command flags
Use Cobra’s flag system to add options to your subcommand:
func newCommand(gs *state.GlobalState) *cobra.Command {
var target string
var verbose bool
cmd := &cobra.Command{
Use: "validate",
Short: "Validate configuration",
Run: func(cmd *cobra.Command, args []string) {
if verbose {
gs.Logger.Info("Verbose mode enabled")
}
gs.Logger.Infof("Validating target: %s", target)
// Validation logic here
},
}
cmd.Flags().StringVarP(&target, "target", "t", "localhost", "Target to validate")
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
return cmd
}Usage:
./k6 x validate --target example.com --verboseBest practices
- Read-only state: Never modify the
GlobalStatepassed to your constructor - Naming: Use descriptive, kebab-case names for your commands
- Documentation: Provide clear
ShortandLongdescriptions - Error handling: Return errors from
RunErather than panicking in command execution - Logging: Use
gs.Loggerfor consistent output with k6’s logging system

