Task Scheduler
True\TaskScheduler schedules PHP task scripts for a future time, or on a recurring interval, or both. It's the deferred counterpart to TaskQueue (which runs tasks immediately).
How it works
The scheduler keeps a SQLite table of upcoming tasks (next_run, optional interval_seconds). A small worker script invoked once a minute by cron asks the scheduler to run any tasks whose next_run is in the past. Recurring tasks are automatically re-scheduled; one-shots are deleted on success or kept with an error message on failure.
Setup
The runner is self-contained — no project-level bootstrap file is required. The SQLite database is created on first use at BP/app/data/scheduled-tasks.sqlite; no migrations to run. Just install the cron line and start scheduling.
* * * * * /usr/bin/php /path/to/project/vendor/truecastdesign/true/workers/scheduledTaskRunner.php >> /path/to/project/logs/scheduler.log 2>&1
The runner derives the project root from its own location, loads composer's autoloader, builds a fresh \True\App (with site.ini loaded if present), and calls runDue().
Scheduling tasks
Drop your task script into app/tasks/, then schedule it from anywhere in your app. Construct a TaskScheduler against the same SQLite file the runner uses.
One-shot at a specific time
$scheduler = new True\TaskScheduler(BP . '/app/data/scheduled-tasks.sqlite');
$scheduler->addTask('cleanup.php', ['olderThan' => '30 days'], [
'name' => 'Clean up temp uploads',
'runAt' => '2026-05-01 14:00:00',
]);
Recurring at a fixed interval
// Every 10 minutes
$scheduler->addTask('refresh-cache.php', [], [
'name' => 'Cache refresh',
'intervalSeconds' => 600,
]);
Recurring with a fixed start time (e.g. daily at 3am)
$scheduler->addTask('reindex.php', [], [
'name' => 'Daily search reindex',
'runAt' => 'tomorrow 3:00 AM',
'intervalSeconds' => 86400,
]);
runAt accepts anything strtotime() understands.
Writing a task script
Same convention as TaskQueue: variables and the $App object are extracted into local scope. Throw on failure — the scheduler catches and records the error.
<?php
// app/tasks/refresh-cache.php
// $App is available — the runner builds it with site.ini loaded.
// Per-task variables passed via addTask() are also extracted.
$db = new \Truecast\Hopper($App->getConfig('mysql.ini'));
$db->execute('DELETE FROM cache_entries WHERE expires_at < ?', [date('Y-m-d H:i:s')]);
echo "Cache cleaned at " . date('c') . "\n";
Inspecting and managing tasks
$scheduler = new True\TaskScheduler(BP . '/app/data/scheduled-tasks.sqlite');
// All upcoming tasks, soonest-first
$tasks = $scheduler->getTasks();
// Remove a scheduled task by id
$scheduler->removeTask(42);
// Recover tasks that crashed mid-run (called automatically before each tick)
$scheduler->resetStuckTasks();
Failure handling
- One-shot, ok — row is deleted.
- One-shot, failed — row is kept with
last_status = 'failed'andlast_errorpopulated. Not retried automatically; remove and re-add to retry. - Recurring, ok or failed —
next_runadvances byinterval_seconds. The error is recorded inlast_errorand remains visible until the next successful run. - Stuck
running— any task inrunningfor more than 5 minutes is reset on the next tick (configurable viaresetStuckTasks($maxRuntimeSeconds)).
Errors are also written to BP/php-error.log so you can audit failed runs without querying SQLite.
Scheduler vs. queue
| TaskQueue | TaskScheduler | |
|---|---|---|
| Trigger | Run immediately when added | Run at a future time, optionally repeating |
| Worker cadence | Continuous (~2s) via systemd | Once a minute via cron |
| Use case | Email send, image processing, anything offloaded now | Daily reindex, hourly cleanup, run-once-at-a-future-time tasks |
You can use both side-by-side — they have separate SQLite databases and separate runners.