Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Smart Scheduler Laravel Package

jiordiviera/laravel-smart-scheduler

A Laravel package for smarter task scheduling: manage scheduled jobs with dynamic frequencies, overlap prevention, condition-based runs, and clear logging. Simplify cron management and keep recurring tasks predictable, organized, and easy to monitor.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Upgrade and publish what changed

    composer require <vendor>/smart-scheduler:^1.1
    php artisan vendor:publish --tag=smart-scheduler-config   # pick up stuck_timeout_minutes + notify_on_stuck
    

    If your config file was published previously it will not be overwritten — diff it against the package's config/smart-scheduler.php and add the two new keys manually, otherwise config('smart-scheduler.stuck_timeout_minutes') resolves to null and detection silently does nothing useful.

  2. Run the new migration if one ships with the release. STATUS_STUCK needs to be storable; if status is an enum column, confirm the migration adds the value before you deploy code that writes it.

  3. Look here first:

    • config/smart-scheduler.php → stuck_timeout_minutes (default 60), notify_on_stuck.
    • SmartSchedulerManager → the new pre-flight stuck sweep that runs before the overlap check.
    • App\Models\...\ScheduleRun → isStuck().
    • SmartNotifierInterface → sendStuckNotification().
  4. First run — always dry:

    php artisan smart-scheduler:detect-stuck --timeout=30 --dry-run
    

    Confirm the rows it would mark match your expectations (timezone, long-running imports, queue-backed jobs) before letting it write.

  5. Schedule it once you're happy — this is the intended steady state:

    // routes/console.php (Laravel 11+) or Console\Kernel::schedule()
    Schedule::command('smart-scheduler:detect-stuck')->everyFiveMinutes();
    

Implementation Patterns

Tune the threshold to your slowest legitimate job, not your average. stuck_timeout_minutes is a single global value. If you have a nightly 90-minute report, a 60-minute default will flag it every night. Either raise the global default and accept slower detection for short jobs, or run the command with an explicit --timeout on a separate schedule entry per task group:

Schedule::command('smart-scheduler:detect-stuck --timeout=15 --no-notify')
    ->everyFiveMinutes()
    ->environments(['production']);

Schedule::command('smart-scheduler:detect-stuck --timeout=180')
    ->hourly()
    ->environments(['production']);

Let the manager handle the common case. Since SmartSchedulerManager now sweeps for stuck tasks before the overlap check, the most valuable pattern is simply not fighting it: your next scheduled run will self-heal stale rows. The standalone command exists for the case where nothing else is scheduled to run — a once-daily report, a cron that's been disabled — and the row would otherwise sit in RUNNING forever.

Wire isStuck() into your observability surface rather than re-deriving the logic:

ScheduleRun::query()
    ->where('status', ScheduleRun::STATUS_RUNNING)
    ->get()
    ->filter->isStuck()
    ->each(fn ($run) => $run->schedule->name);

That keeps the definition of "stuck" in one place as the package evolves, instead of a second where('started_at', '<', now()->subMinutes(...)) in your dashboard.

Custom notifiers must be updated. If you implemented SmartNotifierInterface (Slack, PagerDuty, etc.), add sendStuckNotification():

public function sendStuckNotification(ScheduleRun $run): void
{
    Slack::send(":warning: {$run->schedule->name} stuck since {$run->started_at}");
}

Prefer implementing the full contract over adding the method ad hoc — a class ... implements SmartNotifierInterface that's missing the method is a fatal error at container resolution, not a graceful degradation.

Use --no-notify in non-production and in CI. Detection is a state change; notification is a side effect. Running detection on staging shouldn't page anyone.

Dry-run in a deploy step / health check to surface drift without mutating state:

$stuck = Artisan::call('smart-scheduler:detect-stuck', ['--dry-run' => true, '--no-notify' => true]);

Gotchas and Tips

  • The interface change is a breaking change for downstream implementors. sendStuckNotification() was added to SmartNotifierInterface. The release note files this under "Changed", but any third-party or in-house notifier implementing that contract will fatal on composer update. Grep your codebase (and any internal packages) for SmartNotifierInterface before upgrading.
  • Auto-detection only fires when a new run starts. A scheduler where every task has stalled, or a queue worker that died entirely, produces no new runs and therefore no sweep. Don't treat the manager hook as a replacement for the scheduled detect-stuck command — they cover different failure modes.
  • Stuck rows are cleared before the overlap check, which changes concurrency semantics. Previously a hung run blocked the next one as an overlap; now it's marked stuck and the new run proceeds. That's usually what you want, but if two instances run concurrently against shared state, verify the overlap protection still holds for your longest-running tasks. This is the change most worth testing explicitly.
  • Repeated notifications. If a task is genuinely wedged and the sweep runs every five minutes, watch for notification spam. Gate on notify_on_stuck, and consider throttling in your custom notifier (e.g. only notify on transition into stuck, not on every detection pass).
  • Handle the new status everywhere you enumerate statuses. Dashboards, match expressions over status, whereIn('status', [...]), chart legends, and any enum cast will now see an unexpected value. A match without a default arm throwing UnhandledMatchError is the classic symptom.
  • Timezone/clock skew. Detection compares stored timestamps against now(). If app.timezone differs from the DB server's time or you're mid-DST, you'll get false positives. Test with Carbon::setTestNow() rather than waiting an hour.
  • Debugging a false positive: run --dry-run --timeout=<n> with a deliberately large n first; if the row disappears, it's a timeout-tuning problem, not a bug. If it persists, inspect started_at — a null or zeroed timestamp will look infinitely stuck.
  • Rollback plan. Downgrading after STATUS_STUCK rows exist leaves rows in a status the old code doesn't understand. Have a one-liner ready to normalise them (update ... set status = 'failed' where status = 'stuck') before reverting.
  • Extension point: if you need per-task timeouts before the package offers them, wrap the command in your own artisan command that reads a per-schedule map and invokes detection with --timeout per group — cheaper than forking, and it keeps you on the upgrade path.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib