husam-tariq/filament-database-schedule
A Filament plugin for managing Laravel scheduled tasks stored in the database. Create, edit, pause, and delete cron jobs from a visual panel interface, with run history and task logs, without touching code or redeploying.
Install and register:
composer require husam-tariq/filament-database-schedule
php artisan vendor:publish --tag="filament-database-schedule-migrations"
php artisan migrate
Register the plugin on your panel (typically app/Providers/Filament/AdminPanelProvider.php):
use HusamTariq\FilamentDatabaseSchedule\FilamentDatabaseSchedulePlugin;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->plugin(FilamentDatabaseSchedulePlugin::make());
}
Where to look first:
config/filament-database-schedule.php (if published) — for table name, navigation grouping, and route prefix overrides.src/Models/ directory — the Schedule model is the source of truth for what your scheduler will execute. Inspect its casts and relationships before writing custom code.without_overlapping, on_one_server, etc.).Your first use case: Replace a hard-coded app/Console/Kernel.php entry. Create a schedule in the UI for inspire running * * * * *, make sure your server's cron is calling php artisan schedule:run every minute, and confirm the entry shows in php artisan schedule:list. Once that works, migrate real jobs (backups, cleanup commands, queued jobs) out of Kernel.php into the database one at a time.
Database-backed vs. code-backed scheduling. The plugin exposes a Schedule model, so you can seed schedules from a migration and keep Kernel.php for anything environment-specific or requiring closures. Treat the database as the "runtime ops" layer (things admins want to toggle) and keep Kernel.php for structural things (queued jobs that must always run).
Seeding schedules per environment. In DatabaseSeeder or a dedicated seeder:
Schedule::create([
'name' => 'Nightly reports',
'command' => 'reports:generate',
'expression' => '0 2 * * *',
'timezone' => config('app.timezone'),
'without_overlapping' => true,
'on_one_server' => true,
'enabled' => app()->isProduction(),
]);
This gives you deterministic environments — enabled = false in local/staging so you don't accidentally fire production commands during development.
Controlling access. Because this is a Filament resource, gate it with your existing policies. If your Schedule model lives in the plugin's namespace, create a policy and point at it:
Gate::policy(
\HusamTariq\FilamentDatabaseSchedule\Models\Schedule::class,
\App\Policies\SchedulePolicy::class
);
Restrict to a super_admin role or a dedicated permission (manage schedules) so only trusted operators can add arbitrary artisan commands — this resource is effectively remote command execution.
Audit + approval workflow. Add observers on the Schedule model to log who created/edited/toggled what. A common pattern: a schedule_audit_logs table with schedule_id, user_id, action, diff. Hook created, updated, deleted and store the JSON snapshot. This is essential when the resource is exposed to more than one admin.
Multi-panel / tenant integration. If you're on Filament's multi-tenancy, scope the resource query. Either override getEloquentQuery() in a subclass or register the plugin on a panel where tenancy is already enforced. For per-tenant schedules, add a tenant_id column and filter by Filament::getTenant() inside a custom policy or resource query.
Testing. Don't test the plugin — test your schedules. Use Bus::fake(), Queue::fake(), and $this->artisan('schedule:run') while asserting your commands ran. For the schedule expressions themselves, a small unit test that asserts CronExpression::factory($schedule->expression)->isDue() at a fixed time is fast and catches cron-edit regressions.
Deployment pattern. Ship a deploy step that runs php artisan schedule:list and greps for the expected entries. It's the cheapest smoke test that proves the database schedules are being loaded by the scheduler on the live box.
schedule:run must already be running. This package does not schedule itself — your server cron or Supervisor (via schedule:work) must still invoke Laravel's scheduler. A very common support ticket is "I added a schedule in the UI and nothing happened" because cron was never configured. Document it prominently for your team.
Cron expression validation is not always strict. Invalid expressions can be saved if the field doesn't validate against CronExpression. Add a Rule::in or a custom validator on the form, or a model-level cast/exception, because a bad row can silently break schedule:run for all schedules in some Laravel versions.
Timezone drift. Laravel runs the scheduler in config('app.timezone') unless a schedule specifies its own. If the plugin exposes timezone, make sure it defaults to the app timezone rather than UTC or the server's local zone. Set this explicitly per schedule; relying on defaults is a debugging nightmare.
Overlapping jobs are silent killers. without_overlapping and on_one_server need to be explicit on long-running commands. Without them, a slow backup will stack and eventually saturate the queue/DB. Combine without_overlapping with a meaningful job timeout so stuck locks don't block future runs indefinitely.
Closures and command strings are not interchangeable. The database stores a command string (e.g. backup:run --only-db); you cannot store a PHP closure. If your existing Kernel.php used ->call(function () { ... }), refactor that logic into an artisan command first, then add it via the UI.
Cache/invalidation after edits. Filament caches some resource metadata; after deploying plugin upgrades run php artisan filament:optimize-clear (or php artisan optimize:clear) to avoid stale form schemas. If you see old form fields after an update, this is almost always the cause.
Logging on failure. Laravel's scheduler can email on failure via ->emailOutputOnFailure(), but that's not exposed in the UI by default. If you need failure alerts, add an events table and hook Scheduling/ScheduledTaskFailed events in a service provider, or extend the resource with a custom field.
Locked-down commands. Never expose the resource to users who shouldn't run arbitrary artisan commands. Even with a policy, the underlying command field is a text input — consider a select populated from Artisan::all() if you want to constrain choices to an allowlist.
Extension points. The plugin is small enough that subclassing the resource is the cleanest extension path: extend the resource class, override form() and table(), and swap the plugin registration to point at your subclass. Similarly, extend the Schedule model if you need extra columns — but be aware the plugin's resource may not render those columns unless you also override table().
Upgrade pinning. The listed last release is 2026-07-17; pin to a minor version in composer.json (^1.0 or similar) rather than *. Filament plugins move fast and a major bump often changes form field names, migration columns, or plugin registration signatures.
Debugging checklist when a schedule doesn't fire: (1) php artisan schedule:list — is it visible? (2) is cron actually running? (3) php artisan schedule:test (Laravel 11+) to invoke by name. (4) Check storage/logs/laravel.log for the command's own failures vs. the scheduler's. (5) Verify enabled is true and the timezone is right.
How can I help you explore Laravel packages today?