Zodiac Tech CRM Docs

Cron & queue workers

The one cron entry that runs everything — the full schedule table, queue configuration for shared and VPS hosting, and the cron-based worker pattern.

super-admin developer support Medium ⏱ 15 min Updated 2026-10-05

Almost everything time-driven in Zodiac Tech CRM — reminders, WhatsApp appointment sweeps, Meta syncs, billing lifecycle, backups, e-mail sequences — rides on one cron entry hitting Laravel's scheduler every minute. On production installs the queue should be database, drained by the same cron (or a supervised worker on VPS).

Who this is for

  • Super-admin / hosting admin — wiring the cron line.
  • Developer — choosing queue strategy per host.
  • Support — "e-mails/reminders aren't sending" almost always ends here.

Prerequisites: the app is installed and reachable.

Step 1 — Add the cron entry

Standard (Terminal/SSH available or cPanel Cron Jobs):

* * * * * cd /home/YOURUSER/crm-app && php artisan schedule:run >> /dev/null 2>&1

Shared hosting without a usable PHP binary path: use cPanel's shown PHP binary:

* * * * * /usr/local/bin/ea-php84 /home/YOURUSER/crm-app/artisan schedule:run >> /dev/null 2>&1

No SSH and no artisan cron available — the shipped fallback runner:

* * * * * php /home/YOURUSER/public_html/crm/cron.php

cron.php drains queued jobs (up to 30 per run, within a ~55-second budget) across all named queues, then runs schedule:run — a complete replacement for both the worker and the scheduler. It requires storage/installed.lock, and if you open it in a browser it refuses to work unless you set a CRON_SECRET first.

Expected result: within two minutes, storage/logs/laravel.log starts showing scheduled task activity, and queued e-mails/jobs stop accumulating in the jobs table.

Step 2 — Configure the queue

QUEUE_CONNECTION Behaviour When to use
sync Jobs run inside the web request Local dev / last resort (slower pages; no cron needed)
database (recommended) Jobs stored in MySQL, drained by a worker Shared hosting and VPS
redis Fastest; needs Redis + a worker VPS/dedicated with Redis available

The shipped .env.example already sets QUEUE_CONNECTION=database. Jobs are partitioned into named queues — default, webhooks, automations, integrations, notifications, exports, ai-sdr, mail — so heavy work (exports, AI) cannot delay time-sensitive work (webhooks, mail).

Step 3 — Drain the queue

Shared hosting (recommended): add a second cron entry that runs a finite worker every minute:

* * * * * cd /home/YOURUSER/crm-app && php artisan queue:work --queue=default,webhooks,automations,integrations,notifications,exports,ai-sdr,mail --stop-when-empty --max-time=55 >> /dev/null 2>&1

--stop-when-empty exits when there is nothing to do; --max-time=55 guarantees the run ends before the next minute's cron fires. Combined with the scheduler entry from Step 1, this needs no long-running process. (The cron.php fallback already does this for you.)

VPS / dedicated (supervised worker): run a persistent worker with a process supervisor, and list the named queues explicitly:

php artisan queue:work --queue=default,webhooks,automations,integrations,notifications,exports,ai-sdr,mail --tries=3 --timeout=120

Workers that don't list queues queue:work defaults to the default queue only. A worker started without --queue=… will silently ignore webhooks, mail, exports and AI jobs.

After every deploy, restart workers so they pick up new code:

php artisan queue:restart

The full schedule

Everything below is registered in routes/console.php. Every task runs with withoutOverlapping(10) (a stuck run never blocks the slot for a day) and logs a structured warning — schedule task failed: <name> — if it throws.

