Get it All
Together

I’ve been struggling to understand exactly how the cron functions of WordPress work.

I’ve been working on a plugin for some friends of mine that need a mailing list manager, this plugin in turn requires the ability to schedule tasks. So, I spent a long time puzzling over the various bits of documentation and posts on other blogs about WP-Cron, pouring over code and experimenting with my own, until I finally got a working plugin.

And while I’m sure that the information that is out there now about Cron functions is adequate for some, it’s clear to me that others may need a different set of explanations, which is where this blog post comes in. I’m going to attempt to lay out the WP-Cron world in a different way in hopes of improving slightly on what’s out there.

Let me start by saying that the best article I’ve read on the subject of crons is definitely “Timing is everything: scheduling in WordPress,” by Glenn Slaven. I highly recommend that, even before reading this article, you take a gander at that one. There are really just a few things on which I would like to expand and revise slightly, because they threw me off. I’m also slightly reorganizing the order of discussion into something more akin to a step-by-step set of instructions.

Editor Update: Ack! Looks like this other article is no longer available. Sorry, everybody!

The Scenario

The example we’re going to work with is the mailing list manager plugin I’m developing. We’re not going to get into too much of the actual code of that plugin, but the scenario is doubtless a common need. We’ll be assuming that we’re working with a blog which has many, many users. (real world example: 400+) We want to keep our users up to date with periodic email blasts. Rather than those blasts being separated from the regular content, we want to specify that certain posts will become both posts on the site and email messages.

But of course, sending emails out to 400+ recipients at once is going to make the SMTP server very, very suspicious. In order to avoid problems with the mail server, we need to throttle the messages by sending blasts every twenty minutes ((or at least, approximately twenty minutes, remember that WordPress cannot be exact about timing)) until the last user group has been emailed. To do this, we will be using the wp_schedule_single_event() function within the WP-Cron suite of tools.

Scheduling: What You’ll Need

In order to schedule an event of any kind, you’re going to need a few basic components. We’ll get into more detail further down the line, but it’s important to be able to identify the landmarks as we proceed:

  • Your Cron Function ~ This is the actual function that does the stuff you want when the scheduled event is fired
  • A Custom Action Hook ~ Here’s where we get into the nitty-gritty of how WordPress operates. It’s very simple, actually, but understanding how to create custom action hooks is key to understanding WordPress cron functions. And a great deal more about WordPress!!
  • Your Scheduling Function ~ This is the function that initially assigns the scheduled task.

Your Cron Function

my_scheduled_function([$arg1, [$arg2, [$arg3. . . ]]])

This function lies at the heart of your scheduling world! It is here that you put all the stuff you want to get done at a specified time. In our example, this is where you actually send the email to your latest batch of users. Note that this function can include an arbitrary number of arguments. We’ll get into how you pass those arguments on to the function later. For now, just notice that this is a standard function like pretty much any other. In fact, in the case of the mailing list manager I created, it’s as simple a function as you could ask for:

function bhd_cron_send($sender_email, $subject, $mail_text, $send_headers) {
	wp_mail($sender_email, $subject, $mail_text, $send_headers);
}

It’s probably advisable to create your function and make sure it works without adding in the cron functionality first. This is because a) there’s no need adding extra complexity to the system until you’re sure the function works and b) you’ll need to know how many arguments will need to be passed to the function and in what order.

Your Custom Action Hook

This is what really threw me. I’ve never created my own custom action hooks and never really understood them, but you’ll need to in order to get things done with cron, so this is important. Creating a custom hook is as simple as typing the following:

add_action('my_custom_hook', 'some_function' [, $priority, [$num_args]]);

WordPress assumes that anything added as the first argument exists without any additional registration of that new hook. In other words, ‘my_custom_hook’ actually is the declaration of the action hook in this example. The second argument should be a valid function name and will be the function associated with ‘my_custom_hook’ whenever ‘my_custom_hook’ is invoked anywhere in the system.

Obviously, since you’re writing your custom action, you’ll also need to invoke that custom action somewhere. That’s the last piece of the puzzle, which is explained down the page a bit.

But before we leave this topic, it’s worth pointing out that, just like any other action, it’s entirely possible to hook more than one function into the custom action you’ve created. It’s also possible to invoke the hook somewhere else in your code, separate from your Cron function. This bit of WordPress theory may prove helpful to you down the line, so it’s just an FYI.

