Guides

Scheduled work and jobs

Set up background work and operate scheduled execution.

Scheduler and jobs

For manual VPS deployments, install the scheduler orchestrator once per server. Register every deployed project; scheduling is required even without application-specific tasks:

sudo php /var/www/site/core/cli/nimbly.php scheduler:install
sudo php /var/www/site/core/cli/nimbly.php scheduler:project:add site /var/www/site
sudo php /var/www/site/core/cli/nimbly.php scheduler:cron:install --user=www-data

This creates /etc/cron.d/nimbly-scheduler, which runs /usr/local/bin/nimbly-scheduler-orchestrator every minute as www-data, and /etc/logrotate.d/nimbly-scheduler, which rotates the scheduler log weekly. The orchestrator then dispatches registered project schedulers independently. In Docker containers the scheduler runs automatically, so no host cron setup is required.

Run schedule:init once to create project schedule files in ext/cli/:

./nimbly schedule:init

This generates three files:

File Used when
ext/cli/schedule.inc APP_ENV=dev or no env-specific file found
ext/cli/schedule.prod.inc APP_ENV=prod
ext/cli/schedule.stage.inc APP_ENV=stage

The prod and stage schedules include ext:sync, which commits and pushes data changes in ext/ back to the repository every hour. The dev schedule does not. Customize each file independently for environment-specific tasks.

When migrating an existing VPS, first install the orchestrator, register the projects, run scheduler:run --dry-run, run the wrapper once manually as www-data, and confirm /var/log/nimbly-scheduler.log. Back up the old www-data crontab under /etc/nimbly/backups/, then remove only migrated per-project schedule:run lines. Do not remove unrelated cron jobs.

Admin git pull remains available under Settings as a simple self-managed update option. For release-managed sites, prefer a repeatable deploy path: pull the intended git revision, run setup/build/lint, and make sure the scheduler is active.


schedule:run

Runs due scheduled commands for one project. On manual VPS servers with multiple Nimbly projects, do not create one cron entry per project. Use the server-level scheduler orchestrator instead and let it call each registered project's schedule:run sequentially.

Projects may define schedules in ext/cli/. The scheduler selects files in this order:

  1. SCHEDULE_FILE from .env or the process environment, as an explicit file path.
  2. ext/cli/schedule.<env>.inc, where <env> comes from SCHEDULE_ENV, APP_ENV, or NIMBLY_ENV.
  3. ext/cli/schedule.inc.
  4. core/cli/schedule.inc.

Environment aliases are normalized: production → prod, staging → stage, development and local → dev.

Use environment-specific schedule files when staging or development must run background jobs but must not run production-only tasks such as member reminders.

Schedule tasks may use every_minutes for intervals longer than one minute:

[
    'id' => 'uptime-poll',
    'command' => 'uptime:poll',
    'every_minutes' => 2,
]

The value must be a positive integer. A new task is due immediately; after it runs, it becomes due when the clock enters the next interval bucket. Existing every, daily_at, and optional timezone schedules remain supported.

Scheduler last-run state is stored in ext/data/.state/schedule. Existing installs with older ext/data/.config/schedule state are read as a migration fallback until the scheduler writes the new state file.

scheduler:*

Runs multiple project schedulers from one server-level cron. The project schedule files remain project-local in ext/cli/; the server registry is the only source of truth for which projects are orchestrated.

The orchestrator uses these server-managed files by default:

Path Purpose
/usr/local/bin/nimbly-scheduler-orchestrator Wrapper run by cron
/etc/nimbly/scheduler-projects.json Registered projects and defaults
/etc/cron.d/nimbly-scheduler System cron entry
/var/log/nimbly-scheduler.log Scheduler output log
/etc/logrotate.d/nimbly-scheduler Weekly rotation of the log, eight weeks kept

Install it once per server from any current Nimbly core checkout:

sudo php core/cli/nimbly.php scheduler:install
sudo php core/cli/nimbly.php scheduler:project:add site-name /var/www/site-name
sudo php core/cli/nimbly.php scheduler:cron:install --user=www-data

Useful management commands:

php core/cli/nimbly.php scheduler:project:list
sudo -u www-data php core/cli/nimbly.php scheduler:run --dry-run
sudo -u www-data /usr/local/bin/nimbly-scheduler-orchestrator
php core/cli/nimbly.php scheduler:cron:status

scheduler:run takes a short global lock and dispatches two workers per registered project: one runs jobs:run 10 for prompt queue delivery, and the other runs the project's schedule:run. Workers log their completion and exit code to the host scheduler log. A long application task therefore does not delay another project's minute tasks or its own queued alerts. Job runners use a per-project lock so the direct queue worker and mandatory jobs-run task do not dispatch the same job concurrently. The project scheduler retains its own lock, so later cron ticks do not duplicate a long schedule run. Legacy default_delay_after_seconds values in the registry are ignored.

scheduler:run itself always dispatches every registered project's workers immediately (it never waits before spawning), so a slow project cannot delay the next cron tick's dispatch. Concurrency is instead capped inside each scheduler:worker: before running the project's real jobs:run/schedule:run, a worker blocks on a shared slot semaphore, and releases it when done. This keeps a large fleet from pushing every project's worker onto a single-CPU host at the same instant. The cap defaults to 5 simultaneous workers across the whole host and is configurable with NIMBLY_SCHEDULER_MAX_CONCURRENCY; the slot lock files themselves live under NIMBLY_SCHEDULER_SEMAPHORE_DIR (default: the system temp directory).

View this page as Markdown