Features

Task Scheduling

Run tasks on a schedule with cron-like syntax

Schedule recurring tasks to run automatically at specified times using cron expressions.

Quick Start

1. Define Scheduled Task

config/schedule.php:

return [
    'jobs' => [
        [
            'name' => 'cleanup_sessions',
            'schedule' => '0 0 * * *', // Daily at midnight
            'handler_class' => 'App\\Jobs\\CleanupSessionsJob',
            'description' => 'Clean up expired sessions',
            'enabled' => true,
        ],
    ],
];

2. Run Scheduler

# Run all due jobs once
php glueful queue:scheduler run

# Run continuously (daemon mode)
php glueful queue:scheduler work --interval=60

Cron Expressions

Standard 5-part cron syntax:

*  *  *  *  *
│  │  │  │  │
│  │  │  │  └─ Day of week (0-7, Sun=0/7)
│  │  │  └──── Month (1-12)
│  │  └─────── Day of month (1-31)
│  └────────── Hour (0-23)
└───────────── Minute (0-59)

Note: In Glueful’s scheduler, day of week is 0–6 (Sunday = 0).

Common Patterns

// Every minute
'schedule' => '* * * * *'

// Every 5 minutes
'schedule' => '*/5 * * * *'

// Every hour
'schedule' => '0 * * * *'

// Every hour at 30 minutes past
'schedule' => '30 * * * *'

// Daily at midnight
'schedule' => '0 0 * * *'

// Daily at 3:00 AM
'schedule' => '0 3 * * *'

// Weekdays at 9:00 AM
'schedule' => '0 9 * * 1-5'

// First day of month
'schedule' => '0 0 1 * *'

// Every Sunday at 2:00 AM
'schedule' => '0 2 * * 0'

Shortcut Macros

'schedule' => '@yearly'   // 0 0 1 1 *
'schedule' => '@monthly'  // 0 0 1 * *
'schedule' => '@weekly'   // 0 0 * * 0
'schedule' => '@daily'    // 0 0 * * *
'schedule' => '@hourly'   // 0 * * * *

Defining Jobs

Config-Based

config/schedule.php:

return [
    'jobs' => [
        [
            'name' => 'send_daily_digest',
            'schedule' => '@daily',
            'handler_class' => 'App\\Jobs\\SendDailyDigestJob',
            'parameters' => [],
            'description' => 'Send daily email digest to users',
            'enabled' => true,
            'timeout' => 300,
            'retry_attempts' => 3,
            // optional
            // 'queue' => 'scheduled',
            // 'persistence' => true, // persist for DB tracking/history
        ],

        [
            'name' => 'cleanup_temp_files',
            'schedule' => '0 */6 * * *', // Every 6 hours
            'handler_class' => 'App\\Jobs\\CleanupTempFilesJob',
            'description' => 'Delete temporary files older than 24 hours',
            'enabled' => true,
        ],

        [
            'name' => 'generate_reports',
            'schedule' => '0 1 * * *', // 1:00 AM daily
            'handler_class' => 'App\\Jobs\\GenerateReportsJob',
            'description' => 'Generate daily analytics reports',
            'enabled' => env('REPORTS_ENABLED', true),
        ],
    ],

    'settings' => [
        'enabled' => env('SCHEDULER_ENABLED', true),
        'max_concurrent_jobs' => env('MAX_CONCURRENT_JOBS', 5),
        'default_timeout' => env('DEFAULT_JOB_TIMEOUT', 300),
        // Additional options
        'default_queue' => env('DEFAULT_SCHEDULED_QUEUE', 'scheduled'),
        'queue_connection' => env('SCHEDULE_QUEUE_CONNECTION', 'default'),
        'use_queue_for_all_jobs' => env('USE_QUEUE_FOR_SCHEDULED_JOBS', true),
        'log_execution' => env('LOG_JOB_EXECUTION', true),
        'notification_on_failure' => env('NOTIFY_ON_JOB_FAILURE', env('APP_ENV') === 'production'),
    ],

    // Map named queues to job names (optional)
    'queue_mapping' => [
        'critical' => ['database_backup'],
        'maintenance' => ['session_cleaner', 'log_cleanup', 'cache_maintenance'],
        'notifications' => ['notification_retry_processor'],
        'system' => ['queue_maintenance'],
    ],
];

Persistence

If you add 'persistence' => true to a job in config/schedule.php, the scheduler persists it to the scheduled_jobs table and records executions in job_executions:

  • Jobs gain last_run/next_run tracking and show up in list, status, and history.
  • The scheduler invokes your handler class’s handle(array $params = []) method with the configured parameters.

Without persistence, jobs are registered in memory for the current process only (useful for quick, ephemeral schedules during development).

Programmatic

use Glueful\Scheduler\JobScheduler;

$scheduler = app(JobScheduler::class);

$scheduler->register('@hourly', function() {
    // Cleanup task
    $storage->cleanupOldFiles('temp/', 86400);
}, 'hourly-cleanup');

Job Handlers

Create job handler classes:

Note: When jobs are loaded from config/database, the scheduler invokes your handler as handle($params). Define your method as handle(array $params = ): void and read values from $params when needed.

namespace App\Jobs;

class CleanupSessionsJob
{
    public function handle(array $params = []): void
    {
        $cutoff = date('Y-m-d H:i:s', strtotime('-30 days'));

        db()->table('sessions')
            ->where('last_activity', '<', $cutoff)
            ->delete();

        logger()->info('Cleaned up expired sessions');
    }
}

Common Scheduled Tasks

Daily Cleanup

