arraypress / wp-register-cron
Declarative WordPress cron jobs and custom schedules, cleaned up properly on uninstall.
Requires
- php: >=8.3
Requires (Dev)
- phpcompatibility/phpcompatibility-wp: ^2.1
- phpunit/phpunit: ^12.0
- squizlabs/php_codesniffer: ^3.13.5
- wp-coding-standards/wpcs: ^3.4
This package is auto-updated.
Last update: 2026-08-25 15:47:23 UTC
README
Declarative WordPress cron jobs and custom schedules, cleaned up properly on uninstall.
Install
composer require arraypress/wp-register-cron
Requires PHP 8.3.
Use
add_action( 'init', function () { register_cron_job( 'myplugin_sync', [ 'schedule' => 'daily', 'callback' => 'myplugin_run_sync', ] ); } );
On init, on every request — not on activation. That is the part everyone
gets wrong, and it is worth being clear about why.
A cron event fires on some later request than the one that scheduled it, and
WordPress fires it by doing do_action( 'myplugin_sync' ). If the callback
was attached only at activation, nothing is listening when that happens: the
event runs, the hook fires into an empty room, WordPress marks it done and
schedules the next one. It repeats for ever, does nothing, and reports
nothing.
So register_cron_job() attaches the callback every time it is called, and
queues the event only if it is not queued already. Calling it on every request
is correct and cheap.
Options
| Option | Type | What it does |
|---|---|---|
callback |
callable | What to run. Required. |
schedule |
string | A recurrence. Omit for a job that runs once. |
args |
array | Passed to the callback, and part of what identifies the event. |
start |
int | When it first runs. Defaults to now. |
Schedules
WordPress ships hourly, twicedaily, daily and weekly. Anything else:
register_cron_schedule( 'every_five_minutes', 5 * MINUTE_IN_SECONDS, __( 'Every five minutes', 'my-plugin' ) );
A recurrence that nothing has registered is refused rather than scheduled. WordPress accepts an unknown one happily — the event runs once, and then never again, because rescheduling it needs an interval that does not exist. There is nothing anywhere to say so, which is why this refuses at the point the mistake is made.
An existing recurrence is never replaced. A plugin quietly redefining what
daily means for every other plugin on the site is not something to do by
accident.
Uninstall
unregister_cron_jobs(); // everything this plugin registered unregister_cron_job( 'myplugin_sync' ); // one of them
This is the reason to have the library. An orphaned cron event outlives the plugin that created it: it stays in the queue, fires into code that is no longer installed, and keeps costing the site something on every visit. It is the most common way an uninstalled plugin is still there.
Clearing removes every occurrence of the hook, not the next one. A hook that ended up in the queue twice — which is exactly the mess uninstall is there to tidy — was otherwise left half present.
Arguments identify an event, so a hook scheduled for two different feeds is two events and clearing one leaves the other:
unregister_cron_job( 'myplugin_sync', [ 'feed-a' ] );
Asking
$next = cron_job_next_run( 'myplugin_sync' ); // timestamp, or null
Null rather than false, because wp_next_scheduled() returns false for "not
queued" and a timestamp otherwise, and false is also what a careless read of a
zero timestamp looks like.
Testing
composer test # phpunit composer lint # phpcs, defect sniffs composer format:check # phpcs, formatting