LeaveFlow: Operations and cron jobs
Operating LeaveFlow: Messenger workers, scheduled jobs, console commands for year transition and escalation, mail delivery and queue.
LeaveFlow: Operations and cron jobs
Nothing time-driven happens without a worker
LeaveFlow runs scheduled tasks through the Symfony Scheduler. The scheduler only ticks while a Messenger worker is running. Without a worker there are no expiry warnings, no escalations, no illness alerts, no automatic deactivation after an exit and no automatic year transition.
The stack from
compose.yamldoes not start a worker. Set one up yourself, otherwise the scheduled jobs never run.
graph TD
A["Cron or systemd timer"] --> B["messenger:consume scheduler_notifications"]
A --> C["messenger:consume scheduler_maintenance"]
B --> D["Carryover expiry warning, approval escalation, illness alert"]
C --> E["Year transition, exit deactivation"]
Scheduled jobs
| Job name | Schedule | Transport | Effect |
|---|---|---|---|
year-transition |
0 1 1 1 * (1 January, 01:00) |
scheduler_maintenance |
Creates carryover entries expiring on 31 March from the remainder of last year's regular leave |
exit-deactivation-check |
0 7 * * * (daily 07:00) |
scheduler_maintenance |
Deactivates the user accounts of employees whose exit date has been reached |
entitlement-expiry-check |
0 3 * * * (daily 03:00) |
scheduler_notifications |
Notifies owners of carryover leave that expires within the next 30 days |
approval-escalation-check |
0 * * * * (hourly) |
scheduler_notifications |
Notifies all active admins about pending requests waiting longer than the escalation threshold |
illness-alert-check |
0 6 * * * (daily 06:00) |
scheduler_notifications |
Reports 42 consecutive calendar days of illness to the team lead, the deputy or the admins |
The times are fixed in the application. The bundled PHP image sets date.timezone = Europe/Berlin.
Managing jobs in the interface
Under Admin → System → Cron jobs (/admin/scheduled-jobs) you see frequency, status (Success, Failure, Skipped) and last run per job. You can enable and disable individual jobs there. A disabled job runs as "Skipped". The trigger times themselves cannot be changed.
Managing jobs on the console
docker compose exec -u app app php bin/console app:scheduled-job:toggle list
docker compose exec -u app app php bin/console app:scheduled-job:toggle off year-transition
docker compose exec -u app app php bin/console app:scheduled-job:toggle on year-transition
Setting up workers
There are two transports so that a clogged notification queue cannot delay the year-end work. Both workers must be running. The scheduler does not catch up on ticks missed during an outage.
Cron variant: a worker starts every minute and exits after 60 seconds.
* * * * * cd /var/www/leaveflow && php bin/console messenger:consume scheduler_notifications --time-limit=60 --memory-limit=128M --quiet
* * * * * cd /var/www/leaveflow && php bin/console messenger:consume scheduler_maintenance --time-limit=60 --memory-limit=128M --quiet
The repository ships examples: docs/examples/crontab.example and one .service and .timer per transport under docs/examples/systemd/. With systemd and Persistent=true the timer replays missed minute ticks after the host restarts.
If the instance runs in Docker, start the workers with
docker compose exec -u app app php bin/console messenger:consume ...(for example in a separate container or by cron on the host). Paths such as/var/www/leaveflowin the examples apply to an installation without Docker.
Running jobs by hand
| Command | Effect |
|---|---|
php bin/console app:entitlement:year-transition --year=2026 |
Year transition; --year is the source year (default: previous year), so in January 2027 use --year=2026 |
php bin/console app:entitlement:year-transition --year=2026 --dry-run |
Prints the report only, saves nothing |
php bin/console app:notifications:check-entitlement-expiry |
Runs the carryover expiry warning immediately |
php bin/console app:notifications:check-approval-escalation |
Runs the approval escalation immediately |
The year transition creates one carryover entry for the following year per employee, only if hours of the source year's regular leave remain, the employee did not leave before the target year and no carryover entry for the target year exists yet. It expires on 31 March of the target year. The command is safe to repeat.
Mail delivery and queue
| Environment | Behavior |
|---|---|
APP_ENV=dev (Compose stack) |
Emails are sent synchronously right away, to Mailpit in the stack |
| other environments | Emails and chat messages go through the async transport and need a worker: php bin/console messenger:consume async |
Failed messages end up in the failed transport after three retries. The queue lives in the database by default (doctrine://default).
The sender of all system emails is hard-coded as
no-reply@leaveflow.test(name "LeaveFlow") and cannot be configured. Check that your SMTP server accepts this sender.
Logs and database
Symfony writes to var/log/<environment>.log. The web server logs to the container's standard output, without headers. LeaveFlow ships no backup mechanism: back up the MariaDB database (the volume database_data in the stack) and the public/uploads/ directory with the company logo.