How to Create Custom WP-CLI Commands: Developer’s Guide

WP-CLI comes with hundreds of built-in commands for managing WordPress. But what if you need a command specific to your workflow, plugin, or client requirements? Creating custom WP-CLI commands lets you extend the CLI with your own functionality.

In this advanced guide, you’ll learn to create production-ready custom WP-CLI commands from scratch. You’ll understand command structure, parameter handling, the WP_CLI API, and how to package your commands for distribution.

By the end, you’ll have the skills to build professional CLI tools that make WordPress management even more powerful.

Why Create Custom WP-CLI Commands?

Use Cases for Custom Commands

Plugin-Specific Operations

  • Flush custom caches
  • Run migrations
  • Import/export plugin data
  • Administrative tasks

Agency/Enterprise Workflows

  • Client onboarding automation
  • Standardized site audits
  • Custom deployment tasks
  • Compliance reporting

Development Tools

  • Generate boilerplate code
  • Database seeding
  • Testing helpers
  • Performance profiling

Benefits Over Manual Scripts

Integration with WP-CLI Ecosystem

  • Automatic WordPress context loading
  • Access to all WordPress functions
  • Works with WP-CLI packages
  • Follows CLI conventions

Professional API

  • WP_CLI::success()WP_CLI::error()WP_CLI::warning()
  • Built-in progress bars
  • Table formatting
  • Color output

Distribution

  • Share via Composer packages
  • Install with wp package install
  • Version management

Prerequisites

Before creating custom commands, you should have:

  • WP-CLI installed – Installation guide
  • PHP knowledge – Object-oriented PHP basics
  • WordPress familiarity – Understanding of WordPress hooks and functions
  • Bash fundamentals – Bash scripting guide

Command Structure Basics

Command Anatomy

WP-CLI commands follow this structure:

wp <command> <subcommand> <arguments> --<flags>

Examples:

wp plugin install woocommerce
#  ^^^^^^ ^^^^^^^ ^^^^^^^^^^^
#  cmd    subcmd  argument

wp post list --post_type=page --format=table
#  ^^^^ ^^^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
#  cmd  sub  flag=value        flag=value

Command Registration

Commands are registered using WP_CLI::add_command():

WP_CLI::add_command('hello', 'My_Hello_Command');

This creates: wp hello

Class-Based vs Function-Based

Class-based (recommended):

class My_Command {
		public function greet($args, $assoc_args) {
				WP_CLI::success("Hello!");
		}
}

WP_CLI::add_command('mycommand greet', 'My_Command');

Function-based:

function my_greet_command($args, $assoc_args) {
		WP_CLI::success("Hello!");
}

WP_CLI::add_command('greet', 'my_greet_command');

Best practice: Use classes for better organization and multiple subcommands.

Your First Custom Commandnd}

Let’s create a simple custom command step-by-step.

Step 1: Create Plugin File

Create a plugin to hold your command:

<?php
/**
 * Plugin Name: My WP-CLI Commands
 * Description: Custom WP-CLI commands
 * Version: 1.0.0
 */

// Don't execute in non-CLI context
if (!defined('WP_CLI')) {
		return;
}

/**
 * Hello World command
 */
class Hello_Command {

		/**
		 * Prints a greeting
		 *
		 * ## EXAMPLES
		 *
		 *     wp hello greet
		 *     wp hello greet --name=John
		 */
		public function greet($args, $assoc_args) {
				$name = isset($assoc_args['name']) ? $assoc_args['name'] : 'World';

				WP_CLI::success("Hello, {$name}!");
		}
}

// Register the command
WP_CLI::add_command('hello', 'Hello_Command');

Step 2: Install Plugin

Save this as wp-content/plugins/my-cli-commands/my-cli-commands.php

Activate it:

wp plugin activate my-cli-commands

Step 3: Test Your Command

wp hello greet
# Success: Hello, World!

wp hello greet --name=John
# Success: Hello, John!

Congratulations! You’ve created your first custom WP-CLI command.

Step 4: View Help

WP-CLI automatically generates help documentation:

wp help hello greet

Using the WP_CLI Class

The WP_CLI class provides powerful methods for CLI interaction.

Output Methods

// Success message (green)
WP_CLI::success("Operation completed!");

// Error message (red) - exits script
WP_CLI::error("Something went wrong!");

// Warning message (yellow)
WP_CLI::warning("Proceeding with caution...");

// Info message (no color)
WP_CLI::log("Processing item 5 of 10");

// Debug message (only shown with --debug)
WP_CLI::debug("Variable value: " . $value);

Example:

public function process() {
		WP_CLI::log("Starting processing...");

		if (!file_exists($file)) {
				WP_CLI::warning("File not found, using defaults");
		}

		if ($error) {
				WP_CLI::error("Processing failed!");  // Exits here
		}

		WP_CLI::success("Processing complete!");
}

Progress Bars

For long-running operations:

public function import_posts($args) {
		$posts = range(1, 1000);

		$progress = \WP_CLI\Utils\make_progress_bar('Importing posts', count($posts));

		foreach ($posts as $post) {
				// Do import work
				sleep(0.01);  // Simulate work

				$progress->tick();
		}

		$progress->finish();
		WP_CLI::success("Imported " . count($posts) . " posts");
}

Output:

Importing posts  500/1000 [==============>              ] 50%

Confirmation Prompts

Confirm destructive operations:

public function delete_all_posts() {
		WP_CLI::confirm("Are you sure you want to delete ALL posts?");

		// User must type 'y' or 'yes' to continue
		// Otherwise, script exits

		// Proceed with deletion
		WP_CLI::success("All posts deleted");
}

Usage:

wp mycommand delete-all-posts
# Are you sure you want to delete ALL posts? [y/n]

Skip confirmation in scripts:

wp mycommand delete-all-posts --yes

Learn more in the official WP_CLI class documentation.

Parameter Handling with Synopsis

Synopsis defines your command’s parameters and flags.

Basic Synopsis

/**
 * Import data from file
 *
 * ## OPTIONS
 *
 * <file>
 * : Path to import file
 *
 * [--format=<format>]
 * : Import format (json, csv)
 * ---
 * default: json
 * options:
 *   - json
 *   - csv
 * ---
 *
 * ## EXAMPLES
 *
 *     wp mycommand import data.json
 *     wp mycommand import data.csv --format=csv
 *
 * @when after_wp_load
 */
public function import($args, $assoc_args) {
		list($file) = $args;  // Required positional argument

		$format = $assoc_args['format'];  // Has default value

		WP_CLI::log("Importing {$file} as {$format}");
}

Parameter Types

Required positional argument:

/**
 * <file>
 * : Path to file
 */

Usage: wp cmd import file.json

Optional positional argument:

/**
 * [<file>]
 * : Path to file (optional)
 */

Required flag:

/**
 * --user=<id>
 * : User ID (required)
 */

Usage: wp cmd process --user=1

Optional flag with default:

/**
 * [--format=<format>]
 * : Output format
 * ---
 * default: table
 * options:
 *   - table
 *   - json
 *   - csv
 * ---
 */

Boolean flag:

/**
 * [--dry-run]
 * : Run without making changes
 */

Usage: wp cmd process --dry-run

Check in code:

$dry_run = isset($assoc_args['dry-run']) && $assoc_args['dry-run'];

Accessing Parameters

public function command($args, $assoc_args) {
		// Positional arguments (ordered)
		$first_arg = isset($args[0]) ? $args[0] : '';
		$second_arg = isset($args[1]) ? $args[1] : '';

		// Or use list()
		list($file, $action) = $args;

		// Associative arguments (flags)
		$format = $assoc_args['format'];  // No default, may not exist
		$format = isset($assoc_args['format']) ? $assoc_args['format'] : 'table';

		// Get with default helper
		$format = \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');
}

Output Formatting

Table Format

Display data as tables:

public function list_users() {
		$users = get_users(array('number' => 10));

		$items = array();
		foreach ($users as $user) {
				$items[] = array(
						'ID' => $user->ID,
						'Username' => $user->user_login,
						'Email' => $user->user_email,
						'Role' => implode(', ', $user->roles)
				);
		}

		\WP_CLI\Utils\format_items('table', $items, array('ID', 'Username', 'Email', 'Role'));
}

Output:

+----+----------+------------------+-------------+
| ID | Username | Email            | Role        |
+----+----------+------------------+-------------+
| 1  | admin    | admin@site.com   | administrator |
| 2  | editor   | editor@site.com  | editor      |
+----+----------+------------------+-------------+

JSON Format

\WP_CLI\Utils\format_items('json', $items, array('ID', 'Username', 'Email'));

Output:

[
	{"ID":"1","Username":"admin","Email":"admin@site.com"},
	{"ID":"2","Username":"editor","Email":"editor@site.com"}
]

CSV Format

\WP_CLI\Utils\format_items('csv', $items, array('ID', 'Username', 'Email'));

Output:

ID,Username,Email
1,admin,admin@site.com
2,editor,editor@site.com

Supporting Multiple Formats

/**
 * [--format=<format>]
 * : Output format
 * ---
 * default: table
 * options:
 *   - table
 *   - json
 *   - csv
 *   - count
 * ---
 */
public function list($args, $assoc_args) {
		$format = \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');

		$items = $this->get_items();

		if ('count' === $format) {
				WP_CLI::log(count($items));
				return;
		}

		\WP_CLI\Utils\format_items($format, $items, array('ID', 'Title', 'Status'));
}

Error Handling

Validation

Always validate parameters:

public function import($args, $assoc_args) {
		list($file) = $args;

		// Check file exists
		if (!file_exists($file)) {
				WP_CLI::error("File not found: {$file}");
		}

		// Check file is readable
		if (!is_readable($file)) {
				WP_CLI::error("File is not readable: {$file}");
		}

		// Validate format
		$format = $assoc_args['format'];
		$allowed = array('json', 'csv');
		if (!in_array($format, $allowed)) {
				WP_CLI::error("Invalid format: {$format}. Must be: " . implode(', ', $allowed));
		}

		// Proceed with import...
}

Try-Catch

Handle exceptions gracefully:

public function process() {
		try {
				$this->do_risky_operation();
				WP_CLI::success("Operation completed");
		} catch (Exception $e) {
				WP_CLI::error("Operation failed: " . $e->getMessage());
		}
}

Exit Codes

Commands should return appropriate exit codes:

  • 0 – Success
  • 1 – Error (automatic with WP_CLI::error())

Complete Example: Site Audit Command

Let’s build a real-world command that audits a WordPress site.

<?php
/**
 * Plugin Name: Site Audit Command
 * Description: Custom WP-CLI command for site auditing
 * Version: 1.0.0
 */

if (!defined('WP_CLI')) {
		return;
}

/**
 * Performs comprehensive site audits
 */
class Site_Audit_Command {

		/**
		 * Run a complete site audit
		 *
		 * ## OPTIONS
		 *
		 * [--format=<format>]
		 * : Output format
		 * ---
		 * default: table
		 * options:
		 *   - table
		 *   - json
		 * ---
		 *
		 * [--save=<file>]
		 * : Save results to file
		 *
		 * ## EXAMPLES
		 *
		 *     wp site-audit run
		 *     wp site-audit run --format=json
		 *     wp site-audit run --save=audit-report.json
		 *
		 * @when after_wp_load
		 */
		public function run($args, $assoc_args) {
				WP_CLI::log("Running site audit...");

				$results = array();

				// WordPress version
				$results[] = $this->check_wordpress_version();

				// Plugin updates
				$results[] = $this->check_plugin_updates();

				// Theme updates
				$results[] = $this->check_theme_updates();

				// Database size
				$results[] = $this->check_database_size();

				// Upload directory size
				$results[] = $this->check_uploads_size();

				// Orphaned data
				$results[] = $this->check_orphaned_data();

				// Get format
				$format = \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');

				// Output results
				if ('json' === $format) {
						WP_CLI::log(json_encode($results, JSON_PRETTY_PRINT));
				} else {
						\WP_CLI\Utils\format_items('table', $results, array('Check', 'Status', 'Details'));
				}

				// Save to file if requested
				if (isset($assoc_args['save'])) {
						file_put_contents($assoc_args['save'], json_encode($results, JSON_PRETTY_PRINT));
						WP_CLI::success("Audit saved to: " . $assoc_args['save']);
				}

				WP_CLI::success("Audit complete!");
		}

		private function check_wordpress_version() {
				global $wp_version;

				$updates = get_core_updates();
				$has_update = isset($updates[0]) && $updates[0]->response === 'upgrade';

				return array(
						'Check' => 'WordPress Version',
						'Status' => $has_update ? 'โš  Update Available' : 'โœ“ Up to Date',
						'Details' => 'Current: ' . $wp_version
				);
		}

		private function check_plugin_updates() {
				$updates = get_plugin_updates();
				$count = count($updates);

				return array(
						'Check' => 'Plugin Updates',
						'Status' => $count > 0 ? "โš  {$count} available" : 'โœ“ All updated',
						'Details' => $count > 0 ? implode(', ', array_keys($updates)) : 'None'
				);
		}

		private function check_theme_updates() {
				$updates = get_theme_updates();
				$count = count($updates);

				return array(
						'Check' => 'Theme Updates',
						'Status' => $count > 0 ? "โš  {$count} available" : 'โœ“ All updated',
						'Details' => $count > 0 ? implode(', ', array_keys($updates)) : 'None'
				);
		}

		private function check_database_size() {
				global $wpdb;

				$size = $wpdb->get_var("
						SELECT SUM(data_length + index_length) / 1024 / 1024
						FROM information_schema.TABLES
						WHERE table_schema = '{$wpdb->dbname}'
				");

				$size_mb = round($size, 2);
				$status = $size_mb > 500 ? 'โš  Large' : 'โœ“ Normal';

				return array(
						'Check' => 'Database Size',
						'Status' => $status,
						'Details' => "{$size_mb} MB"
				);
		}

		private function check_uploads_size() {
				$upload_dir = wp_upload_dir();
				$path = $upload_dir['basedir'];

				$size = $this->get_directory_size($path);
				$size_mb = round($size / 1024 / 1024, 2);
				$status = $size_mb > 5000 ? 'โš  Large' : 'โœ“ Normal';

				return array(
						'Check' => 'Uploads Directory',
						'Status' => $status,
						'Details' => "{$size_mb} MB"
				);
		}

		private function check_orphaned_data() {
				global $wpdb;

				// Count orphaned post meta
				$orphaned = $wpdb->get_var("
						SELECT COUNT(*)
						FROM {$wpdb->postmeta} pm
						LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
						WHERE p.ID IS NULL
				");

				$status = $orphaned > 0 ? "โš  {$orphaned} found" : 'โœ“ None';

				return array(
						'Check' => 'Orphaned Post Meta',
						'Status' => $status,
						'Details' => $orphaned > 0 ? "Run cleanup recommended" : 'Clean'
				);
		}

		private function get_directory_size($path) {
				$size = 0;

				foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)) as $file) {
						if ($file->isFile()) {
								$size += $file->getSize();
						}
				}

				return $size;
		}
}

WP_CLI::add_command('site-audit', 'Site_Audit_Command');

Usage

# Run audit
wp site-audit run

# JSON output
wp site-audit run --format=json

# Save to file
wp site-audit run --save=audit-2025.json

Output:

+---------------------+-------------------+------------------+
| Check               | Status            | Details          |
+---------------------+-------------------+------------------+
| WordPress Version   | โœ“ Up to Date      | Current: 6.4.2   |
| Plugin Updates      | โš  3 available     | woocommerce, ... |
| Theme Updates       | โœ“ All updated     | None             |
| Database Size       | โœ“ Normal          | 45.23 MB         |
| Uploads Directory   | โš  Large           | 5432.12 MB       |
| Orphaned Post Meta  | โš  156 found       | Run cleanup...   |
+---------------------+-------------------+------------------+
Success: Audit complete!

Packaging Commands

Create Composer Package

Structure your command as a Composer package:

my-wp-cli-command/
โ”œโ”€โ”€ composer.json
โ”œโ”€โ”€ command.php
โ””โ”€โ”€ README.md

composer.json:

{
		"name": "vendor/my-wpcli-command",
		"description": "Custom WP-CLI command for site auditing",
		"type": "wp-cli-package",
		"require": {
				"php": ">=7.4"
		},
		"autoload": {
				"files": ["command.php"]
		},
		"extra": {
				"commands": [
						"site-audit"
				]
		}
}

Install via Composer

Users can install your command:

wp package install vendor/my-wpcli-command

Or via composer.json:

{
		"require": {
				"vendor/my-wpcli-command": "^1.0"
		}
}

Learn more: WP-CLI Package Index

Testing Custom Commands

Manual Testing

# Test basic functionality
wp site-audit run

# Test with flags
wp site-audit run --format=json

# Test error handling (invalid flag)
wp site-audit run --format=invalid

Automated Testing with Behat

For serious projects, use WP-CLI’s testing framework:

# Install testing framework
composer require --dev wp-cli/wp-cli-tests

Example test:

Feature: Site audit command

	Scenario: Run basic audit
		Given a WP install

		When I run `wp site-audit run`
		Then STDOUT should contain "Audit complete"
		And the return code should be 0

	Scenario: Save audit to file
		Given a WP install

		When I run `wp site-audit run --save=test-audit.json`
		Then the test-audit.json file should exist
		And STDOUT should contain "Audit saved"

Learn more: WP-CLI Testing Documentation

Distribution

GitHub Repository

Host your command on GitHub:

  1. Create repo: my-wpcli-command
  2. Push code with composer.json
  3. Tag releases: v1.0.0v1.1.0, etc.

WP-CLI Package Index

Submit to WP-CLI Package Index:

  1. Follow package guidelines
  2. Submit pull request to package index
  3. Wait for review and approval

WordPress Plugin Directory

Optionally distribute as WordPress plugin:

  • Works automatically when plugin is activated
  • Discoverable via WordPress.org
  • Easier for non-technical users

Real-World Examples

Official WP-CLI Packages

Study these for inspiration:

  • WooCommerce CLI – WooCommerce management
  • ACF CLI – Advanced Custom Fields operations
  • Yoast SEO CLI – SEO management

Browse more: WP-CLI Package Index

Troubleshooting

Command Not Found

Problemwp mycommand returns “Error: ‘mycommand’ is not a registered wp command.”

Solutions:

  1. Check plugin is activated:
wp plugin list
  1. Verify WP_CLI is defined:
if (!defined('WP_CLI')) {
		return; // Command won't register
}
  1. Check command registration:
WP_CLI::add_command('mycommand', 'My_Command');

Parameters Not Working

Problem: Flags are ignored or cause errors.

Solutions:

  1. Check synopsis syntax in docblock
  2. Verify parameter names match:
$format = $assoc_args['format'];  // Must match --format
  1. Provide defaults for optional parameters:
$format = \WP_CLI\Utils\get_flag_value($assoc_args, 'format', 'table');

Memory Limit Errors

Problem: Command fails with “Allowed memory size exhausted.”

Solution: Process items in batches:

public function process_all() {
		$per_page = 100;
		$page = 1;

		do {
				$items = get_posts(array(
						'posts_per_page' => $per_page,
						'paged' => $page
				));

				foreach ($items as $item) {
						// Process item
				}

				$page++;

		} while (count($items) === $per_page);
}

Next Steps

You now know how to create production-ready custom WP-CLI commands!

Continue Learning

  1. Bash Functions for WordPress – Combine with shell scripts
  2. WordPress CI/CD with GitHub Actions – Use commands in pipelines
  3. WP-CLI Config Files – Advanced configuration

Build These Commands

Practice projects:

  • Content migration command
  • Security audit command
  • Performance optimization command
  • Database cleanup command
  • Multi-site sync command

Master WordPress Automation

Want to build advanced automation systems with custom commands, APIs, and deployment pipelines?

Join the WPCLI Mastery waitlist and get:

  • Advanced command development patterns
  • Real-world command examples library
  • Package creation templates
  • Early bird course pricing ($99 vs $199)

Conclusion

Creating custom WP-CLI commands lets you extend WordPress automation with your specific workflow needs. Whether you’re building internal tools, distributing plugins, or streamlining client management, custom commands are essential for professional WordPress development.

Key takeaways:

  • Use classes for organization and multiple subcommands
  • Leverage WP_CLI class methods for professional output
  • Define parameters with synopsis for clear documentation
  • Support multiple output formats (table, JSON, CSV)
  • Package with Composer for easy distribution
  • Test thoroughly before releasing

The custom commands you build today become reusable tools that save time forever.

Ready to build? Start with the site audit example and adapt it to your needs.


Questions about custom WP-CLI commands? Drop a comment below!

Found this helpful? Share with other WordPress developers building CLI tools.

Next: Learn how to integrate external APIs with WordPress using WP-CLI commands.