Step-by-step
-
1
Create an Artisan command
Generate a new Artisan command with
make:command. Give it a descriptive name. The command's$signaturebecomes the string you use to call it from the scheduler and the terminal.bashphp artisan make:command SendDailyReport -
2
Write the command logic
Open
app/Console/Commands/SendDailyReport.php. Set the$signatureand$description, then write your task logic in thehandle()method. ReturnCommand::SUCCESSat the end so the scheduler knows the command completed without errors.php<?php namespace App\Console\Commands; use Illuminate\Console\Command; class SendDailyReport extends Command { protected $signature = 'report:daily'; protected $description = 'Send the daily summary report to admins'; public function handle(): int { // Your task logic here $this->info('Generating daily report...'); // e.g. Mail::to('admin@example.com')->send(new DailyReport()); $this->info('Report sent successfully.'); return Command::SUCCESS; } } -
3
Register the schedule in routes/console.php (Laravel 11+)
In Laravel 11 and newer, scheduled tasks are defined in
routes/console.phpusing theSchedulefacade. This file is automatically loaded — no service provider registration needed.In Laravel 10 and older, you define the schedule in the
schedule()method ofapp/Console/Kernel.phpinstead. The frequency methods are identical in both versions.php<?php // routes/console.php (Laravel 11+) use Illuminate\Support\Facades\Schedule; Schedule::command('report:daily')->dailyAt('07:00'); // Laravel 10 — app/Console/Kernel.php // protected function schedule(Schedule $schedule): void // { // $schedule->command('report:daily')->dailyAt('07:00'); // } -
4
Use the right frequency method
Laravel ships with fluent frequency helpers for almost every interval. A few of the most useful ones are shown below. Chain multiple constraints to compose precise schedules — for example
->weekdays()->hourly().phpSchedule::command('report:daily')->daily(); // midnight Schedule::command('report:daily')->dailyAt('08:00'); // 08:00 every day Schedule::command('sync:data')->everyFiveMinutes(); // every 5 min Schedule::command('sync:data')->everyFifteenMinutes(); // every 15 min Schedule::command('cleanup')->weekly()->sundays()->at('03:00'); Schedule::command('newsletter')->monthlyOn(1, '09:00'); // 1st of month Schedule::command('ping')->everyMinute(); // every minute Schedule::command('report')->weekdays()->at('09:00'); // Mon–Fri at 9am -
5
Prevent overlapping runs
By default, if a scheduled command is still running when the next execution fires, both instances run simultaneously. Add
->withoutOverlapping()to ensure only one instance runs at a time. Laravel uses an atomic lock to enforce this.phpSchedule::command('sync:orders') ->everyFiveMinutes() ->withoutOverlapping() // skip if previous run is still active ->runInBackground(); // run the command without blocking the scheduler process -
6
Log command output to a file
Use
->appendOutputTo()to capture the command's output (everything printed via$this->info()or echoed) to a log file. This is invaluable for debugging cron failures silently swallowed by the scheduler.phpSchedule::command('report:daily') ->daily() ->appendOutputTo(storage_path('logs/daily-report.log')); -
7
Add the single cron entry on the server
On your production server, add exactly one cron entry — it runs
schedule:runevery minute. Laravel reads all your scheduled tasks from that single command and decides which ones to fire based on the current time.Open the crontab with
crontab -eand add the line below. Replace the path with your actual project path and the PHP binary path (find it withwhich php).bash* * * * * cd /var/www/myapp.com && php artisan schedule:run >> /dev/null 2>&1 -
8
Test the schedule locally
You don't need to wait for cron to test your tasks. Use
schedule:runmanually to fire all tasks due right now, orschedule:workto run the scheduler in the foreground every minute (useful during development).bash# Trigger all due tasks once (same as what cron does) php artisan schedule:run # Run scheduler in foreground, polling every minute php artisan schedule:work # List all registered tasks and their next run time php artisan schedule:list
Tips & gotchas
- Always use `->withoutOverlapping()` on any task that touches external APIs or runs database-heavy operations — duplicate runs cause subtle data corruption that's hard to debug.
- Run `php artisan schedule:list` to verify the next expected run time for each task before deploying to production.
- Set the cron entry to run as the web server user (e.g., `www-data`) not root, so file permissions created during the task match what the app expects.
- Use `->onSuccess()` and `->onFailure()` callbacks to send a notification or write to a log when a scheduled task completes or fails.
Wrapping up
You now have a fully working task scheduler — a single cron entry drives all your scheduled commands, with overlap protection and log output. From here, explore ->sendOutputTo() combined with a Slack or email notification on failure, or look at running closures and anonymous jobs directly in the scheduler for lightweight one-off tasks.