Register a scheduled cron task on the FastCron pipeline

Metadata

Field Value
Article type How-to (Developer)
Plugin slug mam-main
Category Extending MAM Suite
Audience PHP developer
Estimated time 15–30 minutes
Last verified 2026-06-03

Goal

Register a recurring task that fires on a fixed, externally-driven schedule instead of WordPress’s request-driven wp-cron.php. You add one entry to the mam_cron_manager filter and implement a callback. The mam-main SetCron Manager registers a FastCron job that pings an AJAX endpoint on every tick; the endpoint walks every registered task and runs the ones that are due.

This is the right pipeline for any scheduled work on a MAM site — draining a queue, syncing with an external service, sending a digest — because it does not depend on a visitor hitting the site to fire.


Why not wp-cron?

WordPress’s built-in wp-cron.php only runs when a request reaches the site. On a low-traffic site, scheduled events drift or never fire; queue-draining work (push notifications, SMS retries) stalls. mam-main delegates scheduling to FastCron — a third-party service that pings a known AJAX endpoint on a fixed schedule, decoupling cron from site traffic. Your task rides that pipeline by registering with mam-main rather than calling wp_schedule_event() yourself.

The implementation lives in mam-main/includes/setcron-manager/.


Prerequisites

  • A sibling plugin (or a site-specific mu-plugin) where your registration and callback can live. Do not edit mam-main core.
  • mam-main active. The SetCron Manager is part of mam-main core.
  • FastCron configured on the customer site. This is an admin task: a real FastCron API token must be stored in the SetCron Manager admin page. If FastCron is not configured, no registered task ever fires. See the FastCron integration reference.
  • An understanding that your callback is invoked via apply_filters( $action, array() ) — so register it with add_filter, not add_action.

Steps

1. Choose an action handle

Each task has an action string. It is used two ways: as the dispatch hook the processor fires, and as the WordPress option key where the task’s run state (last_run, next_run, status) is stored. It must be unique site-wide.

Convention: mam_<plugin_slug>_<task_name> (snake_case), for example mam_my_plugin_nightly_sync. mam-main’s own mail/push task uses the handle mam_mail_handler, so follow the same flat, descriptive style.

2. Register the task on mam_cron_manager

The registry is a plain indexed array. Append your entry — do not key it yourself.

add_filter( 'mam_cron_manager', function ( $crons ) {

    $crons[] = array(
        'action'    => 'mam_my_plugin_nightly_sync', // dispatch hook + option key (unique)
        'name'      => 'My Plugin: Nightly Sync',     // label shown in the admin grid
        'run_at'    => '0200',                          // run once a day at 02:00 (HHMM, site timezone)
    );

    return $crons;
} );

You pick one scheduling style per entry:

  • 'frequency' => 15 — run roughly every 15 minutes (integer minutes).
  • 'run_at' => '0200' — run once a day at the given HHMM time, in the site timezone.

If you supply run_at, it wins; frequency is then used only to compute the next daily slot. If you supply neither, the task defaults to a 60-minute interval.

Note: the field names above (action, name, frequency, run_at) are the only ones the live processor and the admin grid read. There is no title, expression, or callback field, and no full cron-expression syntax — the schedule is expressed entirely through frequency or run_at.

3. Implement the callback

Register your callback against the action handle using add_filter, and return an array. The return value is not discarded — the processor takes your return array, stamps last_run and next_run onto it, and writes the whole array to the task’s option (any keys you return that aren’t overwritten are persisted).

add_filter( 'mam_my_plugin_nightly_sync', 'mam_my_plugin_nightly_sync_run' );

function mam_my_plugin_nightly_sync_run( $unused = array() ) {

    $count = mam_my_plugin_sync_recent_changes();

    // Returned keys are persisted to the task's option and shown in the admin grid.
    return array(
        'status'  => 'success',
        'message' => sprintf( 'Synced %d records', $count ),
    );
}

A returned status and message surface in the Status column of the Scheduled Jobs grid, which makes this the cheapest way to expose “did my last run succeed” to an admin. Avoid returning last_run or next_run yourself — the processor overwrites those.

The callback receives an empty array as its only argument; it is a fire-and-forget invocation, not a value transformer. Treat the parameter as unused.

4. Make the callback idempotent

The pipeline has no retry semantics and no run-once guarantee within a window. A task is “due” whenever the current time has passed its stored next_run; the processor then runs it and computes the next slot. Two ticks landing close together, a manually triggered endpoint, or a re-activated plugin can all run your callback again. Design the work so a repeat run is harmless.

