Recipe: Register a scheduled cron task

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 (with last_run/next_run merged 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 computes next_run = now + frequency * 60.
  • run_at — optional HHMM (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 once frequency minutes 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 (with last_run/next_run merged in) as the job’s state option. Register with add_filter so your callback returns the array.
  • action must be unique. It doubles as the get_option key for the job’s run state — two entries sharing an action collide 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.

  • 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
Was this article helpful?
Contents

    Need Support?

    Can't find the answer you're looking for? Don't worry we're here to help!