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-mainactive. 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 withadd_filter, notadd_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 givenHHMMtime, 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 notitle,expression, orcallbackfield, and no full cron-expression syntax — the schedule is expressed entirely throughfrequencyorrun_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
frequencyof1still effectively runs every 5 minutes. The site timezone is resolved withwp_timezone_string(); offset-style zones like+05:30are 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, notdo_action. Register the callback withadd_filteragainst theactionhandle. Anadd_actioncallback never fires.actionmust 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 stampslast_runonto 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 exactly1is treated as “disabled,” so store your real FastCron API key.
Related
- Integration: FastCron — why the pipeline exists and how
mam_setcron_processoris pinged. - Hook:
mam_cron_manager— the registry filter contract. - Notification queue and cron — the
mam_mail_handlertask as a worked example of the queue-and-drain pattern.
Verification
This article was drafted against:
mam-main/includes/setcron-manager/mam-setcron-manager.php—mam_setcron_processor()dispatch loop (entry fieldsaction/frequency/run_at;apply_filters( $action, array() ); return value merged into the option).mam-main/includes/setcron-manager/cron-list.php—Cron_Listadmin 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.
