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

Cron Manager Laravel Package

zpm-packages/cron-manager

Cron Manager for Laravel lets you define, organize, and monitor scheduled tasks in one place. Manage cron jobs from code or a dashboard, enable or disable entries, and track upcoming and past runs without digging through server crontabs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require zpm-packages/cron-manager

php artisan vendor:publish --tag=cron-manager-config
php artisan vendor:publish --tag=cron-manager-migrations
php artisan migrate

php artisan schedule:list   # smoke test: does the package load at all?

Pin the version explicitly ("zpm-packages/cron-manager": "1.0.*"). With 0 stars, 0 dependents, and a release dated 2026-05-06, you are the QA team — a caret range will eventually hand you a breaking change on a Tuesday.

Where to look first, in this order

  1. src/*ServiceProvider.php — grep for publishes(, mergeConfigFrom(, commands(, and any Schedule/event listener registration. This tells you how the package hooks in and what the real publish tags are.
  2. config/cron-manager.php — connection, table name, model class, cache/lock store, whether the DB loader is enabled.
  3. src/Models/CronJob.php (or CronEntry/ScheduledJob — confirm the name) — casts tell you which flags are supported (enabled, without_overlapping, run_in_background, timezone).
  4. database/migrations/* — the actual column set. The README will drift; the migration will not.

First use case: move one hotfix-prone schedule out of code.

// routes/console.php — before
Schedule::command('reports:daily')->dailyAt('02:00');

Replace it with a row so ops can retime or disable it without a deploy:

CronJob::create([
    'name'       => 'Daily revenue report',
    'command'    => 'reports:daily',
    'expression' => '0 2 * * *',
    'timezone'   => 'America/New_York',
    'enabled'    => true,
]);

If that row appears in schedule:list and executes once, the package works for you. If it doesn't, stop here and reconsider — the whole value proposition is that one behaviour.

Implementation Patterns

Load DB entries alongside code-defined ones. The package almost certainly either registers a listener or exposes a loader. If it exposes one, call it at the end of your schedule so filesystem-defined jobs still win on conflicts:

// routes/console.php
Schedule::command('backup:run')->daily();

if (config('cron-manager.load_from_database')) {
    app(\Zpm\CronManager\CronManager::class)->registerInto(Schedule::getFacadeRoot());
}

If it auto-registers, check when — a booted() callback that queries the DB will break config:cache and any migrate on a fresh DB. Confirm the provider guards against a missing table.

Keep command pointing at a class, not a shell string. A DB-backed scheduler is a remote-code-execution surface by design. Constrain it:

protected static function booted(): void
{
    static::saving(function (CronJob $job) {
        if (! array_key_exists($job->command, Artisan::all())) {
            throw new InvalidArgumentException("Unknown command [{$job->command}]");
        }
    });
}

Whitelist the allowlist in config, and never let the admin UI accept sh -c.

Add run history yourself. Persisting outcomes is the most common gap in small scheduler packages, and it's what you'll actually want at 3am:

Event::listen(ScheduledTaskFinished::class, function ($event) {
    CronJob::where('command', $event->task->command)
        ->first()?->runs()->create(['exit_code' => $event->task->exitCode, 'ran_at' => now()]);
});

Same for ScheduledTaskFailed and ScheduledTaskSkipped (the last one is how you catch silent withoutOverlapping lock contention).

Multi-tenant / environment scoping. Add a team_id or environment column and override the loader query via a bound repository rather than editing vendor files. Keep production and staging job tables separate — a shared table means staging disables production's invoices.

Testing. Don't assert on the scheduler's internals; assert that a row produces a scheduled event:

$this->travelTo(now()->setTime(2, 0));
Artisan::call('schedule:list');
$this->assertStringContainsString('reports:daily', Artisan::output());

Then invoke the command directly to prove the payload is valid, independent of scheduling.

Deployment. Your system crontab stays exactly one line — * * * * * cd /app && php artisan schedule:run >> /dev/null 2>&1. The package does not and cannot install that for you. Include php artisan schedule:list in your deploy script as a post-migrate check.

Gotchas and Tips

  • Silent no-ops are the default failure mode. A malformed cron expression, a disabled row, or a missing table usually doesn't throw — the job just never runs. Add a health check that counts jobs whose last_run_at is older than twice their expected interval.
  • withoutOverlapping locks live in the cache store, not the DB. With CACHE_DRIVER=array or a per-container file cache, the lock is decorative. Require Redis or the database cache driver in any environment with more than one node.
  • Timezone drift. Laravel evaluates expressions in app.timezone unless you call ->timezone() per task. If the package stores a timezone per row, verify it's actually applied to the Event — a stored column that's never read is a classic. Store and display UTC in the admin UI, convert at the edge.
  • Config/route caching. If the loader runs in boot() and queries on every request, config:cache and Octane will both misbehave. Prefer registering in withSchedule() / a console-only path.
  • Publish tags are guesses. --tag=cron-manager-config may be cron-manager-configs, config, or absent. Run php artisan vendor:publish interactively and read the numbered list before scripting it.
  • Upgrades have no changelog safety net. With zero dependents, nobody else has hit the edge case you're about to hit. Read git diff between tags, not just the release notes, and fork (vcs repository in composer.json) if you need to patch a bug — you will not get an upstream fix on your timeline.
  • Security boundary. Restrict the CRUD UI/routes to a super-admin gate and log every create/update/delete with the acting user. If the package ships routes, check whether they're behind auth + can: middleware by default. Assume not.
  • Extension points that are usually safe: the published config, the CronJob model (subclass and rebind in the container if the config exposes a model key), and event listeners. Avoid subclassing the service provider.
  • Vendor code is your documentation. When behaviour contradicts the README, the source wins — and with a project this young, it will contradict it.
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.
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
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle