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' and last_error populated. Not retried automatically; remove and re-add to retry.
  • Recurring, ok or failednext_run advances by interval_seconds. The error is recorded in last_error and remains visible until the next successful run.
  • Stuck running — any task in running for more than 5 minutes is reset on the next tick (configurable via resetStuckTasks($maxRuntimeSeconds)).

Errors are also written to BP/php-error.log so you can audit failed runs without querying SQLite.

Scheduler vs. queue

TaskQueueTaskScheduler
TriggerRun immediately when addedRun at a future time, optionally repeating
Worker cadenceContinuous (~2s) via systemdOnce a minute via cron
Use caseEmail send, image processing, anything offloaded nowDaily 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.