15: Tasks

Phlo has a built-in cross-app task runner. Something triggers tasks::run every minute, a system cron entry or the Phlo Daemon; 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. Trigger tasks::run every minute. With the Phlo Daemon, list the app in its host map and the daemon runs tasks::run for you, no cron line at all:

require('./phlo-daemon.js')(3001, php, { '<host>': { app: '<app>/www/app.php', build: false } })

Without the daemon, a system cron entry does the same:

* * * * * 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). Either way the schedule itself stays in %app->tasks; the trigger only supplies the minute tick.

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:

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 ticks skip the task while it is running.

15.8: Scheduling without cron

If the Phlo Daemon is running, it runs tasks::run for you every minute, so you do not need the per-app cron entry. Every app in the daemon's host map gets the minute tick:

require('./phlo-daemon.js')(3001, '/usr/bin/php-zts', {
	'example.com': { app: '/var/www/example/www/app.php', build: false },
})

The daemon ticks tasks::run once a minute; your %app->tasks schedule (the every: on each task) decides what actually fires, exactly like the cron tick. Everything else is identical: the same %app->tasks matching, the same due-check, the same data/tasks/ state. Cron stays the fallback for hosts without a daemon; pick one trigger, not both.

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.