| name | add-scheduled-event |
| description | Schedule a recurring or single WordPress cron event in a plugin, with proper activation/deactivation hooks. |
Add a Scheduled Event (WP-Cron)
Register a recurring or one-off background task using WP-Cron. Covers scheduling on activation, clearing on deactivation, custom intervals, and the callback implementation.
Inputs
| 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) |
| What it does | Yes | Description of the work the callback should perform |
If any required inputs are missing, ask for them before writing any code.
Step 1 — Define the hook name constant
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';
Step 2 — Schedule on activation, clear on deactivation
Register activation and deactivation hooks in the plugin bootstrap file (not inside a hooked callback):
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.
Step 3 — Register the callback in hooks()
public function hooks(): void {
add_action( self::CRON_HOOK, [ $this, 'run_sync' ] );
}
Step 4 — Implement the callback
public function run_sync(): void {
$response = \wp_remote_get( 'https://api.example.com/feed' );
if ( \is_wp_error( $response ) ) {
return;
}
$body = \wp_remote_retrieve_body( $response );
$data = \json_decode( $body, true );
if ( ! \is_array( $data ) ) {
return;
}
}
Step 5 — Register a custom interval (if needed)
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 );
Step 6 — Single (one-off) event variant
For a task that should fire once rather than recur, use wp_schedule_single_event():
\wp_schedule_single_event( \time() + 5 * MINUTE_IN_SECONDS, self::CRON_HOOK );
No deactivation cleanup is needed for single events that have already fired.
Step 7 — Lint
composer lint
composer lint-fix
composer lint
Conventions
- Hook name prefix: always
{plugin-slug}/hook_name — prevents collisions with Core and other plugins
- No
error_log(): never in committed code — use a custom logging mechanism (option, transient, or the T2 write_log() helper if available)
- Guard on activation: always check
wp_next_scheduled() before scheduling — plugin activation can run more than once
- Clear on deactivation: always unschedule in
deactivate() — orphaned cron hooks that point to missing callbacks generate PHP errors on every cron run
- Custom intervals: prefix the interval key with the plugin slug to avoid collisions with other plugins
- WP_CRON on serverless/managed hosting: note to the user that WP-Cron is request-triggered, not a real system cron. For reliable scheduling, recommend a real cron job calling
wp cron event run --due-now via WP-CLI
Output
Activation 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.