The last two arguments of the add_action function are important. Recall the number of arguments you needed for your scheduling function. That’s the number you’re going to want to put in the $num_args add_action parameter. Otherwise, add_action doesn’t look for and doesn’t accept any arguments provided, which will doubtless render your function either inoperable or ineffective.

Late Update: Per jeremyclark in the comments, this is not really the reason to mess with priority! Thanks Jeremy! If you’re function is particularly computationally expensive (if it’s complex and relies on a lot of processing), it might be worth it to back the priority down a bit. You can do this by setting the $priority argument. The default is 10. In our case, the actual function is trivial, so this is what the add_action looks like for our mailing list:

add_action( 'bhd_cron_send_hook', 'bhd_cron_send', 10, 4 );

Your Scheduling Function

Finally, there is the function which will do the scheduling. Depending on the complexity of your plugin, this may not be very complex at all. But the one thing that is required of your function is that it bundle up all the arguments to your cron function into an associative array in the proper order. In our example, the parameters for our sending function are $sender_email, $subject, $mail_text, $send_headers. Thus, we create an array like so:

$mail_bundle = array(
     'sender_email' => $sender_email,
     'subject' => $subject,
     'mail_text' => $mailtext,
     'send_headers' => $send_headers
);

Next, we need to assign the scheduled task. For this, we use our custom action hook. Now we understand why it was so important to pay close attention to the number of parameters and their order: the hook only allows you to set the number of arguments and the function – which is now removed by one degree of separation from the schedule – needs those arguments sent to it in the proper order. We call the WordPress scheduling function like so:

wp_schedule_single_event(time()+$time, 'bhd_cron_send_hook', $mail_bundle);

The first argument is simply the Unix system time plus an interval of seconds after which we would like the scheduled task to be fired. Since we want the first of our batch email events to happen twenty or so minutes after the post has been published, $time was defined as follows:

$time = rand(1000, 1200);

“Wait,” you say, “why the random number?”

If you look at the multi-dimensional array of scheduled events created by the WP-Cron system, you will find that the primary key for identifying events is the Unix timestamp. The second-level identifier is an MD5 of the arguments. In our example, it’s possible that someone could inadvertently set up the cron events twice by correcting the post after they’ve published it: both events fire the ‘publish_post’ event, which the mailing list uses to start it’s process.

However it happens, the possibility is that two events could have exactly the same timestamp and arguments, which would mean that one overwrites the other in the array. To avoid this, I included the slight randomization, which only alters the schedule by a maximum of three minutes.

So, now let’s look at the entire process in code (with some pseudo-code) in order to understand how everything fits together:

//======================================
// Description: Our scheduling function.  Sends the email in 50-recipient chunks
function bhd_schedule_message($sender_email, $subject, $list, $headers, $mailtext) {
// Establish our random time interval between the publishing of the post and the first email blast:
	$time = rand(1000, 1200);
	for($x = 0; $x  $sender_email,
				'subject' => $subject,
				'mail_text' => $mailtext,
				'send_headers' => $send_headers
			);
// schedule the email blast, increment the $time variable by twenty minutes for the next loop:
			wp_schedule_single_event(time()+$time, 'bhd_cron_send_hook', $mail_bundle);
			$time = $time + 1200;
		}
	}
}

//======================================
// Description: here is our actual cron job.
function bhd_cron_send($sender_email, $subject, $mail_text, $send_headers) {
	wp_mail($sender_email, $subject, $mail_text, $send_headers);
}

//======================================
// Description: Finally, here is our custom action.
add_action( 'bhd_cron_send_hook', 'bhd_cron_send', 10, 4 );

Note: we have not included the function that will be called when the post is published, because of course we’re not dealing with adding actions onto that particular hook. But for reference’s sake, such a function would look something like this:

add_action('publish_post', 'my_mailinglist_function');

This may be apropos of nothing for a lot of people, but just as a helpful little hint, you can get the theme used by a blog by calling the get_option() method thusly:

get_option('current_theme');

I’m using this on a plugin that changes the colors of certain objects (not CSS directable) by checking the current theme and setting matching colors. Think “custom color YouTube video window.”

MU admins, you could use get_site_option() if you’re planning on doing something global.

