Have you ever found yourself going into the WordPress admin area to update themes, plugins, and WP core? Of course you have. Have you been asked, “Can you create/update/delete all the users on this CSV file?” I’m sure you’ve run into that too. Have you tried migrating a site and wished there were a plugin or third-party tool you could reach for to do the job? I know I have!

Automating Repetitive Tasks with WP-CLI

There is a very powerful tool available to help you with these tasks and more. Before I tell you about it, I would like to set up a quick anecdote.

The Problem: In a recent project, there were several programmatic tasks I needed to repeat on a regular basis. One task in particular involved updating user-level permissions based on evidence of membership-level purchase or subscription. If the company couldn’t find a payment from the user for a particular membership level, they wanted the membership level removed from the user. Why was this needed? Perhaps a member stopped a subscription, but an event did not fire, and so the member still has access even though they’re not paying for it (yikes!). Or perhaps someone was on a trial offer, but that offer expired and the client still has a subscription (also yikes!).

The Solution: Instead of going into the admin panel and manually deleting hundreds (maybe thousands) of subscriptions, I opted to reach for one of my favorite WordPress tools, WP-CLI, which fixed the problem in a few keystrokes.

In this post, I want to introduce you to WP-CLI (assuming you are not already close friends), walk you through a simple custom command I wrote for this particular situation, and give you some ideas and resources for using WP-CLI in your own development.

What Is WP-CLI?

If you have never heard of WP-CLI before, you’re not alone. The project, while several years old, seemed to fly under the WordPress radar for a while. Here’s a brief description of what WP-CLI is and does from the official website:

“WP-CLI is a set of command-line tools for managing WordPress installations. You can update plugins, set up multisite installs and much more, without using a web browser.”

The following commands show you the power of WP-CLI out of the box:

  • wp plugin update --all updates all updateable plugins.
  • wp db export exports a SQL dump of your database.
  • wp media regenerate regenerates thumbnails for attachments (e.g., after you change sizing in your theme).
  • wp checksum core verifies that WordPress core files have not been tampered with.
  • wp search-replace searches for and replaces strings in the database.

If you explore more commands here, you will see there are plenty of available commands for repetitive tasks every WordPress developer or site maintainer does on a daily or weekly basis. These commands have saved me countless hours of pointing, clicking, and waiting for page reloads over the course of the year.

Are you convinced? Ready to get started? Great!

You will need to have WP-CLI installed with your WordPress (or globally on your local machine). If you have not yet installed WP-CLI on your local development environment, installation instructions can be found on the website here. If you’re using Varying Vagrant Vagrants (VVV2), WP-CLI is included. Many hosting providers also have WP-CLI included on their platform. I will assume you have this successfully installed moving forward.

Using WP-CLI to Solve the Problem

To solve the problem of the repetitive tasks, we need to make a custom WP-CLI command available to our WordPress install. One of the easiest ways to add functionality to any site is to create a plugin. We will use a plugin in this instance for three main reasons:

  1. We will be able to turn off the custom command if we do not need it
  2. We can easily extend our commands and subcommands all while keeping things modular.
  3. We can maintain functionality across themes and even other WordPress installs.

Creating the Plugin

To create a plugin, we need to add a directory to our /plugins directory in our wp-content directory. We can call this directory toptal-wpcli. Then create two files in that directory:

  • index.php, which should only have one line of code: <?php // Silence is golden
  • plugin.php, which is where our code will go (You can name this file whatever you want.)

Open the plugin.php file and add the following code:

 * Plugin Name: TOPTAL WP-CLI Commands
 * Version: 0.1
 * Plugin URI: https://n8finch.com/
 * Description: Some rando wp-cli commands to make life easier...
 * Author: Nate Finch
 * Author URI: https://n8finch.com/
 * Text Domain: toptal-wpcli
 * Domain Path: /languages/
 * License: GPL v3
 * You can of course take the code and repurpose it:-).