Pattern — a self-defended time guard:

function mam_my_plugin_nightly_sync_run( $unused = array() ) {

    $last = (int) get_option( 'mam_my_plugin_nightly_guard', 0 );

    if ( time() - $last < 12 * HOUR_IN_SECONDS ) {
        // Already ran inside the window — skip.
        return array( 'status' => 'skipped', 'message' => 'ran recently' );
    }

    update_option( 'mam_my_plugin_nightly_guard', time(), false );

    $count = mam_my_plugin_sync_recent_changes();

    return array( 'status' => 'success', 'message' => "synced {$count}" );
}

Other idempotency tactics: upsert instead of insert, key external writes on a stable id, and process items in claim-then-mark batches so a re-run re-claims only what is still unprocessed.

5. Keep the callback short — queue heavy work

Tasks that match the same tick run serially. A callback that runs for minutes blocks every later task on that tick. For heavy work, queue and return immediately rather than processing inline:

function mam_my_plugin_nightly_sync_run( $unused = array() ) {

    foreach ( mam_my_plugin_get_pending_ids() as $id ) {
        wp_schedule_single_event( time() + 60, 'mam_my_plugin_process_item', array( $id ) );
    }

    return array( 'status' => 'success', 'message' => 'queued' );
}

This is exactly how mam-main’s own mam_mail_handler task behaves: each tick it drains a bounded batch from the mail and push/SMS queues, then returns — it never tries to flush everything at once.

6. Verify in the admin grid

Open Mobile App Mgr → Scheduled Jobs (the submenu appears only once at least one task is registered). Your task appears by its name, with its Hook Name (action), a human-readable Frequency (“Every 15 minutes” or “Every day at 0200”), and — after the first run — Status, Last Run, and Next Run. The grid is rendered by Cron_List in includes/setcron-manager/cron-list.php.

7. Force a tick to test

You do not have to wait for FastCron. The processor is a public AJAX action, so you can simulate a tick directly:

curl 'https://example.com/wp-admin/admin-ajax.php?action=mam_setcron_processor'

The endpoint answers to both logged-in and unauthenticated callers (FastCron pings unauthenticated). It resolves the site timezone, walks every registered task, runs the due ones, and returns a JSON status. Watch your callback’s side effects, then confirm Last Run advanced in the grid.


Scheduling cheatsheet

You want… Field Value
Every N minutes frequency integer, e.g. 15
Every minute frequency 1 (bounded by FastCron’s tick rate)
Once a day at a time run_at HHMM, e.g. '0200' for 02:00
Default (no field set) falls back to 60-minute interval

Resolution is bounded by FastCron’s own tick frequency. If FastCron pings every 5 minutes, a frequency of 1 still effectively runs every 5 minutes. The site timezone is resolved with wp_timezone_string(); offset-style zones like +05:30 are best avoided in favor of named timezones (e.g. Asia/Kolkata), which resolve cleanly.


Gotchas

  • Append, don’t key. The registry is an indexed array ($crons[] = ...). Setting a string key works by accident but breaks the house pattern.
  • apply_filters, not do_action. Register the callback with add_filter against the action handle. An add_action callback never fires.
  • action must be unique site-wide. It doubles as the option key for run state; a collision corrupts another task’s schedule.
  • Return an array. The return value is persisted, not discarded. Returning a non-array (null, void, or a scalar) breaks when the processor stamps last_run onto it.
  • No retries. A failed run waits for the next due tick. Build retry into your own logic if it matters.
  • Long callbacks block the tick. Same-tick tasks run serially — queue heavy work.
  • FastCron must be configured. With no real API token stored, the pipeline is effectively off and nothing fires. The API key is stored in the option tsl-setting-setcron-api; a stored value of exactly 1 is treated as “disabled,” so store your real FastCron API key.

  • Integration: FastCron — why the pipeline exists and how mam_setcron_processor is pinged.
  • Hook: mam_cron_manager — the registry filter contract.
  • Notification queue and cron — the mam_mail_handler task as a worked example of the queue-and-drain pattern.

Verification

This article was drafted against:

  • mam-main/includes/setcron-manager/mam-setcron-manager.phpmam_setcron_processor() dispatch loop (entry fields action / frequency / run_at; apply_filters( $action, array() ); return value merged into the option).
  • mam-main/includes/setcron-manager/cron-list.phpCron_List admin grid columns and frequency labels.
  • mam-main/includes/mail-cron-manager/mam-mail-manager.php — a live registration (mam_mail_handler) and queue-drain callback.
Was this article helpful?
Contents

    Need Support?

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