Custom Artisan commands are the right tool for anything that needs to run outside a web request — a nightly cleanup job, a one-off data fix, a scheduled report — and Laravel makes creating one a single command.
1. Generate the command
php artisan make:command PruneStaleInvites
This creates app/Console/Commands/PruneStaleInvites.php with two properties you'll edit: $signature (how it's called) and $description (what shows in php artisan list).
2. Define the signature, including arguments and options
protected $signature = 'invites:prune {--days=30 : Delete invites older than this many days}';
protected $description = 'Delete pending invites older than the given number of days.';
public function handle(): int
{
$days = (int) $this->option('days');
$count = Invite::where('status', 'pending')
->where('created_at', '<', now()->subDays($days))
->delete();
$this->info("Deleted {$count} stale invites older than {$days} days.");
return self::SUCCESS;
}
Run it:
php artisan invites:prune --days=14
Why it might not show up in `php artisan list`
On a fresh Laravel install, commands placed in app/Console/Commands/ are auto-discovered — no manual registration needed. If a command genuinely doesn't appear, the usual cause is a syntax error in the file (check php artisan list output for a warning) or, on an older project still using the pre-Laravel-11 Console/Kernel.php structure, a missing entry in that file's commands() method if auto-discovery was disabled there.
Scheduling it
Once it works manually, scheduling it is one line — in Laravel 11+, in routes/console.php or bootstrap/app.php's withSchedule(); in Laravel 10 and earlier, in Console/Kernel.php's schedule() method:
Schedule::command('invites:prune --days=30')->daily();