[
    'name' => 'daily_cleanup',
    'schedule' => '@daily',
    'handler_class' => 'App\\Jobs\\DailyCleanupJob',
],
class DailyCleanupJob
{
    public function handle(array $params = []): void
    {
        // Delete old sessions
        db()->table('sessions')
            ->where('created_at', '<', date('Y-m-d', strtotime('-7 days')))
            ->delete();

        // Delete old logs
        db()->table('activity_logs')
            ->where('created_at', '<', date('Y-m-d', strtotime('-90 days')))
            ->delete();

        // Delete temporary files
        Storage::cleanupOldFiles('temp/', 86400);
    }
}

Hourly Cache Warm

[
    'name' => 'warm_cache',
    'schedule' => '@hourly',
    'handler_class' => 'App\\Jobs\\WarmCacheJob',
],
class WarmCacheJob
{
    public function handle(array $params = []): void
    {
        // Warm popular product cache
        $products = db()->table('products')
            ->where('is_featured', true)
            ->get();

        foreach ($products as $product) {
            Cache::set('product:' . $product->uuid, $product, 3600);
        }
    }
}

Weekly Reports

[
    'name' => 'weekly_report',
    'schedule' => '0 9 * * 1', // Mondays at 9 AM
    'handler_class' => 'App\\Jobs\\WeeklyReportJob',
],
class WeeklyReportJob
{
    public function handle(array $params = []): void
    {
        $stats = [
            'users' => db()->table('users')->count(),
            'orders' => db()->table('orders')
                ->where('created_at', '>=', date('Y-m-d', strtotime('-7 days')))
                ->count(),
            'revenue' => db()->table('orders')
                ->where('created_at', '>=', date('Y-m-d', strtotime('-7 days')))
                ->sum('total'),
        ];

        // Email report
        Notifications::send(
            type: 'report.weekly',
            notifiable: $admin,
            subject: 'Weekly Report',
            data: ['stats' => $stats]
        );
    }
}

Nightly Backups

[
    'name' => 'backup_database',
    'schedule' => '0 2 * * *', // 2:00 AM daily
    'handler_class' => 'App\\Jobs\\BackupDatabaseJob',
],
class BackupDatabaseJob
{
    public function handle(array $params = []): void
    {
        $filename = 'backup-' . date('Y-m-d') . '.sql';

        exec("mysqldump -u{$user} -p{$pass} {$db} > {$path}/{$filename}");

        // Upload to S3
        Storage::disk('s3')->put("backups/{$filename}", file_get_contents("{$path}/{$filename}"));

        logger()->info('Database backup completed', ['file' => $filename]);
    }
}

Running Scheduler

One-Time Run

# Run all due jobs
php glueful queue:scheduler run

# Force run all jobs (ignore schedule)
php glueful queue:scheduler run --force

# Dry run (show what would run)
php glueful queue:scheduler run --dry-run

Daemon Mode

# Run continuously, check every 60 seconds
php glueful queue:scheduler work --interval=60

# Custom interval
php glueful queue:scheduler work --interval=30

System Cron

Add to system crontab to run scheduler every minute:

* * * * * cd /path/to/app && php glueful queue:scheduler run >> /dev/null 2>&1

Monitoring

List Jobs

php glueful queue:scheduler list

Check Status

php glueful queue:scheduler status

Health Check

php glueful queue:scheduler health

Production Setup

Supervisor

/etc/supervisor/conf.d/scheduler.conf:

[program:scheduler]
command=php /path/to/app/glueful queue:scheduler work --interval=60
autostart=true
autorestart=true
user=www-data
stdout_logfile=/var/log/scheduler.log

Reload supervisor:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start scheduler

Docker

docker-compose.yml:

services:
  scheduler:
    image: your-app
    command: php glueful queue:scheduler work --interval=60
    restart: always

Configuration

.env:

SCHEDULER_ENABLED=true
MAX_CONCURRENT_JOBS=5
DEFAULT_JOB_TIMEOUT=300

# Optional advanced settings
DEFAULT_SCHEDULED_QUEUE=scheduled
SCHEDULE_QUEUE_CONNECTION=default
USE_QUEUE_FOR_SCHEDULED_JOBS=true
LOG_JOB_EXECUTION=true
NOTIFY_ON_JOB_FAILURE=false

Best Practices

Keep Jobs Simple

// ✅ Good - delegates to job queue
class SendDigestJob
{
    public function handle(array $params = []): void
    {
        $users = db()->table('users')->where('digest_enabled', true)->get();

        $queue = service(\Glueful\Queue\QueueManager::class);
        foreach ($users as $user) {
            $queue->push(\App\Jobs\SendUserDigestJob::class, ['userId' => $user->id]);
        }
    }
}

// ❌ Bad - does everything inline
class SendDigestJob
{
    public function handle(array $params = []): void
    {
        // Heavy processing in scheduled job...
    }
}

Idempotent Tasks

// ✅ Good - safe to run multiple times
public function handle(): void
{
    $today = date('Y-m-d');

    if (Cache::has("report:generated:{$today}")) {
        return; // Already ran today
    }

    $this->generateReport();

    Cache::set("report:generated:{$today}", true, 86400);
}

Logging

public function handle(): void
{
    logger()->info('Starting daily cleanup');

    $deleted = db()->table('sessions')
        ->where('expires_at', '<', now())
        ->delete();

    logger()->info('Daily cleanup completed', [
        'sessions_deleted' => $deleted
    ]);
}

Troubleshooting

Jobs not running?

  • Verify scheduler is running (work command or cron)
  • Check cron expression is valid
  • Ensure job is enabled in config

Jobs running multiple times?

  • Check only one scheduler instance is running
  • Verify cron isn't triggering duplicates
  • Consider using distributed locks

Jobs timing out?

  • Increase timeout in job config
  • Break into smaller jobs
  • Queue heavy work instead

Next Steps