Time / frequency Task What it does
Every 5 min CheckImapMailboxes Pulls new mail from configured IMAP mailboxes into the inbox
Every 5 min CheckAppointmentApproaching Fires appointment_approaching WhatsApp automations inside the configured lead time
Every 5 min CheckFollowUpDue Fires follow_up_due automations for overdue tasks
Every 5 min tasks:send-reminders Notifies assignees about due follow-ups
Every 15 min ProcessEmailSequences Sends the next e-mail in active sequences
Every 15 min ProcessAiSdrEnrollments One AI decision/action per due enrollment
Every 15 min leadhub:sync-calendars Two-way calendar sync (Google/Outlook)
Every 30 min leadhub:meta-ops-scan Meta ads needs-attention scan (no delivery, CPL spikes, dead connections)
Every 30 min ReconcileProviderStatus Re-checks payment provider status as a webhook backstop
Every 30 min SyncAppliedChangeRequests Applies approved commercial change requests
Hourly CheckNoActivityAutomations Fires automations for leads with no activity
Hourly notifications:digest Batches notification digests
Hourly reports:deliver Sends scheduled reports
Hourly filters:send-alerts Saved-filter alerts
Hourly CheckPaymentFailures Detects failed payments and starts the failure lifecycle
Every 6 h leadhub:meta-health Pings stored Meta page tokens so "connected but expired" surfaces early
Daily 02:00 leadhub:backup Nightly backup — only if the toggle is on (see Backups & restore)
Daily 03:00 leadhub:purge-audit-logs Retention: 180-day audit logs, 30-day login attempts, 90-day billing events
Daily 03:15 leadhub:purge-recordings 30-day call-recording retention
Daily 03:30 leadhub:capture-benchmarks Captures industry benchmarks for reporting
Daily 04:00 leadhub:execute-erasure GDPR Article 17 deletions whose cool-off elapsed
Daily 04:30 leadhub:sync-meta-ads --days=30 Pulls campaigns, ads, insights
Daily 06:00 leadhub:subscription-lifecycle Subscription state transitions
Daily 06:30 CreateRenewalInvoice Renewal invoices for due agreements
Daily 07:00 CheckUpcomingRenewals Reminds clients of upcoming renewals
Daily 07:00 CheckDateFieldAutomations Birthday/anniversary greetings
Daily 07:15 ApplyGracePeriod Starts grace for failed renewals
Daily 07:15 leadhub:meta-ops-brief Daily ads brief for the agency team
Daily 07:30 ApplyRestriction Escalates overdue accounts to RESTRICTED
Daily 08:15 DetectStuckOnboarding Nudges clients stuck in guided onboarding

Pruning happens in-task Retention windows are enforced by the purge tasks above; override them with LH_AUDIT_RETENTION_DAYS, LH_LOGIN_ATTEMPT_RETENTION_DAYS and LH_BILLING_EVENT_RETENTION_DAYS — see Environment variables.

Failed jobs

With database queues, failures land in the failed_jobs table:

php artisan queue:failed          # list
php artisan queue:retry all       # retry everything
php artisan queue:failed-forget all  # discard

Webhook deliveries and automations are designed to be retry-safe (idempotent), so queue:retry all is normally safe. Investigate recurring failures via storage/logs/laravel.log.

Common mistakes

  • Adding the schedule entry but not the worker drain (with database) — scheduled dispatches work but queued jobs never run.
  • Two queue:work processes on the same queue without --stop-when-empty supervision — duplicated processing on non-idempotent jobs.
  • Forgetting that cron runs as a different user/php.ini — test the command manually and check storage/logs.
  • Using sync in production "because it works" — page loads get slow and a mail timeout blocks the request.
  • Not running queue:restart after deploys (supervised workers keep old code).

Troubleshooting

Symptom Check
Nothing scheduled ever runs Cron entry syntax; PHP binary path; cd into the app directory; log file
Jobs table grows forever Worker not running (or queue:work missing --queue= for named queues)
Reminders not sending / one queue backed up Schedule a check: is tasks:send-reminders running? Is mail queue processed?
"schedule task failed: X" warnings The named task threw — inspect laravel.log around that timestamp
Duplicate WhatsApp sends Overlap guard bypassed by a manual worker run — stop stray workers

Technical details

  • Schedule definition: routes/console.php (all tasks; withoutOverlapping(10) + onFailure logging).
  • Shared-host runner: root cron.php (30 jobs/55 s across default,webhooks,automations,integrations,notifications,exports,ai-sdr,mail, then schedule:run).
  • Nightly backup gate: App\Settings\GeneralSettings->auto_nightly_backup or config('leadhub.backups.auto_nightly').
  • Backup command: leadhub:backup (--prune=DAYS optional).

Was this page helpful?