WordPress has no built-in way to schedule tasks at specific times – everything goes through WP-Cron, which runs based on page visits rather than a real clock. WP Crontrol exposes the full WP-Cron schedule so you can see, add, edit, and manually trigger events. This guide covers writing custom scheduled tasks correctly.
The Right Pattern for a Custom Cron Task
Three things must happen in the right order: register the event schedule, schedule the event (once, on activation), and hook the callback to the event. Missing any one of these is the most common cause of cron tasks that never run.
// 1. Register a custom interval
add_filter('cron_schedules', function($schedules) {
$schedules['twice_daily'] = array(
'interval' => 43200, // 12 hours in seconds
'display' => 'Twice Daily',
);
return $schedules;
});
// 2. Schedule on activation - only if not already scheduled
register_activation_hook(__FILE__, function() {
if (!wp_next_scheduled('sync_external_data')) {
wp_schedule_event(time(), 'twice_daily', 'sync_external_data');
}
});
// 3. Clean up on deactivation
register_deactivation_hook(__FILE__, function() {
wp_clear_scheduled_hook('sync_external_data');
});
// 4. The actual callback
add_action('sync_external_data', function() {
$response = wp_remote_get('https://api.example.com/data', array(
'timeout' => 30,
));
if (is_wp_error($response)) {
error_log('sync_external_data failed: ' . $response->get_error_message());
return;
}
$data = json_decode(wp_remote_retrieve_body($response), true);
if (!empty($data)) {
update_option('cached_external_data', $data);
update_option('cached_external_data_time', time());
}
});
Preventing Duplicate Scheduled Events
A common mistake: calling wp_schedule_event() without first checking wp_next_scheduled(). If this runs on every page load (e.g., placed in functions.php without an activation hook), it schedules the same event repeatedly. WP Crontrol would show dozens of entries for the same hook.
Always wrap the schedule call:
if (!wp_next_scheduled('my_hook')) {
wp_schedule_event(time(), 'daily', 'my_hook');
}
Need a developer to implement custom cron? Describe what you need and get a free estimate.
Running a Task Once at a Specific Time
wp_schedule_single_event() schedules a one-time event. Use this for tasks that should run once after a delay rather than on a recurring schedule:
// Send a follow-up email 48 hours after order completion
function schedule_followup_email($order_id) {
wp_schedule_single_event(
time() + (48 * HOUR_IN_SECONDS),
'send_order_followup',
array($order_id) // passed as arguments to the callback
);
}
add_action('woocommerce_order_status_completed', 'schedule_followup_email');
add_action('send_order_followup', function($order_id) {
$order = wc_get_order($order_id);
if (!$order) return;
// Send email logic here
});
Passing Arguments to Cron Callbacks
wp_schedule_event() does not support passing arguments directly. For recurring tasks that need context (like processing a specific site in a multisite), store the context in an option or use separate hooks per context:
// Store processing queue in options
add_action('process_queue_item', function() {
$queue = get_option('my_processing_queue', array());
if (empty($queue)) return;
$item = array_shift($queue);
update_option('my_processing_queue', $queue);
// Process $item
do_something_with($item);
});
Testing Cron Callbacks Without Waiting
WP Crontrol’s “Run Now” link triggers any scheduled event immediately. For events not yet scheduled, use WP-CLI to run the hook directly:
wp cron event run my_custom_hook
Or schedule a one-time event for now and let WP-Cron pick it up on the next page load:
wp cron event schedule my_custom_hook now