Markus Michalski FAQ

LeaveFlow: Operations and cron jobs

Operating LeaveFlow: Messenger workers, scheduled jobs, console commands for year transition and escalation, mail delivery and queue.

Deutsche Version

Back to overview

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.yaml does 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/leaveflow in 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.