Goal
Register a scheduled task that fires on a fixed external schedule (via FastCron) rather than relying on WordPress’s request-driven wp-cron.php.
Prerequisites
- A sibling plugin with its own slug
- FastCron configured on the customer site (admin task — see Integration: FastCron)
- An understanding that callbacks are invoked via
apply_filters($action, [])— the returned array is persisted (withlast_run/next_runmerged in) as the job’s state option, so always return an array
Steps
1. Choose a cron id
Convention: mam_<plugin_slug>_<task_name> (snake_case). This becomes the entry’s action value — it is both the dispatch handle (apply_filters($action, [])) and the get_option key where the job’s run state is stored. Must be unique site-wide.
Example: mam_my_plugin_nightly_sync.
2. Register the cron entry
Append your entry to the $crons array — the filter reads the action, name, frequency and optional run_at keys from each element. It does not read the array key, a callback, or a cron expression.
add_filter( 'mam_cron_manager', function ( array $crons ): array {
$crons[] = array(
'action' => 'mam_my_plugin_nightly_sync', // dispatch handle + option key
'name' => 'My Plugin: Nightly Sync', // label shown in the admin list
'frequency' => 1440, // minutes between runs (1440 = daily)
'run_at' => '0200', // optional HHMM (24h, site tz)
);
return $crons;
} );
3. Implement the callback
function mam_my_plugin_nightly_sync_action( $unused = array() ) {
// Do the work.
$this->sync_recent_changes();
// Return an array — it is persisted (with last_run/next_run added) as the job's state.
return array();
}
add_filter( 'mam_my_plugin_nightly_sync', 'mam_my_plugin_nightly_sync_action' );
⚠️ The callback is invoked via apply_filters($action, []). Register with add_filter and return an array — the dispatcher does $results = apply_filters($action, []) then stores $results (with last_run/next_run merged in) under get_option($action). Returning a non-array breaks the run-state write.
4. Verify in the admin
Mobile App Manager → Scheduled Jobs lists every registered cron. Your entry appears with its name, last-run and next-run times. The list is rendered by the Cron_List table class (includes/setcron-manager/cron-list.php).
5. Confirm FastCron is configured
If tsl-setting-setcron-api is '1' or empty, FastCron is disabled and your cron never fires. Admin must enter a real FastCron API token under Mobile App Manager → Scheduled Jobs → Cronjob Settings (option tsl-setting-setcron-api).
6. Test
The fastest test: trigger the AJAX endpoint directly.
curl 'https://example.com/wp-admin/admin-ajax.php?action=mam_setcron_processor'
This simulates a FastCron tick. Watch your callback’s logs / side effects.
Scheduling: frequency and run_at
Jobs are scheduled by interval, not by cron expression. Two keys control timing:
frequency— integer minutes between runs. The dispatcher computesnext_run = now + frequency * 60.run_at— optionalHHMM(24-hour, site timezone) anchor for the daily run. Padded to 4 digits, so pass the full time ('0200', not'2'). Omit for “first eligible tick oncefrequencyminutes have elapsed.”
frequency => 1440, run_at => '0200' → ~2am daily
frequency => 60 → hourly
frequency => 15 → every 15 minutes
⚠️ Resolution is bounded by FastCron’s tick frequency. A job with frequency => 1 fires at most once per FastCron ping — if FastCron pings every 5 minutes, it runs every 5 minutes, not every minute.
Patterns
Heavy work → queue + drain pattern
function mam_my_plugin_nightly_sync_action( $unused = array() ) {
// Don't do all the work inline; queue and return.
foreach ( $this->get_pending_items() as $item_id ) {
wp_schedule_single_event( time() + 60, 'my_plugin_process_item', array( $item_id ) );
}
}
Idempotent reads / writes
function mam_my_plugin_nightly_sync_action( $unused = array() ) {
$last_run = (int) get_option( 'mam_my_plugin_nightly_last_run', 0 );
if ( time() - $last_run < 12 * HOUR_IN_SECONDS ) {
// Already ran in the last 12 hours — skip (defends against multiple ticks within a window)
return;
}
update_option( 'mam_my_plugin_nightly_last_run', time() );
$this->do_work();
}
Gotchas
- Return an array from your callback. It’s dispatched via
apply_filters($action, [])and the return value is persisted (withlast_run/next_runmerged in) as the job’s state option. Register withadd_filterso your callback returns the array. actionmust be unique. It doubles as theget_optionkey for the job’s run state — two entries sharing anactioncollide on that option.- No retry semantics. A failed callback isn’t retried until the next matching tick.
- Long callbacks block the tick. If your callback runs for 5 minutes, no other crons matching the same tick fire until you return.
- FastCron API-token sentinel value
'1'means “feature disabled” — a real token is non-numeric. - Time-zone resolution for offset zones (
+05:30) can fall through to UTC. Use named timezones.
Related articles
- Integration: FastCron
- Notification queue and cron
- Hook: mam_cron_manager
Metadata
| Field | Value |
|---|---|
| Article type | Recipe (Developer) |
| Plugin slug | mam-main |
| Applies to plugin version | 2.1.11+ |
| Category | Extending MAM Suite |
| Audience | PHP developer |
| Estimated time | 15–30 minutes |
| Last verified | 2026-05-02 |
