Skip to Content
PluginsScheduled tasks

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

  1. Write the job — extend App\Plugins\Scheduling\PluginScheduledJob and implement run(). 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 as actor_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(); } }
  2. 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); make run() safe to repeat.
  • No session/permission context: system jobs carry empty permissions and write directly to models; they do not pass through can: gates.
Last updated on