WP-CLI custom commands let you expose any plugin functionality as a command-line operation — useful for batch migrations, database repairs, scheduled tasks, and deployment scripts. A well-written WP-CLI command is faster, more scriptable, and more testable than a WordPress admin page.
Problem: Repetitive WP-CLI tasks — resetting a staging environment, exporting a subset of posts, running a multi-step database migration — require either writing shell scripts that call multiple wp commands or running each step manually.
Solution: Register a custom WP-CLI command by adding a class that extends WP_CLI_Command and decorating its methods with @subcommand and @synopsis PHPDoc annotations. Register it with WP_CLI::add_command('my-plugin', 'My_CLI_Command') in a file loaded only when defined('WP_CLI') returns true.
The examples below register a custom WP-CLI command class, add subcommands with argument and flag parsing, and show how to output progress bars and formatted tables for batch operations.
]
* : How many products to process per API page.
* ---
* default: 100
* ---
*
* [--status=]
* : Only sync products with this post status.
* ---
* default: publish
* options:
* - publish
* - draft
* ---
*
* ## EXAMPLES
*
* wp myplugin sync products --dry-run
*
* @subcommand products
*/
public function sync( array $args, array $assoc_args ): void {
$dry_run = WP_CLI\Utils\get_flag_value( $assoc_args, 'dry-run', false );
$batch = absint( $assoc_args['batch'] ?? 100 );
$status = sanitize_key( $assoc_args['status'] ?? 'publish' );
WP_CLI::log( "Starting sync (batch={$batch}, status={$status}" . ( $dry_run ? ', DRY RUN' : '' ) . ')' );
$products = $this->fetch_products( $status, $batch );
$progress = WP_CLI\Utils\make_progress_bar( 'Syncing products', count( $products ) );
$updated = 0;
$skipped = 0;
foreach ( $products as $product_data ) {
if ( ! $dry_run ) {
$this->upsert_product( $product_data );
$updated++;
} else {
WP_CLI::debug( "Would update: {$product_data['sku']}" );
$skipped++;
}
$progress->tick();
}
$progress->finish();
WP_CLI::success( "Done. Updated: {$updated}, Skipped (dry-run): {$skipped}" );
}
}
WP_CLI::add_command( 'myplugin sync', 'MyPlugin_CLI' );
Output structured data as a formatted table:
]
* : Output format.
* ---
* default: table
* options:
* - table
* - csv
* - json
* ---
*/
public function list_products( array $args, array $assoc_args ): void {
$format = $assoc_args['format'] ?? 'table';
$rows = [];
$products = wc_get_products( [ 'limit' => 50, 'status' => 'publish' ] );
foreach ( $products as $product ) {
$rows[] = [
'ID' => $product->get_id(),
'SKU' => $product->get_sku() ?: '—',
'Name' => $product->get_name(),
'Last Synced' => get_post_meta( $product->get_id(), '_last_synced', true ) ?: 'never',
'Status' => $product->get_status(),
];
}
WP_CLI\Utils\format_items( $format, $rows, [ 'ID', 'SKU', 'Name', 'Last Synced', 'Status' ] );
}
// Register the list command on the same parent
WP_CLI::add_command( 'myplugin list', [ new MyPlugin_CLI(), 'list_products' ] );
NOTE: Use WP_CLI::confirm() before destructive operations, WP_CLI::error() to exit with a non-zero code (which fails CI pipelines), and WP_CLI::debug() for verbose output that only appears with the --debug flag. Document every command with @subcommand and ## OPTIONS docblocks — WP-CLI uses them for wp help myplugin.