Schedule a recurring or single WordPress cron event in a plugin, with proper activation/deactivation hooks.
Register a recurring or one-off background task using WP-Cron. Covers scheduling on activation, clearing on deactivation, custom intervals, and the callback implementation.
| Input | Required | Notes |
|---|---|---|
| Plugin | Yes | Which plugin in packages/plugins/ |
| Event name (hook) | Yes | Machine name for the cron hook, e.g. {plugin-slug}/sync_feed. Prefixed with the plugin slug. |
| Frequency | Yes | hourly, twicedaily, daily, weekly — or a custom interval (specify in seconds) |
| Recurrence | Yes | Recurring (repeats indefinitely) or single (fires once) |
| Yes |
| Description of the work the callback should perform |
If any required inputs are missing, ask for them before writing any code.
Add a constant to the plugin's main class to avoid hardcoding the hook string in multiple places:
private const CRON_HOOK = '{plugin-slug}/sync_feed';
Register activation and deactivation hooks in the plugin bootstrap file (not inside a hooked callback):
// In plugin.php
register_activation_hook( __FILE__, [ Plugin::class, 'activate' ] );
register_deactivation_hook( __FILE__, [ Plugin::class, 'deactivate' ] );
Implement the static methods on the main plugin class:
public static function activate(): void {
if ( ! \wp_next_scheduled( self::CRON_HOOK ) ) {
\wp_schedule_event( \time(), 'daily', self::CRON_HOOK );
}
}
public static function deactivate(): void {
$timestamp = \wp_next_scheduled( self::CRON_HOOK );
if ( $timestamp ) {
\wp_unschedule_event( $timestamp, self::CRON_HOOK );
}
}
The wp_next_scheduled() guard in activate() prevents duplicate schedules if activation runs more than once.
hooks()public function hooks(): void {
add_action( self::CRON_HOOK, [ $this, 'run_sync' ] );
}
/**
* Runs the scheduled feed synchronisation.
*/
public function run_sync(): void {
// Fetch data from an external source
$response = \wp_remote_get( 'https://api.example.com/feed' );
if ( \is_wp_error( $response ) ) {
// Log to a custom option or post meta — never error_log() in committed code
return;
}
$body = \wp_remote_retrieve_body( $response );
$data = \json_decode( $body, true );
if ( ! \is_array( $data ) ) {
return;
}
// ... process $data ...
}
If the built-in intervals (hourly, twicedaily, daily) do not fit, register a custom one in hooks():
public function hooks(): void {
add_filter( 'cron_schedules', [ $this, 'add_cron_intervals' ] );
add_action( self::CRON_HOOK, [ $this, 'run_sync' ] );
}
public function add_cron_intervals( array $schedules ): array {
$schedules['{plugin-slug}_every_six_hours'] = [
'interval' => 6 * HOUR_IN_SECONDS,
'display' => __( 'Every 6 hours', '{plugin-slug}' ),
];
return $schedules;
}
Then pass the custom key as the recurrence argument in activate():
\wp_schedule_event( \time(), '{plugin-slug}_every_six_hours', self::CRON_HOOK );
For a task that should fire once rather than recur, use wp_schedule_single_event():
// Schedule to fire in 5 minutes
\wp_schedule_single_event( \time() + 5 * MINUTE_IN_SECONDS, self::CRON_HOOK );
No deactivation cleanup is needed for single events that have already fired.
composer lint
composer lint-fix # if needed
composer lint # re-run to confirm clean
{plugin-slug}/hook_name — prevents collisions with Core and other pluginserror_log(): never in committed code — use a custom logging mechanism (option, transient, or the T2 write_log() helper if available)wp_next_scheduled() before scheduling — plugin activation can run more than oncedeactivate() — orphaned cron hooks that point to missing callbacks generate PHP errors on every cron runwp cron event run --due-now via WP-CLIActivation and deactivation hooks in plugin.php, a CRON_HOOK constant on the main class, a hooks() registration, and a callback method. The event fires on the chosen schedule and is cleaned up when the plugin is deactivated. Lint passes clean.