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

Cadence Laravel Package

directorytree/cadence

Cadence adds driver-based scheduling to Laravel Eloquent models. Attach one or many cron or RRULE recurrence schedules to any model, and Cadence tracks and dispatches events automatically when they're due.

View on GitHub
Deep Wiki
Context7

Technical Evaluation

Architecture fit Cadence is a thin, well-scoped persistence layer for recurrence, not a scheduler. It does not replace Laravel's Illuminate\Console\Scheduling\Schedule; it sits alongside it, converting "when should this fire" from code into rows. That distinction is the whole architectural argument.

  • Right fit: recurrence is a property of a domain record and varies per record — per-workspace digests, per-subscription billing reminders, per-report regeneration, per-tenant timezone-aware notifications. The polymorphic schedules table (schedulable_type/schedulable_id) plus a precomputed, indexed next_run_at is the correct data model for this, and it's the design most teams end up hand-rolling badly.
  • Wrong fit: anything that is one system-wide cron. If the answer is "run this at 02:00 every night," use routes/console.php and skip the dependency entirely. The package earns its place only when recurrence is user-configurable or per-record.
  • Composition: driver-based design with a base Schedule::resolveNextOccurrence() and static tap() hooks for configuring the underlying cron/RRULE library is a mature pattern. The v1.1.0 enable/disable support (disabled_at, clearing next_run_at) maps cleanly onto a real product control — "pause my weekly digest" — without delete semantics.

Integration feasibility Low-to-moderate. The happy path is genuinely short: composer require, publish + migrate, implement Schedulable + HasSchedules on a model, addSchedule(), register schedules:run every minute, listen for ScheduleTriggered. A competent Laravel engineer gets one schedule firing in under a day.

The friction is in the edges, not the core:

  1. Driver auto-registration is implicit. Drivers register themselves "when their backing library is installed." Install Cadence without a driver library and addSchedule() fails with no configuration step to point at. This is a discoverability trap that will generate support tickets.
  2. Hard version floor. PHP >= 8.2 and Laravel >= 11.0. On Laravel 10 this is a non-starter, full stop. Verify before any design work.
  3. README has a live bug in its own example ($event->schedule->schedulable_type = Report::class — assignment, not comparison, in a shouldQueue predicate). Small, but it signals a documentation surface that is one README and a changelog, with no docs site, no API reference, and no upgrade guide beyond per-release notes. Budget for reading src/.
  4. Publishing migrations means the schedules schema is now yours. Future upstream schema changes require manual re-publish-and-diff, as called out in the v1.1.0 upgrade notes.

Technical risk

  • Bus factor. 58 stars, zero dependents, single-vendor (DirectoryTree, reputable but small). Release history is v1.0.0 → v1.0.1 → v1.0.2 → v1.1.0: four releases, three of them trivial fixes (trait added to event, migration filename fix, timezone mutation fix). Low churn is good for stability and bad for the signal that anyone else is exercising it. MIT plus a small, readable codebase means forking is a realistic fallback — that materially de-risks adoption.
  • Correctness depends entirely on one command. next_run_at is the single source of truth and only schedules:run advances it. If the scheduler entry is missing, misconfigured, or withoutOverlapping is holding a stale lock, schedules silently stop firing with no error. This is the highest-severity risk and it is a monitoring problem, not a code problem.
  • Fan-out semantics are underspecified in the docs. The command queries due schedules, dispatches ScheduleTriggered per row, and advances next_run_at. What happens when a listener throws mid-run is not documented — whether next_run_at was already advanced (lost run) or not (repeated run next minute). Treat delivery as at-least-once and make listeners idempotent.
  • No built-in multi-node coordination. withoutOverlapping() is local to the host unless you add onOneServer() with a shared cache. Two app servers and a naive scheduler config produces duplicate dispatches.
  • No chunking, batching, or backpressure mentioned in the run command. A backlog after an outage becomes one large synchronous query plus N dispatches.
  • Don't run below v1.1.0. The timezone mutation bug fixed there corrupts date arithmetic for timezone-aware schedules — a silent-wrong-time failure class that is very hard to diagnose in production.

Key questions to resolve before committing

  1. What is the realistic steady-state count of schedules rows and due-per-minute rate? (Determines whether a single schedules:run is adequate.)
  2. How many app nodes run the scheduler, and do we have a shared cache for onOneServer()?
  3. Do we need RRULE at all, or is cron sufficient? Cron is effectively free (transitive Laravel dep); RRULE adds a second third-party library to vet for a feature we may not ship.
  4. What is our catch-up policy after downtime — skip missed occurrences, or fire once for the most recent? Cadence does the former (advances to next occurrence); does the product agree?
  5. Are listeners queued? If any run synchronously inside the command, one slow handler blocks the entire minute's dispatch.
  6. Who owns this if upstream goes dormant for 12 months? (Answer should be "we can, it's ~500 lines of MIT code.")

Integration Approach

Stack fit Cadence assumes a conventional Laravel monolith or modular monolith with an Eloquent domain layer, a working queue, and a single scheduled-entry pattern. It fits a Laravel 11/12 codebase with queue workers already in place. It does not fit Laravel 10, nor a multi-tenant setup where schedules must be isolated per database — the schedules table lives in the app's default connection and tenancy is not addressed.

