---
model_tier: medium
name: laravel-scheduling
description: "Use when configuring Laravel task scheduling — cron expressions, frequency helpers, overlap prevention, maintenance mode, or output handling."
domain: engineering
workspaces:
  - engineering
packs:
  - laravel
trust:
  level: professional
install:
  default: false
  removable: true
---

# laravel-scheduling

## When to use

Use this skill when scheduling recurring tasks:
- Artisan command scheduling
- Job dispatching on a schedule
- Closure-based scheduled tasks
- Overlap prevention and maintenance mode handling

## Procedure: Create a scheduled task

1. **Inspect existing schedule** — Read `routes/console.php` (Laravel 11+) or `app/Console/Kernel.php` to identify current cadence, overlap-prevention patterns, and output handling.
2. **Pick frequency and concurrency policy** — Choose a frequency helper (see table) or a cron expression; decide whether `withoutOverlapping()` / `onOneServer()` is required.
3. **Register the task** — Add the `Schedule::command(...)` / `Schedule::job(...)` entry; wire log destinations and `runInBackground()` if needed.
4. **Verify** — Run `php artisan schedule:test` (or `schedule:list`) and confirm timing, output path, and overlap behavior.

### In routes/console.php (Laravel 11+)

```php
use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:generate')->dailyAt('06:00');
Schedule::command('telescope:prune --hours=48')->daily();
Schedule::command('queue:prune-batches --hours=48')->daily();
Schedule::job(new CleanupTemporaryFiles())->hourly();
```

### Frequency helpers

| Method | Schedule |
|---|---|
| `->everyMinute()` | Every minute |
| `->everyFiveMinutes()` | Every 5 minutes |
| `->everyFifteenMinutes()` | Every 15 minutes |
| `->hourly()` | Every hour |
| `->hourlyAt(17)` | Every hour at :17 |
| `->daily()` | Daily at midnight |
| `->dailyAt('13:00')` | Daily at 1 PM |
| `->twiceDaily(1, 13)` | Daily at 1 AM and 1 PM |
| `->weekly()` | Weekly on Sunday at midnight |
| `->weeklyOn(1, '8:00')` | Weekly on Monday at 8 AM |
| `->monthly()` | Monthly on the 1st at midnight |
| `->monthlyOn(4, '15:00')` | Monthly on the 4th at 3 PM |
| `->quarterly()` | Quarterly |
| `->yearly()` | Yearly |

### Cron expressions

```php
Schedule::command('reports:generate')->cron('0 6 * * 1-5'); // Weekdays at 6 AM
```

## Overlap prevention

```php
// Prevent overlapping — skip if previous instance is still running
Schedule::command('import:customers')->hourly()->withoutOverlapping();

// With custom expiration (minutes)
Schedule::command('import:customers')->hourly()->withoutOverlapping(30);
```

## Conditional scheduling

```php
// Only in production
Schedule::command('reports:send')->daily()->environments(['production']);

// Custom condition
Schedule::command('sync:data')->hourly()->when(function () {
    return Config::boolean('services.sync.enabled');
});

// Skip condition
Schedule::command('emails:send')->daily()->skip(function () {
    return app()->isDownForMaintenance();
});
```

## Output handling

```php
// Log output to file
Schedule::command('reports:generate')
    ->daily()
    ->appendOutputTo(storage_path('logs/reports.log'));

// Email output on failure
Schedule::command('import:data')
    ->daily()
    ->emailOutputOnFailure('admin@example.com');
```

## Maintenance mode

```php
// Run even during maintenance mode
Schedule::command('queue:work')->everyMinute()->evenInMaintenanceMode();
```

## Running the scheduler

```bash
# The scheduler itself runs every minute via system cron
* * * * * cd /path-to-project && php artisan schedule:run >> /dev/null 2>&1

# Test locally
php artisan schedule:work   # Runs scheduler in foreground
php artisan schedule:list   # Show all scheduled tasks
php artisan schedule:test   # Run a specific task interactively
```

## Grouped schedules

```php
// routes/console.php — include schedule groups
Schedule::group('reports.php');
Schedule::group('imports.php');

// routes/console/reports.php
Schedule::command('reports:daily')->dailyAt('06:00');
Schedule::command('reports:weekly')->weeklyOn(1, '07:00');
```

## Core rules

- **Always use `withoutOverlapping()`** for long-running tasks.
- **Use `environments()`** to restrict production-only tasks.
- **Log output** for debugging — use `appendOutputTo()`.
- **Group related schedules** in separate files under `routes/console/`.
- **Monitor failures** — use `emailOutputOnFailure()` or Horizon for queued jobs.
- **One cron entry** — only `schedule:run` goes in crontab, everything else in Laravel.

## Output format

1. Schedule definition in console kernel or schedule method
2. Overlap prevention and maintenance mode configuration

## Auto-trigger keywords

- schedule
- cron
- scheduled task
- recurring job
- schedule:run
- withoutOverlapping

### Validate

- Verify schedule runs correctly: `php artisan schedule:list` shows the task.
- Confirm `withoutOverlapping()` is set for long-running tasks.
- Check that maintenance mode handling is configured if needed (`evenInMaintenanceMode()`).

## Gotcha

- `withoutOverlapping()` uses cache locks — if the cache is cleared, overlapping can still happen.
- The model forgets `runInBackground()` for long-running tasks — without it, one task blocks the next.
- Don't schedule tasks every minute unless absolutely necessary — it wastes server resources.

## Do NOT

- Do NOT add multiple cron entries — use Laravel's scheduler for everything.
- Do NOT schedule heavy work directly — dispatch a queued job instead.
- Do NOT forget `withoutOverlapping()` for tasks that may run longer than their interval.
- Do NOT hardcode times — use environment-based config when schedules vary per environment.
