Plugin scheduled tasks (cron)
A plugin declares recurring work by implementing ProvidesScheduledTasks on its
ServiceProvider and shipping one PluginScheduledJob subclass per task. The platform
registers each task with Laravel’s scheduler and fans it out to the queue — one job per
tenant that has the plugin installed and activated (perTenant: true), or a single
tenant-less job (perTenant: false).
Author a task
-
Write the job — extend
App\Plugins\Scheduling\PluginScheduledJoband implementrun(). For a per-tenant task,run()executes with the tenant’s system context already bound, so tenant-scoped model reads/writes and audit “just work”; the audit actor is recorded asactor_type='system'(nil-UUID actor id).class FeedRetentionJob extends PluginScheduledJob { public const RETENTION_DAYS = 90; protected function run(): void { FeedEvent::query()->where('created_at', '<', now()->subDays(self::RETENTION_DAYS))->delete(); } } -
Declare it — implement
ProvidesScheduledTasks::scheduledTasks():public function scheduledTasks(): array { return [[ 'key' => 'retention-sweep', // unique within the plugin 'cron' => '0 3 * * *', // standard 5-field cron 'perTenant' => true, // one job per enabled tenant 'job' => FeedRetentionJob::class, ]]; }
That’s all. The entry appears in php artisan schedule:list as plugin:<slug>:<key> and runs
under the Docker scheduler + queue services.
Semantics
- Per-tenant: the platform enumerates
plugin_installs(installed + activated) for your slug and dispatches one job per tenant. Do not enumerate tenants yourself. - Tenant-less (
perTenant: false): one job runs with no tenant context — use for app-global work (e.g. fan-out that itself resolves scope). - Idempotency: jobs may run again on retry (
queue:work --tries=3); makerun()safe to repeat. - No session/permission context: system jobs carry empty permissions and write directly to
models; they do not pass through
can:gates.