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

Schedule Bundle Laravel Package

zenstruck/schedule-bundle

Symfony bundle for scheduling repetitive tasks and console commands with a single cron entry. Define schedules in PHP with flexible frequencies, lock protection, and output logging, powered by Symfony Messenger.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the important framing: zenstruck/schedule-bundle is a Symfony bundle, not a Laravel package. It has no service provider, no config/schedule.php, and cannot be installed into a Laravel app (it requires symfony/framework-bundle and the Symfony Scheduler/Messenger stack). So day-to-day you'll use it when you're in a Symfony codebase — or when you're contributing to a package that must run under both. In Laravel itself the equivalent is Illuminate\Console\Scheduling\Schedule; the patterns below map almost 1:1, which is why the conceptual transfer is worth the read.

Minimal steps in a Symfony app:

  1. composer require zenstruck/schedule-bundle (v1.9.0 adds Symfony 8 support, so it resolves cleanly on Symfony 6/7/8 — Symfony 8 only if your PHP is new enough; otherwise Composer picks the older framework, no forced upgrade).
  2. Register Zenstruck\ScheduleBundle\ZenstruckScheduleBundle in config/bundles.php.
  3. Write a provider:
final class MainSchedule implements ScheduleProvider
{
    public function schedule(Schedule $schedule): void
    {
        $schedule->addCommand('app:prune', ['--days' => 30])->daily()->at('03:00');
        $schedule->addCallback(fn () => /* ... */)->every('5 minutes')->withoutOverlapping();
    }
}
  1. php bin/console schedule:list --detail to confirm what's registered and when it next runs.

Where to look first: the Schedule builder API (addCommand, addCallback, frequency helpers, constraints), then config/packages/zenstruck_schedule.yaml for lock and monitor settings. First real use case: moving a handful of crontab lines into version-controlled PHP.

Implementation Patterns

One provider per domain. app:schedule:orders, app:schedule:billing, etc. Providers are autoconfigured via the schedule.provider tag, so a class implementing ScheduleProvider in src/ is picked up with no wiring. Small providers keep schedule:list readable and let you gate registration by environment.

Two execution models, pick deliberately.

  • schedule:run — synchronous: due tasks execute in the command process. Simplest, closest to cron, easiest to debug.
  • schedule:monitor + Messenger — the monitor runs every minute, dispatches RunCommandMessage / RunCallbackMessage onto a bus, and a worker executes them. This gives you retries, failure transport, and parallelism for free, but only if a worker is actually running.

Frequency and constraints compose. Build with every('5 minutes'), daily()->at('03:00'), cron('0 3 * * *'), then attach ->when(fn () => $this->isBusinessDay()), ->withoutOverlapping(), ->timezone('Europe/Paris'). Constraints are evaluated at dispatch time, so they read as guard clauses rather than control flow.

Laravel mapping (for muscle memory):

This bundle Laravel Scheduler
addCommand('app:foo') $schedule->command('app:foo')
addCallback($fn) $schedule->call($fn)
->every('5 minutes') ->everyFiveMinutes()
->withoutOverlapping() ->withoutOverlapping()
->timezone('...') ->timezone('...')
schedule:monitor + worker schedule:run from cron

If you're porting, keep the provider-per-domain split — it survives the move and is better than one fat Kernel::schedule().

Integration tip: route the monitor's bus to your async transport explicitly (zenstruck_schedule.monitor.bus) rather than relying on the default bus, otherwise production traffic silently rides your sync transport.

Gotchas and Tips

The silent-queue trap. With schedule:monitor, tasks are dispatched, not executed. No Messenger worker consuming the configured bus = nothing runs, no error, and the monitor exits 0 every minute. First debugging step whenever "the schedule isn't firing": check worker processes and the failure transport before touching the schedule definitions.

Locks are host-local by default. withoutOverlapping() uses the configured lock factory, which is file/flock-based out of the box. On multi-host or containerised setups that directory isn't shared, so overlap protection quietly does nothing. Point the lock at shared storage or a distributed factory, and verify by running two monitors concurrently on purpose.

Timezone is a two-layer problem. The schedule has its own timezone; the server (and cron) has another. ->at('03:00') means 03:00 in the schedule's timezone — if that's inherited from php.ini/app config rather than set explicitly, DST transitions will bite you twice a year.

schedule:monitor must be cron'd every minute. It is not a daemon. A common misconfiguration is running it hourly "to save resources", which makes every sub-hourly task fire late at best.

Debugging commands: schedule:list --detail for definitions, schedule:run --force to bypass the lock while iterating, and --verbose to see what was actually dispatched. For callbacks, wrap the body in try/catch and log — an uncaught exception in a callback surfaces as a failed message, which is one hop removed from where you're looking.

Upgrading to v1.9.0 is safe. The release is Symfony 8 support, deprecation fixes (from 1.8.1), a PHPStan baseline refresh, and dev-dependency bumps. No API changes, no deprecations introduced, nothing to migrate. It's a straight composer update — worth doing specifically for the deprecation cleanups if you're already planning a Symfony 8 move, since forward-compatible code here removes one blocker later. The only meaningful constraint is transitive: Symfony 8 requires a recent PHP, so confirm your CI matrix before assuming the bump applies everywhere.

Extension points worth knowing: custom ScheduleProvider implementations (autoconfigured), your own Task subclasses for reusable task shapes, and a swapped lock factory. There is no event system to hook into around task execution — if you need cross-cutting behaviour (metrics, audit logging), wrap it in the callback/command itself rather than looking for a listener.

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.
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
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle