When an eval-file script starts wanting more than three arguments, the job has outgrown a script. Promote it to a command and WP-CLI takes over argument parsing, validation, help text and output formatting for you.
Registration is three lines
Put it in a plugin, behind a guard.
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'acme checklist', 'Acme_CLI_Checklist' );
}
Pass a class and its public methods become subcommands: run() becomes wp acme checklist run, list() becomes wp acme checklist list. If you want no subcommands at all, define __invoke() and nothing else.
Putting it in a plugin rather than the theme matters. A command is an operations tool, and your operations tools should not vanish on the day you swap themes.
Your docblock is the help text
WP-CLI reads each method’s PHPDoc to build both the wp help screen and the argument validation rules. Writing the comment is defining the interface.
/**
* Run the site diagnostic.
*
* ## OPTIONS
*
* [--format=<format>]
* : Output format.
* ---
* default: table
* options:
* - table
* - json
* ---
*
* ## EXAMPLES
*
* wp acme checklist run --format=json
*
* @when after_wp_load
*/
public function run( $args, $assoc_args ) { /* … */ }
$args holds positional arguments and $assoc_args the flags. This is where the restriction from part four disappears — a real command takes -- flags freely, and WP-CLI rejects values outside the declared options before your code ever runs.
Use log, warning and error deliberately
This is the heart of the part. The four differ in where they write and what they do next.
The deciding question is always “is there any point continuing from here?”
One item failing inside a loop over 800 records is a warning. Calling error there stops at the first failure and leaves you knowing nothing about the other 799. Conversely, a missing API credential that guarantees every item will fail is an error — leave it as a warning and the same message floods the log 800 times with the real cause buried inside it.
The distinction is a contract rather than a preference. As part eight shows, CI reads exit codes, not prose. A command that ends in error fails the build; a warning only lands in the log. Choosing between them here is really choosing what counts as a reason to block a deploy.
Output formats and confirmation
When the result is tabular, hand it to the utility rather than printing it yourself.
WP_CLIUtilsformat_items( $assoc_args['format'], $rows, [ 'item', 'status', 'note' ] );
That single line gives you table, CSV, JSON and YAML together. The moment --format=json works, your command becomes input for another script — a part in a pipeline rather than a destination.
Anything irreversible deserves a confirmation step.
WP_CLI::confirm( 'Really delete these?', $assoc_args );
Passing $assoc_args through is the trick: it lets --yes skip the prompt, so the guard protects people without standing in the way of automation.
The diagnostic commands we built this way are published with their source on the free tools page, and the reasoning behind them is collected in the development workflow archive.
Next part
Having built a command, you need it to run on a schedule. The next part explains why WordPress’s own cron depends on traffic, and how to move it to system cron.