Developer documentation for the scheduled task system.
The scheduled tasks system provides a general-purpose framework for running tasks on a schedule. Tasks are PHP classes paired with JSON config files. A single cron entry runs every 15 minutes and executes any tasks that are due.
Cron (every 15 min)
→ utils/process_scheduled_tasks.php
→ Load active ScheduledTask records from DB
→ For each task where is_due():
1. Resolve and instantiate the task class
2. Call run($config) with the task's sct_task_config
3. Update last_run_time and last_run_statusEach task consists of two files sharing the same base name:
tasks/
WeeklyEventsDigest.php ← PHP class implementing ScheduledTaskInterface
WeeklyEventsDigest.json ← Metadata and default configuration
plugins/bookings/tasks/
BookingReminder.php
BookingReminder.jsonPlace in /tasks/ (core) or /plugins/{plugin}/tasks/ (plugin).
{
"name": "My Task Name",
"description": "What this task does",
"default_frequency": "daily",
"default_day_of_week": 1,
"default_time": "09:00:00",
"config_fields": {
"some_setting": {"type": "text", "label": "Some Setting", "required": true}
}
}Fields:
name — Display name in admindescription — Explains what the task doesdefault_frequency — Default frequency: every_run, hourly, daily, weekly (defaults to daily)default_day_of_week — Default schedule day (0=Sunday–6=Saturday, only used for weekly)default_time — Default time of day (HH:MM:SS, only used for daily and weekly)config_fields — Task-specific parameters rendered in admin formtext — Text inputnumber — Numeric inputboolean — Checkboxmailing_list — Mailing list dropdown (populated from database)<?php
require_once(PathHelper::getIncludePath('includes/ScheduledTaskInterface.php'));
class MyTaskName implements ScheduledTaskInterface {
public function run(array $config) {
// $config contains values from sct_task_config (set via admin form)
// Do work here...
// Return an array with status and human-readable message
// Status meanings:
// 'success' — Ran and completed (with or without work to do)
// 'skipped' — Could not run (misconfigured, missing prerequisite)
// 'error' — Attempted to run but failed
return array('status' => 'success', 'message' => 'Processed 5 items');
}
}A task can ask the runner to flip its sct_is_active to false after the
current run by adding 'deactivate' => true to the result array:
return array(
'status' => 'success',
'message' => 'No more work to do.',
'deactivate' => true,
);This is the right pattern for one-shot drain tasks (e.g.
CloudStorageReverseSync deactivates itself once every cloud-stored
file has been pulled back). The runner reads the flag, sets
sct_is_active = false on the task row, and saves — so the row is not
re-evaluated on subsequent ticks until something explicitly reactivates
it.
Setting sct_is_active = false from inside the task with a separate
save() does not work: the runner holds an in-memory snapshot of
the row from before the call to run(), and its post-run save would
overwrite the deactivation. Use the deactivate flag.
Tasks can implement the ScheduledTaskDryRunnable interface to support preview/dry run from the admin UI. This is especially useful for email tasks where you want to see what would be sent without actually sending.
class MyTaskName implements ScheduledTaskInterface, ScheduledTaskDryRunnable {
public function run(array $config) {
// ... normal execution with side effects
}
public function dryRun(array $config) {
// Perform all read/computation logic but skip side effects
// (no sending emails, no deleting records, no API calls)
return array(
'status' => 'success',
'message' => 'Would process 5 items',
'html' => $preview_html, // Optional: rendered in admin UI
);
}
}Return keys:
status (string, required) — Same as run(): success, skipped, errormessage (string, required) — Summary of what would happen (e.g., "Would send 5 events to 42 recipients")html (string, optional) — HTML preview displayed inline on the admin page (e.g., the email body)Navigate to Admin > System > Scheduled Tasks. The task appears under "Available Tasks". Click Activate to create the database row and enable scheduling.
Table: sct_scheduled_tasks
| Column | Type | Description |
|---|---|---|
sct_scheduled_task_id | int8 (serial) | Primary key |
sct_name | varchar(255) | Display name |
sct_task_class | varchar(255) | PHP class name |
sct_is_active | bool | Whether task runs on schedule |
sct_frequency | varchar(20) | every_run, hourly, daily, weekly |
sct_schedule_day_of_week | int4 | 0=Sun–6=Sat (weekly only) |
sct_schedule_time | time | Time of day in site timezone (daily/weekly only) |
sct_task_config | jsonb | Task-specific configuration |
sct_last_run_time | timestamp | When task last ran |
sct_last_run_status | varchar(50) | success/error/skipped |
sct_last_run_message | varchar(500) | Human-readable result detail |
sct_create_time | timestamp | Row creation time |
sct_delete_time | timestamp | Soft delete time |
ScheduledTask (single), MultiScheduledTask (collection)MultiScheduledTask filter options: active (bool), deleted (bool), task_class (string)
ScheduledTask::is_due()Behavior depends on sct_frequency:
every_run — Always due (runs every cron invocation, ~15 min)hourly — Due if not already run in the current clock hourdaily — Due if past sct_schedule_time today (site timezone) and not already run todayweekly — Due if correct sct_schedule_day_of_week, past sct_schedule_time, and not already run todaydefault_timezone setting).ScheduledTask::resolve_task_file()Searches for the PHP class file:
/tasks/{class_name}.php/plugins/*/tasks/{class_name}.phpScheduledTask::get_task_config()Returns sct_task_config as an associative array.
File: utils/process_scheduled_tasks.php
scheduled_tasks_last_cron_run setting (heartbeat)Each task's run() is wrapped in pg_try_advisory_lock(hashtext(sct_name)),
so a long-running task cannot be re-entered by the next cron tick. If the
lock cannot be acquired the task is skipped with skipped: already running
and the runner moves on to the next task. The lock auto-releases when the
PHP connection closes, so a crashed process self-recovers on the next tick.
This is transparent to task implementations — no run() code needs to
know about the lock — but it means tasks that legitimately want to run
in parallel across ticks would be serialized. In practice the cron tick
interval (15 min) is long compared to almost every task's runtime, so
the serialization is rarely visible.
Standard server — Add to the www-data user's crontab (sudo crontab -e -u www-data):
*/15 * * * * php /var/www/html/{sitename}/public_html/utils/process_scheduled_tasks.php >> /var/www/html/{sitename}/logs/cron_scheduled_tasks.log 2>&1Docker container — Ensure cron is installed and running, then create /etc/cron.d/scheduled-tasks:
*/15 * * * * www-data php /var/www/html/{sitename}/public_html/utils/process_scheduled_tasks.php >> /var/www/html/{sitename}/logs/cron_scheduled_tasks.log 2>&1Note: Docker containers may not have cron installed by default. Install with apt-get install -y cron and start the daemon with cron. The cron daemon must also be started after container restart (add to entrypoint if needed).
New installs get the crontab entry automatically via _site_init.sh. Existing sites see a warning on the admin page with setup instructions if cron hasn't run in 30+ minutes.
File: adm/admin_scheduled_tasks.php
Logic: adm/logic/admin_scheduled_tasks_logic.php
Menu: System > Scheduled Tasks (permission level 10)
Sections:
Tasks in /plugins/{plugin}/tasks/ are discovered automatically alongside core tasks. Each needs both a .json and .php file.
Each plugin task record stores the owning plugin name in sct_plugin_name. This field is populated automatically when a task is activated via the admin UI for a task discovered in a plugin's /tasks/ directory.
Plugin-owned tasks follow the plugin lifecycle:
sct_plugin_name are suspended (sct_is_active = false). They will not run until the plugin is reactivated.sct_is_active = true).sct_plugin_name are permanently deleted (not just suspended).| File | Purpose |
|---|---|
data/scheduled_tasks_class.php | Data model classes |
includes/ScheduledTaskInterface.php | Task interface |
utils/process_scheduled_tasks.php | Cron runner |
adm/admin_scheduled_tasks.php | Admin page view |
adm/logic/admin_scheduled_tasks_logic.php | Admin page logic |
tasks/WeeklyEventsDigest.php | Example email digest task |
tasks/WeeklyEventsDigest.json | Example email digest config |
tasks/PurgeOldErrors.php | Example cleanup task |
tasks/PurgeOldErrors.json | Example cleanup config |
migrations/migration_scheduled_tasks_init.php | Setup migration |