15: Tasks
Phlo has a built-in cross-app cron runner. One system cron entry per app triggers tasks::run every minute; the tasks resource matches declaratively against %app->tasks. No cron syntax in your app, no external scheduler.
15.1: Setup
Three steps.
1. Activate the resource in data/app.json:
{
"resources": [..., "tasks"]
}
2. Describe your tasks in app.phlo:
prop tasks => arr(
cleanup: arr(do: 'account::cleanup', every: '5 minutes'),
poll: arr(do: fn() => external::pull(), every: 'minute'),
backup: arr(do: 'backup::run', daily: '03:00'),
report: arr(do: 'report::weekly', weekly: 'monday 09:00'),
)
3. One cron entry per app with an absolute path:
* * * * * php-zts <app>/www/app.php tasks::run
Place it in /etc/cron.d/example-tasks (system, 6 fields incl. user) or via crontab -u <user> (per-user, 5 fields).
15.2: Schedule
Pick exactly one scheduling key per task:
| Key | Format | Example |
|---|---|---|
every: |
PHP-readable duration string | 'minute', '5 minutes', '2 hours', '1 day' |
daily: |
'HH:MM' |
'03:00' |
weekly: |
'<weekday> HH:MM' |
'monday 09:00' |
every: 'minute' (without a leading number) becomes '1 minute' internally. Parsing via strtotime("+$every", 0).
15.3: Callable (`do:`)
The do: field accepts three forms:
| Type | Example | Is called as |
|---|---|---|
| Closure | fn() => external::pull() |
directly |
'Class::method' |
'account::cleanup' |
account::cleanup() |
| Resource name | 'backup' |
phlo('backup') |
Unlike during a normal request, a task runs outside an HTTP lifecycle: there is no %req, no %session. Write your task so that it is self-contained.
15.4: State on disk (`data/tasks/`)
tasks::run creates data/tasks/ automatically and guards each task with three files:
| File | Contents | When |
|---|---|---|
<name>.last |
raw unix timestamp | Per successful run, for the due check |
<name>.json |
{schedule, return} for the Control Center |
Per successful run |
<name>.lock |
empty (mtime counts) | During a run, TTL 1 hour |
Locks prevent a slow task from lapping itself. The TTL is deliberately 1 hour: a failed task is parked until the lock expires; other tasks keep running as usual.
15.5: Error flow
No try/catch in tasks::run. A Throwable bubbles up to Phlo's framework exception handler and writes to data/errors.json, just like build errors do. The Phlo Control Center shows them in the tasks tab.
15.6: Phlo Control Center
The Phlo Control Center detects data/tasks/ automatically:
- A Tasks tab appears in the nav (only if the directory exists), right after Home.
- Per task: schedule (from JSON), last-run-ago, return value (type-aware: scalar / array / string), lock status.
- The Control Center is fully agnostic about the
tasksresource and the app, it reads purely fromdata/tasks/. Schedule info comes from<name>.json, not via an app route (that would trigger an HTTP response and disrupt the Control Center render).
15.7: Example
prop tasks => arr(
heartbeat: arr(do: 'app::heartbeat', every: 'minute'),
)
static heartbeat => file_put_contents(data.'heartbeat.log', date('Y-m-d H:i:s').' tasks::run fired'.lf, FILE_APPEND | LOCK_EX)
Cron entry:
* * * * * php-zts <app>/www/app.php tasks::run
Every minute tasks::run is invoked, sees that heartbeat is every: 'minute' and lastRun < 60s ago, and runs app::heartbeat(). The files data/tasks/heartbeat.last, .json and .lock are updated; intermediate cron ticks skip the task while it is running.