Migration path Purely additive and low-blast-radius:

  • One new schedules table, no changes to existing models or schema.
  • Coexists indefinitely with existing routes/console.php entries; nothing forces a migration.
  • Can be piloted on a single model behind a feature flag with zero impact on other code paths.
  • Rollback is php artisan migrate:rollback plus removing the scheduler entry — the only residual state is orphaned listener code.

Compatibility

  • PHP 8.2+ / Laravel 11+ are hard gates; confirm against composer.json first.
  • Driver library is an explicit dependency choice. Recommendation: cron driver only for v1 (dragonmantank/cron-expression is already in Laravel core). Add RRULE only when a concrete product requirement demands "every second Tuesday" or "last business day of the month" semantics that cron expresses badly. Each added driver is another library in the supply chain for marginal feature gain.
  • Base driver and tap() hooks make a custom driver viable if we need bespoke recurrence (e.g., business-calendar-aware), at the cost of owning that logic.

Sequencing

  1. Spike (1–2 days, branch only). Install, migrate locally, wire one model + one queued listener, manually set next_run_at in the past, run schedules:run, confirm event dispatch, model rehydration via SerializesModels, and next_run_at advancement. Separately test disable()/enable() semantics — specifically confirm re-enable computes from now, not the old anchor, so a paused daily schedule doesn't fire a backlog.
  2. Wrap before spreading. Introduce an internal RecurrenceService / adapter that owns addSchedule, disable, enable, and expression validation. Do not let CronSchedule leak into controllers, Livewire components, or Filament resources. This is the single highest-leverage decision: it keeps the vendor swappable and gives you one place to add validation, audit, and telemetry.
  3. Product surface. Schedule CRUD, timezone picker (default from the tenant/user), pause/resume toggle, and human-readable "next run" display sourced from next_run_at. Copy must state that re-enabling starts from now.
  4. Ops hardening before GA. Scheduler entry with ->withoutOverlapping()->onOneServer()->everyMinute(); alerting on overdue next_run_at and on schedules stuck at NULL; queue-based listeners with idempotency keys.
  5. Generalize. Only after one model is stable in production for a few weeks, extend to the next two or three schedulable models — using per-domain listeners filtered by shouldQueue() predicates, not one large switch.

Operational Impact

Maintenance Small, stable surface: one table, one command, one event, four driver classes. Pinning the version and treating upgrades as routine is sufficient. The real maintenance line item is the published migration — the schema is now ours to evolve, and upstream changes require manual reconciliation. Watch Laravel major releases: a package at 58 stars with four lifetime releases will not always be patched promptly, so budget a "can we patch it ourselves" reserve. MIT licensing and a compact codebase make that reserve credible rather than theoretical.

Support Community-only. No vendor SLA, no docs site, no issue-triage guarantees. All debugging knowledge lives with the adopting team. Mitigate by writing an internal runbook at adoption time covering: where next_run_at lives, how to manually force a schedule, how to verify the scheduler entry, and how to inspect the event payload. Without that runbook, the first incident will be expensive regardless of how simple the package is.

Scaling The read path is fine: an indexed next_run_at <= now() query per minute handles tens of thousands of rows without drama. The scaling problem is fan-out, not query cost:

  • Every due schedule dispatches an event. At hundreds of concurrent due schedules, the run command's wall-clock time is dominated by dispatch overhead and any synchronous listener work. All listeners must be queued, non-negotiable.
  • At high volume, the single-minute batch becomes a thundering herd on the queue. Consider spreading work by listener-side delay or by sharding schedules:run by schedulable_type once the single command's runtime approaches the minute boundary — that's the natural break point, and it's a cheap change.
  • Multi-node deployments require onOneServer() plus a shared cache; without it, duplicate execution scales linearly with node count.
  • No pruning story: rows accumulate. Add a retention/archive job for disabled or orphaned schedules (soft-deleted parent models leave dangling polymorphic rows — clean these up on model delete).

Failure modes (ordered by likelihood × severity)

  1. Silent stall — scheduler entry missing, lock stuck, or queue worker down. Symptom: next_run_at in the past and growing. Highest severity because it is invisible. Mitigation: alert on MIN(next_run_at) < now() - 15 minutes.
  2. Double dispatch — multi-node without onOneServer(), or a listener that partially succeeded before throwing. Mitigation: idempotency keys in handlers; treat as at-least-once.
  3. Lost run on listener exception — if next_run_at is advanced before the listener completes, the occurrence is gone. Mitigation: idempotent handlers plus a dead-letter path; do not rely on the package for retry.
  4. Opaque setup failure — driver library absent, migration unpublished, or model missing the Schedulable contract. Symptom: confusing exception at addSchedule() or a SQL error at runtime. Mitigation: validate at boot / in the internal adapter, and cover in the runbook.
  5. Wrong-time firing — timezone handling. Fixed in v1.1.0, so pin >=1.1.0 and never downgrade. Verify DST transitions explicitly in the spike; they are the classic failure for "9am local" schedules.
  6. Unintended backlog on resume — a long-disabled schedule re-enabled. Cadence correctly computes from now; product copy must say so, or users will report "it fired immediately."

Ramp-up Engineering: roughly half a day to first firing, one to two days to production-grade wiring with the adapter, monitoring, and idempotent listeners. The learning curve is not the code — it's the product semantics the team must decide and document: catch-up policy, pause/resume behavior, timezone precedence (schedule vs. tenant vs. app default), and whether schedules survive deletion of their parent model. Those decisions outlast the package and should be settled before the first line of integration code.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator