In Part 1, we got WP-CLI installed and covered the essential commands. Now it's time to go deeper — custom commands, automation pipelines, remote management, and safe deployment patterns.
📌 Table of Contents
- Advanced Custom PHP Commands
- WP-CLI in CI/CD Pipelines
- Managing Remote WordPress Sites
- The --dry-run Safety Net
- Final Thoughts
Advanced Custom PHP Commands
What Makes a WP-CLI Command "Advanced"?
In Part 1, we wrote a simple shell script that chained built-in commands. That's great for setup tasks — but what if you need logic that WP-CLI doesn't have out of the box?
That's where WP_CLI::add_command() comes in. It lets you register your own commands in PHP, with full access to the WordPress codebase, custom arguments, flags, progress bars, formatted output, and error handling.
The anatomy of a custom command looks like this:
WP_CLI::add_command( 'namespace action', callable, $args );
-
namespace — your command group (e.g.
cleanup,mysite,tools) -
action — what it does (e.g.
posts,users,cache) - callable — a function or class method that runs the logic
- $args — optional: description, synopsis, usage examples
💡 Always wrap custom commands in
if ( defined( 'WP_CLI' ) && WP_CLI )— this ensures the code only runs in a CLI context and never on a web request.
Example 1 — Simple: Bulk Delete Posts by Status
A clean, practical command to delete posts by any status (draft, pending, trash). Useful after a content migration or cleanup sprint.
<?php
// Usage: wp cleanup posts --status=draft
// File: mu-plugins/cli-cleanup.php
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'cleanup posts', function( $args, $assoc ) {
$status = $assoc['status'] ?? 'draft';
$allowed = [ 'draft', 'pending', 'trash', 'private' ];
if ( ! in_array( $status, $allowed ) ) {
WP_CLI::error( "Invalid status. Allowed: " . implode( ', ', $allowed ) );
}
$posts = get_posts([
'post_status' => $status,
'numberposts' => -1,
'fields' => 'ids',
]);
if ( empty( $posts ) ) {
WP_CLI::warning( "No posts found with status: {$status}" );
return;
}
foreach ( $posts as $id ) {
wp_delete_post( $id, true );
WP_CLI::line( "Deleted post #$id" );
}
WP_CLI::success( count( $posts ) . " posts permanently deleted." );
});
}
Run it:
wp cleanup posts --status=draft
wp cleanup posts --status=trash
What it handles:
- Validates the status before running
- Warns if nothing is found instead of silently exiting
- Gives per-item feedback + a final success count
Example 2 — Advanced: User Audit & Export Command
A more powerful command that audits all users, filters by role and last login, exports a CSV report, and optionally deactivates inactive accounts — with --dry-run support baked in.
<?php
// Usage: wp audit users --role=subscriber --days=90 --export --dry-run
// File: mu-plugins/cli-audit.php
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'audit users', function( $args, $assoc ) {
$role = $assoc['role'] ?? 'subscriber';
$days = (int) ( $assoc['days'] ?? 90 );
$dry_run = isset( $assoc['dry-run'] );
$export = isset( $assoc['export'] );
$cutoff = strtotime( "-{$days} days" );
WP_CLI::log( "Auditing '{$role}' users inactive for {$days}+ days..." );
if ( $dry_run ) WP_CLI::log( "[DRY RUN] No changes will be made." );
$users = get_users([ 'role' => $role, 'number' => -1 ]);
if ( empty( $users ) ) {
WP_CLI::warning( "No users found with role: {$role}" );
return;
}
$flagged = [];
$progress = \WP_CLI\Utils\make_progress_bar(
'Scanning users', count( $users )
);
foreach ( $users as $user ) {
$last_login = (int) get_user_meta( $user->ID, 'last_login', true );
// Flag if never logged in OR inactive beyond cutoff
if ( ! $last_login || $last_login < $cutoff ) {
$flagged[] = [
'ID' => $user->ID,
'login' => $user->user_login,
'email' => $user->user_email,
'registered' => $user->user_registered,
'last_login' => $last_login
? date( 'Y-m-d', $last_login )
: 'Never',
];
if ( ! $dry_run ) {
// Deactivate: remove role instead of deleting
$user_obj = new WP_User( $user->ID );
$user_obj->remove_role( $role );
$user_obj->add_role( 'inactive' );
}
}
$progress->tick();
}
$progress->finish();
if ( empty( $flagged ) ) {
WP_CLI::success( "All users are active. Nothing to flag." );
return;
}
// Display results as a table
WP_CLI\Utils\format_items( 'table', $flagged,
[ 'ID', 'login', 'email', 'registered', 'last_login' ]
);
// Export to CSV if requested
if ( $export ) {
$file = 'inactive-users-' . date('Y-m-d') . '.csv';
$fp = fopen( $file, 'w' );
fputcsv( $fp, array_keys( $flagged[0] ) );
foreach ( $flagged as $row ) fputcsv( $fp, $row );
fclose( $fp );
WP_CLI::success( "Exported to {$file}" );
}
$action = $dry_run ? "would be deactivated" : "deactivated";
WP_CLI::success( count( $flagged ) . " users {$action}." );
});
}
Run it:
# Preview only — safe, no changes
wp audit users --role=subscriber --days=90 --dry-run
# Run it for real + export a CSV report
wp audit users --role=subscriber --days=90 --export
# Target a different role and timeframe
wp audit users --role=editor --days=180
What makes this advanced:
- Progress bar for large user sets
-
--dry-runflag built-in — preview before committing - Formatted table output using WP-CLI's own utils
- CSV export with a timestamped filename
- Degrades gracefully: warns, doesn't crash
WP-CLI in CI/CD Pipelines
The Basic Idea
CI/CD pipelines (GitHub Actions, GitLab CI, Bitbucket Pipelines, etc.) automate what happens after you push code. WP-CLI fits naturally into the deploy step — after files are transferred, you run WP-CLI commands to bring the environment in sync.
Common Pipeline Tasks with WP-CLI
| Task | Command |
|---|---|
| Run DB migrations | wp core update-db |
| Flush rewrite rules | wp rewrite flush |
| Clear object cache | wp cache flush |
| Activate/deactivate plugins | wp plugin activate <slug> |
| Sync options across environments | wp option update <key> <value> |
| Verify WordPress is healthy | wp core verify-checksums |
Example — GitHub Actions Deploy Step
# .github/workflows/deploy.yml
- name: Deploy to Staging
run: |
ssh user@staging.example.com << 'EOF'
cd /var/www/html
# Pull latest code
git pull origin main
# Run WP-CLI post-deploy tasks
wp core update-db
wp rewrite flush
wp cache flush
wp plugin activate my-custom-plugin
echo "✅ Deploy complete"
EOF
Use Case: Environment Sync on Deploy
When deploying from local → staging → production, option values often differ (API keys, URLs, feature flags). Instead of manually editing the DB:
# Set the correct API base URL for this environment
wp option update api_base_url 'https://staging.example.com/api'
# Enable maintenance mode plugin only on production
wp plugin activate wp-maintenance-mode --url=production.example.com
💡 Keep it simple in pipelines. Only run idempotent commands — things that are safe to re-run if the pipeline retries.
flush,update-db, andoption updateare all safe. Avoid commands that delete data.
Managing Remote WordPress Sites
WP-CLI isn't limited to the machine you're sitting at. With SSH aliases, you can run commands against any remote WordPress site as if you were logged into the server.
Setting Up SSH Aliases
Add this to your wp-cli.yml file in your project root (or ~/.wp-cli/config.yml for global use):
# wp-cli.yml
@staging:
ssh: user@staging.example.com/var/www/html
@production:
ssh: user@production.example.com/var/www/html
Now prefix any command with the alias:
# Run a command on staging
wp @staging plugin list
# Flush cache on production
wp @production cache flush
# Search-replace on staging DB
wp @staging search-replace 'http://' 'https://' --all-tables
Managing Multiple Sites at Once
# Update all plugins on both environments
wp @staging plugin update --all
wp @production plugin update --all
# Check core version across all environments
wp @staging core version
wp @production core version
⚠️ Always run on staging first. Confirm the output is what you expect — then run on production.
Useful Flags for Remote Work
| Flag | Purpose |
|---|---|
--ssh=user@host/path |
One-off remote command without an alias |
--skip-plugins |
Run without loading plugins (useful when a plugin is broken) |
--skip-themes |
Same as above for themes |
--quiet |
Suppress output — good for cron jobs |
--url=<domain> |
Target a specific site in a multisite network |
The --dry-run Safety Net
What Is --dry-run?
--dry-run is a flag supported by several WP-CLI commands that lets you preview exactly what will happen — without actually doing it.
Think of it as reading the recipe out loud before you start cooking. You catch mistakes before they cost you.
# See what would be replaced — nothing changes in the DB
wp search-replace 'http://oldsite.com' 'https://newsite.com' --all-tables --dry-run
The output shows every table and row that would be affected, with a count — but the database is untouched.
Why --dry-run Is Your Safest Habit
When you're working with WP-CLI, there is no undo button. A wrong search-replace or an accidental delete is permanent unless you have a backup. --dry-run gives you a confirmation step before anything irreversible happens.
Here's why it matters in practice:
- Catches scope creep — you might expect 10 rows to change, but dry-run shows 1,400. That's a signal to narrow your query.
- Validates your syntax — a typo in your command fails safely instead of corrupting data.
- Builds confidence — especially useful when running commands on production for the first time.
- Documents what will happen — share the dry-run output with a teammate for a quick review before committing.
🔒 Best practice: Make
--dry-runthe first run, always. Only move to the real command after the preview looks exactly right.
Real-World Use Cases
Use Case 1 — Safe Site Migration
Before doing a full URL swap after moving a site:
# Step 1: Preview — how many rows will change?
wp search-replace 'https://old-domain.com' 'https://new-domain.com' \
--all-tables \
--dry-run
# Output shows: 47 replacements across 6 tables
# Looks right — now run for real:
# Step 2: Commit
wp search-replace 'https://old-domain.com' 'https://new-domain.com' \
--all-tables
Use Case 2 — Bulk Post Deletion Preview
Before deleting old draft posts:
# Step 1: Dry run — list what would be deleted
wp post list --post_status=draft --format=count
# Shows: 83 posts
# Step 2: Delete only if count looks right
wp post delete $(wp post list --post_status=draft --format=ids) \
--force
Use Case 3 — Plugin Update Safety Check
Before mass-updating on production:
# Check what has updates available (non-destructive by nature)
wp plugin update --all --dry-run
# Review the list — then update for real
wp plugin update --all
Final Thoughts
WP-CLI's true value isn't any single command — it's the compounding effect of everything working together. Custom commands give you project-specific tools. CI/CD integration makes deployments repeatable. SSH aliases let you manage any site from one terminal. And --dry-run means you can move fast without breaking things.
If Part 1 was about getting started, Part 2 is about building trust in your own workflow. The developers who get the most out of WP-CLI are the ones who start scripting their own patterns — and this is exactly how you do it.
🔗 Missed Part 1? Start here → — installation, must-know commands, and your first shell script.
Found this useful? Drop a ❤️ and share it with someone who's still clicking through wp-admin.
Top comments (0)