Skip to content

Last updated:

How-To: Schedule a Recurring Task

Wire a console command into the scheduler so it runs automatically.

The scheduler lives in routes/console.php (not the legacy app/Console/Kernel.php).

1. Register the schedule

php
// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('queue:reservations:mark-no-shows')
    ->dailyAt('00:05')
    ->withoutOverlapping()
    ->onOneServer();

2. Use the safety guards by default

For any scheduled command, add these three:

GuardWhy
->withoutOverlapping()Prevents two runs from stacking if the previous one took longer than the interval
->onOneServer()In multi-server deployments, only one server runs the job (requires Redis cache)
->runInBackground() (optional)Frees the scheduler tick for long-running jobs

The only schedules in CPR that don't use these guards are very fast cleanup tasks (app:prune-activity-logs).

3. Cadence patterns in use

php
->daily()                     // 00:00
->dailyAt('03:00')            // pick an off-peak hour
->hourly()
->everyFiveMinutes()
->weeklyOn(0, '01:00')        // Sunday 01:00
->cron('*/15 * * * *')        // raw cron when you need it

CPR conventions:

  • Backups: dailyAt('03:00')cpr:backup:database
  • Pruning: dailyAt('02:30')app:prune-audit-log
  • Daily roll-overs (no-show, etc.): dailyAt('00:05') — just after midnight, so the previous day is locked in
  • Avoid dailyAt('00:00') — many other systems run then

4. Verify the schedule

bash
# List every scheduled task with its next run time
php artisan schedule:list

# Trigger a one-off run (does NOT respect overlapping guards)
php artisan schedule:test

# Simulate the scheduler tick locally
php artisan schedule:work

In production the OS cron must call:

cron
* * * * * cd /var/www/cpr-backend && php artisan schedule:run >> /dev/null 2>&1

(In Docker/Sail, the queue container handles this; see compose.yaml.)

5. Pass arguments via array

For commands with options, pass them as an array — string concatenation is fragile:

php
// Good
Schedule::command('cpr:backup:database', ['--source=scheduled', '--keep-days=30'])
    ->dailyAt('03:00');

// Bad — quoting breaks on some shells
Schedule::command('cpr:backup:database --source=scheduled --keep-days=30')->dailyAt('03:00');

6. Failure alerting

Sentry already captures uncaught exceptions in scheduled commands. For commands that should never be silent on failure:

php
Schedule::command('cpr:backup:database')
    ->dailyAt('03:00')
    ->onFailure(function () {
        // Send Slack/Email notification
    });

Checklist

  • [ ] Schedule defined in routes/console.php
  • [ ] withoutOverlapping() + onOneServer() for anything non-trivial
  • [ ] Cadence avoids 00:00 and aligns with surrounding jobs
  • [ ] Options passed as an array, not concatenated
  • [ ] Verified via php artisan schedule:list

CPR - Clinical Patient Records