I’ve been meaning to incorporate the force_balance_tags() function into a few plugins that I’ve created which sample text from posts, but haven’t gotten to it yet. Well, today’s the day, and I’m going to experiment by writing some stuff in italics to see if it screws up my formatting on the page that uses the plugin.

The balance tags function is supposed to check a string of text for all the HTML tags used in that string to determine if there are unclosed tags. We shall see if this works or not. I have to write enough text that the closing tag is past the sample point, which in this case is eighty characters. If it works, everything on the page should look fine. If it does not, everything below my widget should be in italics. We shall see. . . .

You can clearly see that the formatting of this post was just silly and useless from a normal reading perspective.  But what has been proven here is that using the force_balance_tags() function will balance any and all tags and prevent a plugin from ruining your formatting by leaving tags open.  This is a very highly useful function that I plan to implement much, much more often in the future.

Summary: Toggles all settings to a different blog’s settings, thus allowing you to perform functions as though you were on that blog. Stores the current blog’s settings in a temporary array, so they can be restored when desired.

Detail: This function switches all the settings in the $wpdb object to the new blog ($new_blog), while also updating the current user’s role as per the new blog. The end result is that all operations performed after using the switch_to_blog function will behave as you would anticipate if you were on that new blog. You would presumably use this function along with the restore_blog() function.

This is probably the single most important and strangely, least-used function in the WPMU platform. The advantages of using switch_to_blog are many. For one, it saves you the trouble of having to rewrite the database blog prefix (wp_24_posts, for example), and instead let’s you query the dB using the same pseudonyms you would use in any WordPress setting ($wpdb->posts). For another, you can use all the standard, built in WordPress functions such as get_permalink() exactly as you would in any other setting. Best of all, because it stores all the current blog’s settings in a temporary array, you can quickly restore the settings without a lot of complex coding.

Additionally, since the user’s roles are carried over, and because you’re using the standard WordPress functions, information normally not visible to a user remains invisible without complex coding. For example, if you want to display the most recent posts from a given blog, you can simply switch to that blog, use the get_posts() function to perform whatever tasks you’d like, and switch back. In doing this, you can avoid showing posts from private, spam or adult blogs. Moreover, on each blog, you can eliminate the possibility that visitors could see private or unpublished posts, or that posts set to be visible only to certain user levels. However, users who do have the proper privileges on that second blog will be able to see those additional posts. Thus you can write supple, flexible plugins that play ball with most available plugins on all public blogs. No additional coding is required, nor are lengthy database queries.

A word of warning: do not attempt to use this function to iterate over a variety of blog ID’s hoping to restore the original blog’s ID at the end, unless you switch back to the original blog in between. At first, I thought that this would be possible, but it is not. Thus, swapping back and forth between several blogs would need to be done like this pseudo-code:

switch_to_blog(4)
. . . do some stuff . . .
restore_blog()
switch_to_blog(5)
. . . do some stuff . . .
restore_blog()

Summary: High-level function that creates a new blog with the given parameters

Detail:  This function is a high-level function that combines a number of other functions to create a new blog.  It uses the install_blog(), install_blog_defaults() and add_user_to_blog() to set all the needed values in the database.  This would be the preferred function to use when creating a new blog, rather than all the lower-level functions.

Summary:  Installs the default values of a WP blog during the creation of a new blog

Details:  This function does a lot, and depending on how you want your blogs setup, may be useful to modify.  It sets up default values for such things as the blogroll, the first post, the first comment, the default category.  Of these options, only the default post can be set in Blog Options, so for finer tuning of the end-user experience, editing this file might be helpful.

Summary:  Creates a new blog with the id $blog_id and the title $blog_title

Detail:  This is a very complex function that involves calling the upgrade-functions.php file and using a bunch of it’s functions to complete its task.  This is definitely a function which – in the absence of a specific need – is better off left on its own and accessed through the API.

Summary:  Creates an entry in the wp_blogs table (or equivalent) for a new blog with the domain $domain, the path $path and within the site $site_id.

Detail:  This is one of the functions called when a new blog is created.  It’s not the whole process, and unless you have a specific reason for calling it, is probably best left to itself.  It only really performs two functions, the first being the insertion of the new blog into the database, and the second being a refresh on the wp_cache to include the new blog.