Skip to content

Powered by Grav + Helios

Scheduler

Scheduler

Manage Grav's cron-based scheduler. List registered jobs, inspect cron installation status, review execution history, and trigger manual runs. Permissions: api.scheduler.read / api.scheduler.write.

Manage Grav's cron-based scheduler. List registered jobs, inspect cron installation status, review execution history, and trigger manual runs.

Permissions: api.scheduler.read / api.scheduler.write.

List Scheduler Jobs

GET /scheduler/jobs
List every registered scheduler job (both plugin-registered and system jobs like cache-purge, cache-clear, backups). Each job includes its cron expression, enabled flag, status (`pending` until it has run), last-run timestamp and what triggered it (`cron`, `webhook`, `manual` and so on), the next scheduled run, whether it is `overdue` (missed its last scheduled slot), and any error from the last run. A job defined as a closure reports its `command` as `(closure)`. Fires `onSchedulerInitialized` so system jobs show up even if no one has touched the scheduler yet this request.
JSON
{"data": [{"id": "default-site-backup", "command": "Grav\\Common\\Backup\\Backups::backup", "expression": "0 3 * * *", "enabled": true, "status": "success", "last_run": "2026-03-26T03:00:00+00:00", "last_run_trigger": "cron", "next_run": "2026-03-27T03:00:00+00:00", "overdue": false, "error": null}]}

Response Codes

200 Jobs returned.
401 Unauthorized.
403 Missing `api.scheduler.read` permission.

Scheduler Status

GET /scheduler/status
Report the scheduler's crontab installation state, the cron + scheduler commands Grav would expect, the effective user (`whoami`), health diagnostics, and whether the optional scheduler-webhook plugin is installed/enabled. Drives the "is cron set up?" panel on the Admin2 dashboard. Every field is best-effort, so a host that can't report how its scheduler is triggered still gets a 200. For demo accounts, `cron_command`, `scheduler_command` and `whoami` are replaced with `(hidden in demo mode)`.
JSON
{"data": {"crontab_status": "installed", "cron_detection": "crontab", "process_available": true, "last_run": "2026-08-20T12:00:00+00:00", "last_manual_run": null, "environment": "www.example.com", "environment_has_overrides": true, "last_run_environment": "cli", "cron_command": "* * * * * cd /path/to/grav && /usr/bin/php bin/grav scheduler", "scheduler_command": "bin/grav scheduler", "whoami": "www-data", "health": {}, "triggers": [], "webhook_installed": false, "webhook_enabled": false}}

Response Codes

200 Status returned.
401 Unauthorized.
403 Missing `api.scheduler.read` permission.

crontab_status is one of: not_installed, installed, unknown. cron_detection names how that was worked out, or unavailable when detection failed. process_available is false when PHP's proc_open is disabled, in which case POST /scheduler/run returns 501. last_run is the last run by a real trigger (cron or webhook); last_manual_run is the last run started from the admin or the API, which never counts as proof that cron is working.

environment is the environment this request resolved to (normally the hostname), environment_has_overrides says whether it has its own user/env/<name>/config folder, and last_run_environment is the environment the scheduler last actually ran under (cli for a crontab line that does not pass --env). When the last two disagree and overrides exist, jobs defined only in the override never run from that trigger; scheduler_command already carries the --env flag to fix the crontab line.

Job History

GET /scheduler/history
Paginated job execution history sorted by most recent first. Only the latest run of each job is kept, so this lists one entry per job that has run, not every past run. Each record has the job id, its last status (`unknown` if none was recorded), the last run as an ISO-8601 timestamp, and any error message captured on failure.

Parameters

Name Type Description
page optional integer Page number (default 1).
per_page optional integer Items per page (default `plugins.api.pagination.default_per_page`, 20).
JSON
{"data": [{"job_id": "default-site-backup", "status": "success", "last_run": "2026-03-26T03:00:00+00:00", "error": null}], "meta": {"pagination": {"page": 1, "per_page": 20, "total": 12, "total_pages": 1}}, "links": {"self": "/api/v1/scheduler/history?page=1&per_page=20"}}

Response Codes

200 History returned.
401 Unauthorized.
403 Missing `api.scheduler.read` permission.

Run Scheduler

POST /scheduler/run
Run scheduler jobs now, without waiting for cron. By default (`mode: overdue`) it runs every enabled job that missed the last slot its schedule gave it, which is what a manual trigger almost always wants: a job is only "due" during the exact minute its cron expression names. Runs started this way are recorded as manual and never count as evidence that cron is set up. Returns a summary, per-job results, and every job's state after the run. For demo accounts each job's `output` is replaced with `(hidden in demo mode)`.

Parameters

Name Type Description
mode optional string `overdue` (default) runs the jobs that missed their last scheduled slot, `due` runs only what a cron tick this minute would run, `all` runs every enabled job whatever its schedule says.
job optional string Run only this job id, whatever its schedule says. Ignores `mode`, and the response reports `mode: job`.
force optional boolean Legacy alias for `mode: all`.
JSON
{"mode": "overdue"}
JSON
{"data": {"message": "1 job ran successfully.", "mode": "overdue", "forced": false, "jobs_run": 1, "jobs_failed": 0, "duration": 1.42, "results": [{"id": "default-site-backup", "successful": true, "output": ""}], "job_states": {"default-site-backup": {"state": "success", "last-run": 1790000000, "trigger": "manual"}}}}

Response Codes

200 Run completed. `jobs_run` is 0 (with the message `Nothing to run: no jobs were due.`) when nothing matched.
400 Unknown `mode`.
401 Unauthorized.
403 Missing `api.scheduler.write` permission.
404 No job with the requested `job` id.
409 The requested job is disabled.
501 Running jobs is not available because PHP's `proc_open` is disabled on the host.