Queues, jobs & scheduler
The complete scheduled-task inventory, the queue configuration for local and production, and how failures surface.
Queues, jobs & scheduler
Almost everything "automatic" in the product — WhatsApp appointment reminders, follow-up sweeps, Meta lead health, billing lifecycle, retention, backups — rides on two mechanisms: queued jobs and the Laravel scheduler. The entire schedule is declared in routes/console.php; this page transcribes it in full.
Who this is for
Hosting admins and developers who must configure cron/queues and reason about background behavior.
Difficulty: Medium · Time: about 12 minutes.
Queue configuration
| Environment | QUEUE_CONNECTION |
What runs the queue |
|---|---|---|
Local XAMPP (default in .env) |
sync |
Nothing — jobs run inline during the request |
| Production (recommended) | database |
A long-lived worker: php artisan queue:work |
- With
sync, deferred behaviors still work because the product is sweep-based: checks run on a schedule and query state, rather than relying on delayed jobs being processed at a precise moment. - Production worker (one is enough on a small cPanel install; keep it alive via cron or a process manager):
php artisan queue:work --tries=3 --timeout=120 - Failed jobs land in
failed_jobs(Laravel default). Inspect (php artisan queue:failed), retry (php artisan queue:retry all), or flush (php artisan queue:flush). - Jobs may target a named queue; for example push delivery (
SendPushToUser) runs on thenotificationsqueue with 2 tries and 30/120-second backoff.
Scheduler setup
One cron entry drives everything:
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1
Every task below runs inside Laravel's scheduler, so a single missed cron minute only delays tasks, and withoutOverlapping prevents pile-ups.
The complete schedule (routes/console.php)
All entries use ->withoutOverlapping() (10 minutes unless noted) and ->onFailure(...) logging a structured schedule task failed: <name> error.
| # | Task | Cadence | Purpose |
|---|---|---|---|
| 1 | CheckImapMailboxes |
every 5 min | Poll connected mailboxes for inbound email |
| 2 | CheckNoActivityAutomations |
hourly | Fire no-activity automations |
| 3 | CheckAppointmentApproaching |
every 5 min | WhatsApp appointment reminders inside each tenant's hours_before window (dedupe key prevents repeats) |
| 4 | CheckFollowUpDue |
every 5 min | Fire follow_up_due automations for overdue lead tasks |
| 5 | CheckDateFieldAutomations |
daily 07:00 | Birthday/anniversary greetings from date custom fields |
| 6 | ProcessEmailSequences |
every 15 min | Advance email sequence enrollments |
| 7 | ProcessAiSdrEnrollments |
every 15 min | One AI SDR decision/action per due enrollment (no-op in demo mode) |
| 8 | notifications:digest |
hourly | Send notification digests |
| 9 | tasks:send-reminders |
every 5 min | Notify assignees of due tasks (separate from #4, which drives automations) |
| 10 | leadhub:meta-health |
every 6 h | Ping stored Meta page tokens so "connected but expired" surfaces early |
| 11 | leadhub:sync-meta-ads --days=30 |
daily 04:30 | Pull campaign/ad/insights data; skips tenants without credentials |
| 12 | leadhub:purge-recordings |
daily 03:15 | 30-day recording retention (files deleted, metadata row marked expired) |
| 13 | leadhub:meta-ops-scan |
every 30 min | Needs-attention scan (no delivery / zero leads / CPL spikes / paused / dead connections) |
| 14 | leadhub:meta-ops-brief |
daily 07:15 | Daily internal ads brief, after the nightly sync |
| 15 | reports:deliver |
hourly | Deliver scheduled reports |
| 16 | filters:send-alerts |
hourly | Saved-filter alert emails |
| 17 | leadhub:subscription-lifecycle |
daily 06:00 | Commercial subscription lifecycle transitions |
| 18 | invoices:process-recurring |
daily 07:00 | Generate recurring invoices; due-date reminders; mark overdue |
| 19 | leadhub:capture-benchmarks |
daily 03:30 | Capture metric benchmarks |
| 20 | leadhub:sync-calendars |
every 15 min | Calendar sync |
| 21 | leadhub:purge-audit-logs |
daily 03:00 | Audit 180 d / login attempts 30 d / processed billing events 90 d (env-overridable) |
| 22 | leadhub:execute-erasure |
daily 04:00 | GDPR Art. 17 hard-delete after the 30-day cool-off |
| 23 | leadhub:backup |
daily 02:00 | Nightly backup — guarded by GeneralSettings::auto_nightly_backup (or leadhub.backups.auto_nightly) |
| 24 | CreateRenewalInvoice |
daily 06:30 | Create renewal invoices for active schedules |
| 25 | CheckUpcomingRenewals |
daily 07:00 | Renewal reminders |
| 26 | CheckPaymentFailures |
hourly | Detect and react to failed payments |
| 27 | ApplyGracePeriod |
daily 07:15 | Start grace period (failure ladder step 1) |
| 28 | ApplyRestriction |
daily 07:30 | Apply restriction (failure ladder step 2) |
| 29 | ReconcileProviderStatus |
every 30 min | Reconcile subscription state with the payment provider |
| 30 | SyncAppliedChangeRequests |
every 30 min | Apply approved billing change requests |
| 31 | DetectStuckOnboarding |
daily 08:15 | Flag client onboarding steps unsettled > 24 h, notify SA (once/day/step) |
No phone-home The former daily update check and nightly license re-check were removed. Nothing in this scheduler makes outbound calls to the vendor's servers.
Failure handling
->withoutOverlapping(10)means a stuck run releases the slot after 10 minutes (production-critical syncs use 20–30).->onFailure($logFailure('Name'))writesschedule task failed: Nameto the log, which is greppable for alerting — a generic "Exception in cron" no longer hides which task broke.- Long-running data work (Meta sync, recording purge, Meta ops) uses larger overlap windows deliberately.
Related guides
- Architecture overview
- Storage & file handling — retention commands in depth
- Cron & queue workers (deployment)
- Daily checks (operations)
Was this page helpful?
Thanks — noted. ✦