@the_hasanov answer is the closest one but it's not complete:
Using his method will run the closure every minute after the date you want!
There is -at least- two methods to avoid this.
1. Using a "already_runned" status
The first one is to store the "already run" status of the schedule somewhere: database or file and use this kind of schedule:
// Using a scheduled closure here to be explicit
// Setting is a generic model to store configuration data.
// it could be replaced by Redis or other key/value storage
$schedule->call(function() {
// Run here the wanted logic
Setting::create(['name' => 'cron_already_runned', 'value' => true]); // Store the already_runned status
})->when(function (){
$already_runned = Setting::firstWhere('name', 'cron_already_runned');
return
optional($already_runned)->value
&&
Carbon::create(2020,4,28,13)->isPast();
});
2. Using two "time-condition" which could be true only once
The second one is to use a double "time-condition" which could be true only once:
$schedule->command('command')->when(function (){
return
Carbon::create(2021,4,28,13)->isPast()
&&
Carbon::create(2021,4,28,14)->isFuture();
});
Using this method is not 100% safe as it could fail if the server is down when the conditions are met