if ( !defined( 'WP_CLI' ) && WP_CLI ) {
    //Then we don't want to load the plugin

There are two parts to these first several lines.

First, we have the plugin header. This information is pulled into the WordPress Plugins admin page and allows us to register our plugin and activate it. Only the plugin name is required, but we should include the rest for anyone who might want to use this code (as well as our future selves!).

Second, we want to check that WP-CLI is defined. That is, we are checking to see if the WP-CLI constant is present. If it is not, we want to bail and not run the plugin. If it is present, we are clear to run the rest of our code.

In between these two sections, I’ve added a note that this code should not be used “as is” in production, since some of the functions are placeholders for real functions. If you change these placeholder functions to real, active functions, feel free to delete this note.

Adding the Custom Command

Next, we want to include the following code:

class TOPTAL_WP_CLI_COMMANDS extends WP_CLI_Command {

	function remove_user() {

		echo "\n\n hello world \n\n";



WP_CLI::add_command( 'toptal', 'TOPTAL_WP_CLI_COMMANDS' );

This block of code does two things for us:

  1. It defines the class TOPTAL_WP_CLI_COMMANDS, which we can pass arguments into.
  2. It assigns the command toptal to the class, so we can run it from the command line.

Now, if we execute wp toptal remove_user, we see:

$ wp toptal hello

 hello world

This means our command toptal is registered and our subcommand remove_user is working.

Setting Up Variables

Since we are bulk processing removing users, we want to set up the following variables:

// Keep a tally of warnings and loops
  $total_warnings = 0;
  $total_users_removed = 0;

// If it's a dry run, add this to the end of the success message
  $dry_suffix = '';

// Keep a list of emails for users we may want to double check
  $emails_not_existing = array();
  $emails_without_level = array();

// Get the args
  $dry_run = $assoc_args['dry-run'];
  $level = $assoc_args['level'];
  $emails = explode( ',', $assoc_args['email'] );

The intent of each of the variables is as follows:

  • total_warnings: We will tally a warning if the email does not exist, or if the email is not associated with the membership level we are removing.
  • $total_users_removed: We want to tally the number of users removed in the process (see caveat below).
  • $dry_suffix: If this is a dry run, we want to add wording to the final success notice.
  • $emails_not_existing: Stores a list of emails that do not exist.
  • $emails_without_level: Stores a list of emails that do not have the specified level.
  • $dry_run: A boolean that stores whether the script is doing a dry run (true) or not (false).
  • $level: An integer representing the level to check and possibly remove.
  • $email: An array of emails to check against the given level. We will loop through this array

With our variables set, we are ready to actually run the function. In true WordPress fashion, we will run a loop.

Writing the Function Itself

We start by creating a foreach loop to cycle through all the emails in our $emails array:

// Loop through emails
foreach ( $emails as $email ) {

	// code coming soon

} // end foreach

Then, we add a conditional check:

// Loop through emails
foreach ( $emails as $email ) {
	//Get User ID
	$user_id = email_exists($email);

	if( !$user_id ) {

		WP_CLI::warning( "The user {$email} does not seem to exist." );

		array_push( $emails_not_existing, $email );


} // end foreach

This check ensures we have a registered user with the email we are checking. It uses the email_exists()function to check if there is a user with that email. If it does not find a user with that email, it throws a warning so that we know on our terminal screen that the email was not found:

$ wp toptal remove_user --email=me@me.com --dry-run

Warning: The user me@me.com does not seem to exist.

The email is then stored in the $emails_not_existing array for display later. Then we increment the total warning by one and continue through the loop to the next email.

If the email does exist, we will use the $user_id and the $level variables to check if the user has access to the level. We store the resulting boolean value in the $has_level variable:

// Loop through emails
foreach ( $emails as $email ) {
	//Get User ID
	$user_id = email_exists($email);

	if( !$user_id ) {

		WP_CLI::warning( "The user {$email} does not seem to exist." );

		array_push( $emails_not_existing, $email );



	// Check membership level. This is a made up function, but you could write one or your membership plugin probably has one.
	$has_level = function_to_check_membership_level( $level, $user_id );

} // end foreach

Like most functions in this example, this function_to_check_membership_level() function is fabricated, but most membership plugins should have helper functions to get you this information.

Now, we’ll move on to the main action: removing the level from the user. We will use an if/else structure, which looks like this:

foreach ( $emails as $email ) {

	// Previous code here...

	// Check membership level. This is a made up function, but you could write one or your membership plugin probably has one.
	$has_level = function_to_check_membership_level( $level, $user_id );

	if ( $has_level ) {

		if ( !$dry_run ) {

		// Deactivate membership level. This is a made up function, but you could write one or your membership plugin probably has one.
			function_to_deactivate_membership_level( $level, $user_id, 'inactive' );


		WP_CLI::success( "Membership canceled for {$email}, Level {$level} removed" . PHP_EOL );


	} else {

		WP_CLI::warning( "The user {$email} does not have Level = {$level} membership." );

		array_push( $emails_without_level, $email );


	// We could echo something here to show that things are processing...

} // end foreach

If the value of $has_level is “truthy,” meaning the user has access to the membership level, we want to run a function to remove that level. In this example, we will use the function_to_deactivate_membership_level()function to perform this action.

However, before we actually remove the level from the user, we want to enclose that function in a conditional check to see if this is actually a dry-run. If it is, we do not want to remove anything, only report that we did. If it is not a dry-run, then we will go ahead and remove the level from the user, log our success message to the terminal, and continue looping through the emails.

If, on the other hand, the value of $has_level is “falsey,” meaning the user does not have access to the membership level, we want to log a warning to the terminal, push the email to the $emails_without_levelarray, and continue looping through the emails.

Finishing Up and Reporting

Once the loop has finished, we want to log our results to the console. If this was a dry run, we want to log an extra message to the console:

if ( $dry_run ) {
	$dry_suffix = 'BUT, nothing really changed because this was a dry run:-).';

This $dry-suffix will be appended to the warnings and success notifications that we log next.

Finishing up, we want to log our results as a success message and our warnings as warning messages. We will do so like this:

WP_CLI::success( "{$total_users_removed} User/s been removed, with {$total_warnings} warnings. {$dry_suffix}" );

if ( $total_warnings ) {

	$emails_not_existing = implode(',', $emails_not_existing);
	$emails_without_level = implode(',', $emails_without_level);


		"These are the emails to double check and make sure things are on the up and up:" . PHP_EOL .
		"Non-existent emails: " . $emails_not_existing . PHP_EOL .
		"Emails without the associated level: " . $emails_without_level . PHP_EOL


Note that we are using the WP_CLI::success and WP_CLI::warning helper methods. These are provided by WP-CLI for logging information to the console. You can easily log strings, which is what we do here, including our $total_users_removed$total_warnings, and $dry_suffix variables.

Finally, if we did accrue any warnings throughout the runtime of the script, we want to print that information to the console. After running a conditional check, we convert the $emails_not_existing and $emails_without_level array variables into string variables. We do this so we can print them to the console using the WP_CLI::warning helper method.

Adding a Description

We all know comments are helpful to others and to our future selves going back to our code weeks, months, or even years later. WP-CLI provides an interface of short descriptions (shortdesc) and long descriptions (longdesc) which allows us to annotate our command. We will put at the top of our command, after the TOPTAL_WP_CLI_COMMANDS class is defined:

 * Remove a membership level from a user
 * --level=<number>
 * : Membership level to check for and remove
 * --email=<email>
 * : Email of user to check against
 * [--dry-run]
 * : Run the entire search/replace operation and show report, but don't save changes to the database.
 * wp toptal remove_user --level=5 --email=me@test.com,another@email.com, and@another.com --dry-run
 * @when after_wp_load

In the longdesc, we define what we expect our custom command to receive. The syntax for the shortdesc and longdesc is Markdown Extra. Under the ## OPTIONS section, we define the arguments we expect to receive. If an argument is required, we wrap it in < >, and if it is optional, we wrap it in [ ].

These options are validated when the command is run; for example, if we leave out the required email parameter, we get the following error:

$ wp toptal remove_user --level=5 --dry-run
Error: Parameter errors:
 missing --email parameter (Email of user to check against)

The ## EXAMPLES section includes an example of what the command could look like when being called.

Our custom command is now complete. You can see the final gist here.

A Caveat and Room for Improvement

It is important to review the work that we have done here to see how the code could be improved, expanded and refactored. There are many areas of improvement for this script. Here are some observations about improvements that could be made.

Occasionally, I have found this script will not remove all the users it logs as “removed.” This is most likely due to the script running faster than the queries can execute. Your experience may vary, depending on the environment and setup in which the script is run. The quick way around this is to repeatedly run with the same inputs; it will eventually zero out and report that no users have been removed.

The script could be improved to wait and validate that a user has been removed before logging the user as actually removed. This would slow down the execution of the script, but it would be more accurate, and you would only have to run it once.

Similarly, if there were errors found like this, the script could throw errors to alert that a level had not been removed from a user.

Another area to improve the script is to allow for multiple levels at a time to be removed from one email address. The script could auto-detect if there were one or more levels and one or more emails to remove. I was given CSV files by level, so I only needed to run one level at a time.

We could also refactor some of the code to use ternary operators instead of the more verbose conditional checks we currently have. I have opted to make this easier to read for the sake of demonstration, but feel free to make the code your own.

In the final step, instead of printing emails to the console in the final step, we could also automatically export them to a CSV or plain text file

Finally, there are no checks to make sure we’re getting an integer for the $level variable or an email or comma-separated list of emails in the $emails variable. Currently, if someone were to include strings instead of integers, or user login names instead of emails, the script would not function (and throw no errors). Checks for integers and emails could be added.

Ideas for Further Automation and Further Reading

As you can see, even in this specific use case, WP-CLI is quite flexible and powerful enough to help you get your work done quickly and efficiently. You may be wondering to yourself, “How can I begin to implement WP-CLI in my daily and weekly development flow?”

There are several ways you can use WP-CLI. Here are some of my favorites:

  • Update themes, plugins, and WP core without having to go into the admin panel.
  • Export databases for backup or perform a quick SQL dump if I want to test a SQL query.
  • Migrate WordPress sites.
  • Install new WordPress sites with dummy data or custom plugin suite setups.
  • Run checksums on core files to make sure they have not been compromised. (There is actually a project underway to expand this to themes and plugins in the WP repo.)
  • Write your own script to check, update, and maintain site hosts (which I wrote about here).

The possibilities with WP-CLI are just about limitless. Here are some resources to keep you moving forward:

This article is written by Nathan Finch and originally published at Toptal

About the Author:  Nathan works primarily in WordPress front-end development—consulting and implementing with Genesis and WooCommerce for SME businesses and enterprise level projects. He uses WordPress, HTML, CSS, PHP, and JavaScript on a daily basis as well as Sass, Git, jQuery, and Grunt. Nathan is constantly exploring and experimenting with other front-end technologies and frameworks like Angular and React.


Here are the top 15 places to find a WordPress developer:

1. Toptal

Toptal is a matching service, initially created with only tech talent in mind. Although it has expanded its pool of talent to include designers and finance experts, the company’s bread and butter is its developer vertical. If you want to be sure that a WordPress developer is up to the job, hiring an exceptional developer from Toptal is likely your best option.

Why? Toptal boasts an elite developer base. Their trademark system for vetting talent allows for only the best to become a part of their community. According to Toptal, only 3% of applicants make it through their battery of technical tests and their comprehensive vetting process.

2. Codeable.io

Codeable.io is a curated marketplace of WordPress developers. Unlike most other freelance marketplaces, Codeable focuses on a niche and only retains WordPress developers. According to Codeable, over 64,000 jobs have been completed by their developers with a satisfaction rate of nearly 99%.

Codeable’s standards for its pool of developers is a cut above most freelance marketplaces. The site boasts that its 300+ WordPress freelancers comprise the top 2% of applicants.

3. Hired

Hired helps employers find software engineers and developers. On Hired, you can use their pipeline to find custom matches. You can create a company profile, search for candidates using their search algorithm (which can eliminate gender and racial identifiers for fairer hiring), and request interviews with candidates.

What’s best about Hired? It’s great for finding specialized WordPress developers who are actively searching for new opportunities, have relevant experience (as most candidates on Hired have at least two years of experience), and are in your area.

4. GitHub Jobs

Don’t waste your time perusing large job boards like Monster and Indeed. You’ll have far better luck with job boards geared toward tech talent. GitHub has a massive developer community as it’s one of the largest open-source online repositories for coders. For a relatively small fee, you can post a WordPress developer job listing and gain a great deal of exposure on GitHub’s huge developer community.

5. Stack Overflow

Stack Overflow has an online community that rivals GitHub. Arguably, it’s the absolute largest and most trusted community of developers on the web. Stack Overflow is often used as a resource for all kinds of developers, novice to expert, seeking to learn more about coding. Their job board, like GitHub’s, allows for an incredible amount of exposure to dedicated WordPress developers around the world.

6. Upwork

If recruiting services and job boards aren’t your thing, you might want to consider a freelance marketplace like Upwork.

Upwork has one of the largest marketplaces with millions of registered freelancers. You can hire contractors for a few simple coding tasks or begin a long-term relationship with a series of complex WordPress projects. If you like the idea of finding, interviewing, and managing freelancers, Upwork’s colossal marketplace will likely meet your needs.

7. People Per Hour

People Per Hour is another freelance marketplace akin to Upwork. What makes People Per Hour unique is that it holds contests and allows Freelancers to post their own job postings called hourlies.

People Per Hour has millions of members, thousands of confirmed hours, and a plethora of success stories from freelancers and entrepreneurs alike. The ease of posting jobs, contacting freelancers, and paying for hours worked makes People Per Hour a superb choice for employers interested in searching for and vetting freelance candidates themselves.

Additionally, with People Per Hour, you can connect with local freelancers, so you aren’t necessarily limited to remote talent.

8. WP Hired

WP Hired is a fairly large job board with thousands of job listings for WordPress-related job openings. WordPress jobs are organized by category. You can post job listings under WordPress migration, performance, plugin development, theme development, and programming.

You can post one job listing for free on WP Hired. Monthly plans that expand upon the number of listings and available features are available as well.

9. Gun.io

Gun.io has a growing community of developers over 25,000 strong. Like Toptal, their service is designed to take the tedium out of hiring. Gun.io vets their talent and ensures that their freelancers are committed to each and every project.

Gun.io prides itself on its humanism. Every employer is connected with a VP, instead of a sales representative, and freelancers are provided with the resources to succeed.

What’s most alluring about the network? Gun.io manages and replaces talent – with no risk to you – and back every single hour worked with a 100% money-back guarantee.

10. Guru

Guru has a large global network of freelancers—albeit smaller than Upwork’s and People Per Hour’s massive pool. You can explore the profiles of 1.5 million gurus, propose projects, and pay your hired talent with their secure SafePay system.

Guru isn’t focused on developers, let alone Angular developers, as it is a freelance network comprised of every sort of professional. So, like with Upwork and People Per Hour, you’ll have to narrow your search yourself. Also, as with many freelance networks, the vetting and interviewing will be up to you.

11. Freelancer

Freelancer is an absolutely massive marketplace with 25 million registered users, 12 million total posted jobs, and thousands of completed projects. With the size of the marketplace comes a unique challenge, however. Finding the perfect WordPress developer is like finding a needle in a haystack.

Although website development is one of the most popular job categories on Freelancer, you still will have to search through thousands of Freelancer profiles, vet and interview candidates yourself, and manage payments yourself.

If you’re looking for an affordable option, however, Freelancer is a wonderful hiring solution. For more long-term commitments, you’ll want to consider matching services like Toptal, Codeable.io, or Gun.io.

12. WordPress Jobs

WordPress Jobs publishes only WordPress-related job listings. So, while GitHub and StackOverflow’s job boards give you access to giant developer communities, WordPress-specific job boards help you pinpoint only WordPress developers.

In addition, posting on WordPress Jobs is free, making it an affordable option for small businesses.

13. Find Bacon

Find Bacon is a job board aimed at eliminating the hassle of searching for design and development jobs. Find Bacon is a pleasant alternative to massive job boards like Indeed and Monster and is highly affordable. Posting a job posting for a 30-day period is only $99 dollars.

They also offer subscription packs which allow for 10 job posts a month. If you’re a company looking to fill multiple positions or are planning on hiring freelancers on an ongoing basis, you may want to consider investing in a subscription pack for a niche job board like Find Bacon.

14. X-Team

X-Team matches you with qualified WordPress developers who receive mentorship and educational resources just for being a part of X-Team. Like Toptal and Gun.io, they do the heavy lifting of hiring. You won’t be saddled with rifling through resumes or preparing personalized interview scripts.

The major downside of using X-Team is that, as their name suggests, they are adept at organizing teams. If you’re only looking to hire an individual WordPress developer, you’ll want to use a different matching service.

15. College Recruiter

If you’re looking to fill a part-time, entry-level position, or internship, College Recruiter is a good place to connect with college students and recent grads. This is an especially good option if you happen to constrained to a tight budget and have flexibility in terms of a hiring schedule.

Again, you’ll have to do the hard work of vetting and interviewing, but if interviewing hires doesn’t sound too daunting, College Recruiter is a good place to search for entry-level talent.

Honorable Mention: SimplyHired

SimplyHired is a large job board. It’s similar to big, general job boards like Indeed or Monster. The site comes with loads of resources from salary recommendations to hiring guides, and offers low prices for job listings. Like with Indeed and Monster, you’ll get a great deal of exposure. With over a billion job applications delivered, SimplyHired is a highly-respected job board worth investigating.

Begin exploring salary estimators, post within a network of over a hundred job boards in record time, and browse through the collated jobs by cities to see if posting a job listing on SimplyHired is worth the time and money for you.

Choosing the right site

Finding the best sites to find developers is no easy endeavor. Unless you’re a battle-worn recruiter, you likely won’t know how to navigate the complexities of hiring a WordPress developer. That’s completely okay—there’s plenty of sites and services to help you along the way.

Matching services like Toptal, and to a lesser extent, Codeable.io and Gun.io, are great solutions for employers searching for tech talent, and for those who are looking to place their trust in experienced tech professionals. For those short on time with high-quality developers as a priority, Toptal and Codeable.io are superb choices.

On the other end of the spectrum, there are freelance marketplaces like Upwork, People Per Hour, and Freelancer that allow you to cast a much wider net for WordPress developers.

Employers looking for full-time developers may also benefit from utilizing Stack Overflow and GitHub’s job boards, which can provide wonderful exposure to the WordPress developer community.

Specialized job boards like WPHired and WordPress Jobs can help you pinpoint the developers you need for your project.

Freelance marketplaces like Upwork, Freelancer, and Guru allow you to instantly connect with developers, but you’ll have to care for the hiring details yourself. If you have ample time to devote to screening candidates and are confident in your ability to interview WordPress developers, they are a great choice. Otherwise, you should steer clear from marketplaces and job boards alike.

Job boards, marketplaces, and matching services all have their uses. Which site will best serve you will depend on your specific situation.

Ultimately, which sites you employ depends on a multitude of factors, such as:

  • How quickly you need to hire a WordPress developer (i.e. your timeline)
  • How much experience you have hiring WordPress developers
  • Whether or not you’re equipped to test technical skills
  • How many developers you need to bring on
  • What level of experience those WordPress developers need
  • Whether or not you’re open to remote workers
  • What your budget constraints are
  • How important quality is to your project(s)

Source: DevelopersforHire.com