WP-Cron’s wp_schedule_event() creates recurring tasks — jobs that repeat on a fixed interval forever. But many real workflows need a task that runs exactly once at a specific future time: sending a follow-up email 48 hours after a user registers, rebuilding a cache entry after a manual edit, or triggering a webhook when a post’s scheduled publish time arrives. wp_schedule_single_event() is the correct function for this — it schedules a callback to run at a UTC timestamp without repeating. Once the event fires it is automatically removed from the queue. Combining single events with post meta (to track pending scheduled actions per post) gives you a flexible, lightweight alternative to third-party action-scheduler libraries for simple deferred background work.
Problem: When a user registers, the plugin should send a "getting started" email 24 hours later (not immediately). The delay should survive server restarts, and if the user completes onboarding before the 24 hours, the pending email should be cancelled.
Solution: Schedule a single event with wp_schedule_single_event() in the user_register hook, pass the user ID as an argument, and cancel it with wp_clear_scheduled_hook() if the user completes onboarding first.
<?php
// ── Schedule on user registration ────────────────────────────────────
add_action( 'user_register', function ( int $user_id ) {
$send_at = time() + DAY_IN_SECONDS; // 24 hours from now (UTC timestamp)
wp_schedule_single_event(
$send_at,
'my_plugin_send_welcome_followup', // hook name
[ $user_id ] // arguments passed to callback
);
} );
// ── The callback that runs when the event fires ───────────────────────
add_action( 'my_plugin_send_welcome_followup', function ( int $user_id ) {
$user = get_userdata( $user_id );
if ( ! $user ) return; // user was deleted before the event fired
// Check if onboarding was already completed
if ( get_user_meta( $user_id, 'onboarding_complete', true ) ) return;
wp_mail(
$user->user_email,
__( 'Getting started with our platform', 'textdomain' ),
sprintf( __( 'Hi %s, here are some tips...', 'textdomain' ), $user->display_name )
);
} );
// ── Cancel the event if onboarding completes early ────────────────────
function maybe_cancel_followup_email( int $user_id ): void {
// wp_clear_scheduled_hook removes ALL instances of this hook+args combo
wp_clear_scheduled_hook( 'my_plugin_send_welcome_followup', [ $user_id ] );
}
// ── Check when the event is scheduled ─────────────────────────────────
$next_run = wp_next_scheduled( 'my_plugin_send_welcome_followup', [ $user_id ] );
if ( $next_run ) {
echo 'Followup email scheduled for: ' . wp_date( 'Y-m-d H:i', $next_run );
}
// ── WP time constants (defined by WordPress) ──────────────────────────
// MINUTE_IN_SECONDS = 60
// HOUR_IN_SECONDS = 3600
// DAY_IN_SECONDS = 86400
// WEEK_IN_SECONDS = 604800
// MONTH_IN_SECONDS = 2592000 (30 days)
// YEAR_IN_SECONDS = 31536000
// ── Prevent duplicate scheduling ──────────────────────────────────────
// Only schedule if not already pending (in case user_register fires twice)
add_action( 'user_register', function ( int $user_id ) {
if ( ! wp_next_scheduled( 'my_plugin_send_welcome_followup', [ $user_id ] ) ) {
wp_schedule_single_event(
time() + DAY_IN_SECONDS,
'my_plugin_send_welcome_followup',
[ $user_id ]
);
}
} );
NOTE: WP-Cron runs on page load — if no one visits the site for 25 hours, a 24-hour event will not fire until the next visit after its scheduled time. For production sites with guaranteed event timing, use a real server cron job to ping wp-cron.php every minute: * * * * * curl -s https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1. And set define( 'DISABLE_WP_CRON', true ) in wp-config.php so WordPress does not also try to trigger it on every page load. The arguments array passed to wp_schedule_single_event() is used as the hook's unique identifier — changing the arguments without cancelling the original event will leave the original event in the queue.