Skip to main content

Overview

The Scheduler provides enterprise-grade distributed job scheduling with:
  • Cron Expressions - Standard cron syntax with timezone support
  • Distributed Architecture - Admin + Workers pattern for high availability
  • Multiple Storage Backends - Redis, PostgreSQL, MySQL, SQLite, In-Memory
  • Web UI - Visual schedule management and monitoring
  • Type Safety - Fully typed schedule payloads
Scheduler requires storage that supports auto-scheduling (InMemoryJobStorage, RedisJobStorage, or SQLJobStorage).

Installation

npm install go-go-scope @go-go-scope/scheduler

# Optional: Storage adapters
npm install @go-go-scope/persistence-redis
npm install @go-go-scope/persistence-postgres

Basic Usage

Create a scheduler, define schedules, and register handlers:
import { Scheduler, InMemoryJobStorage, CronPresets } from '@go-go-scope/scheduler';

// Create scheduler with in-memory storage
const scheduler = new Scheduler({
  storage: new InMemoryJobStorage(),
});

// Create a schedule
await scheduler.createSchedule('daily-report', {
  cron: CronPresets.DAILY_AT_MIDNIGHT,
  timezone: 'America/New_York',
  enabled: true,
});

// Register handler
scheduler.onSchedule('daily-report', async (job, scope) => {
  console.log('Generating daily report...');
  await generateReport();
});

// Start scheduler
scheduler.start();

Cron Expressions

Syntax

Cron expressions use 5 fields: minute hour day month dayOfWeek
┌───────── minute (0 - 59)
│ ┌───────── hour (0 - 23)
│ │ ┌───────── day of month (1 - 31)
│ │ │ ┌───────── month (1 - 12)
│ │ │ │ ┌───────── day of week (0 - 6, 0 = Sunday)
│ │ │ │ │
* * * * *

Presets

import { CronPresets } from '@go-go-scope/scheduler';

CronPresets.EVERY_MINUTE           // "* * * * *"
CronPresets.EVERY_5_MINUTES        // "*/5 * * * *"
CronPresets.EVERY_HOUR             // "0 * * * *"
CronPresets.EVERY_DAY_AT_MIDNIGHT  // "0 0 * * *"
CronPresets.EVERY_WEEK_ON_MONDAY   // "0 0 * * 1"
CronPresets.EVERY_MONTH            // "0 0 1 * *"
CronPresets.WORKDAY_MORNING        // "0 9 * * 1-5" (9 AM, Mon-Fri)
CronPresets.WEEKEND_AFTERNOON      // "0 14 * * 0,6" (2 PM, Sat-Sun)

Custom Expressions

await scheduler.createSchedule('frequent-sync', {
  cron: '*/15 * * * *',  // Every 15 minutes
});

Timezone Support

Schedules run in specific timezones using IANA timezone identifiers:
await scheduler.createSchedule('tokyo-task', {
  cron: '0 9 * * *',  // 9 AM
  timezone: 'Asia/Tokyo',  // In Tokyo time
});

await scheduler.createSchedule('ny-task', {
  cron: '0 9 * * *',  // 9 AM
  timezone: 'America/New_York',  // In New York time
});
Use timezone support for global applications to ensure schedules run at the correct local time.

Type-Safe Schedules

Define typed payload schemas for compile-time safety:
import { Scheduler } from '@go-go-scope/scheduler';

// Define schedule types
type AppSchedules = {
  'send-email': {
    to: string;
    subject: string;
    body: string;
  };
  'process-payment': {
    amount: number;
    currency: string;
    userId: string;
  };
  'generate-report': {
    reportType: 'daily' | 'weekly' | 'monthly';
    recipients: string[];
  };
};

// Create typed scheduler
const scheduler = new Scheduler<AppSchedules>({ storage });

// TypeScript enforces payload types
scheduler.onSchedule('send-email', async (job) => {
  // job.payload is typed as { to: string; subject: string; body: string }
  const { to, subject, body } = job.payload;
  await sendEmail({ to, subject, body });
});

// Trigger with type checking
await scheduler.triggerSchedule('send-email', {
  to: '[email protected]',
  subject: 'Hello',
  body: 'World',
});

Storage Backends

In-Memory (Development)

import { InMemoryJobStorage } from '@go-go-scope/scheduler';

const storage = new InMemoryJobStorage();
const scheduler = new Scheduler({ storage });

Redis (Production)

import { RedisJobStorage } from '@go-go-scope/persistence-redis';
import { createClient } from 'redis';

const redis = createClient({ url: 'redis://localhost:6379' });
await redis.connect();

const storage = new RedisJobStorage({ client: redis });
const scheduler = new Scheduler({ storage });

PostgreSQL

import { SQLJobStorage } from '@go-go-scope/persistence-postgres';
import { Pool } from 'pg';

const pool = new Pool({
  host: 'localhost',
  database: 'myapp',
  user: 'postgres',
  password: 'password',
});

const storage = new SQLJobStorage({ pool, dialect: 'postgres' });
const scheduler = new Scheduler({ storage });

SQLite

import { SQLJobStorage } from '@go-go-scope/persistence-sqlite';
import Database from 'better-sqlite3';

const db = new Database('./scheduler.db');
const storage = new SQLJobStorage({ db, dialect: 'sqlite' });
const scheduler = new Scheduler({ storage });

Web UI

Enable the built-in web interface:
const scheduler = new Scheduler({
  storage,
  enableWebUI: true,  // Enable Web UI
  webUI: {
    port: 3000,
    path: '/scheduler',  // Access at http://localhost:3000/scheduler
  },
});

// Web UI provides:
// - Schedule list and status
// - Job history and logs
// - Manual job triggering
// - Schedule enable/disable
// - Real-time job monitoring
Scheduler Web UI

Schedule Management

Create Schedule

await scheduler.createSchedule('backup-db', {
  cron: '0 2 * * *',  // 2 AM daily
  timezone: 'UTC',
  enabled: true,
  maxRetries: 3,
  retryDelay: 60000,  // 1 minute
  timeout: 300000,    // 5 minutes
});

Update Schedule

await scheduler.updateSchedule('backup-db', {
  cron: '0 3 * * *',  // Change to 3 AM
  enabled: false,     // Disable schedule
});

Delete Schedule

await scheduler.deleteSchedule('backup-db');

List Schedules

const schedules = await scheduler.listSchedules();

for (const schedule of schedules) {
  console.log(`${schedule.name}: ${schedule.cron} (${schedule.enabled ? 'enabled' : 'disabled'})`);
}

Get Schedule Stats

const stats = await scheduler.getScheduleStats('backup-db');

console.log(`Total runs: ${stats.totalRuns}`);
console.log(`Successful: ${stats.successCount}`);
console.log(`Failed: ${stats.failureCount}`);
console.log(`Average duration: ${stats.averageDuration}ms`);

Job Handlers

Register Handler

scheduler.onSchedule('send-notifications', async (job, scope) => {
  const { logger, signal } = scope;
  
  logger.info('Processing notifications', { jobId: job.id });
  
  // Use scope for concurrent operations
  const results = await scope.parallel([
    async ({ signal }) => sendEmailNotifications(signal),
    async ({ signal }) => sendPushNotifications(signal),
  ]);
  
  logger.info('Notifications sent', { results });
});

Handler with Retries

import { exponentialBackoff } from 'go-go-scope';

scheduler.onSchedule(
  'unreliable-task',
  async (job, scope) => {
    // Task implementation
    await unreliableOperation();
  },
  {
    retry: {
      maxRetries: 5,
      delay: exponentialBackoff({ initial: 1000, max: 60000 }),
    },
    timeout: 30000,  // 30 second timeout
  }
);

Worker Pool Handler

Run CPU-intensive tasks in worker threads:
scheduler.onSchedule(
  'cpu-intensive',
  async (job) => {
    return computeHash(job.payload.data);
  },
  {
    worker: true,  // Run in worker thread
  }
);

Manual Triggering

Trigger schedules manually:
// Trigger immediately
await scheduler.triggerSchedule('send-email', {
  to: '[email protected]',
  subject: 'Manual trigger',
  body: 'Hello!',
});

// Schedule for later
await scheduler.scheduleJob('send-email', {
  scheduledAt: new Date(Date.now() + 3600000),  // 1 hour from now
  payload: {
    to: '[email protected]',
    subject: 'Scheduled',
    body: 'World',
  },
});

Events

Subscribe to scheduler events:
// Job started
scheduler.on('jobStarted', (job) => {
  console.log(`Job ${job.id} started`);
});

// Job completed
scheduler.on('jobCompleted', (job, result) => {
  console.log(`Job ${job.id} completed`);
});

// Job failed
scheduler.on('jobFailed', (job, error) => {
  console.error(`Job ${job.id} failed:`, error);
});

// Schedule enabled/disabled
scheduler.on('scheduleEnabled', (scheduleName) => {
  console.log(`Schedule ${scheduleName} enabled`);
});

scheduler.on('scheduleDisabled', (scheduleName) => {
  console.log(`Schedule ${scheduleName} disabled`);
});

High Availability

Run multiple scheduler instances with leader election:
const scheduler = new Scheduler({
  storage,
  enableLeaderElection: true,  // Enable HA mode
  leaderHeartbeatInterval: 5000,   // 5 second heartbeat
  leaderElectionTimeout: 15000,    // 15 second timeout
  onBecomeLeader: async () => {
    console.log('This instance is now the leader');
  },
});

// Only the leader schedules jobs
// Workers process jobs from the queue

Monitoring

Metrics

Enable metrics collection:
const scheduler = new Scheduler({
  storage,
  metrics: true,  // Enable metrics
});

// Get metrics
const metrics = await scheduler.getMetrics();

console.log(`Active jobs: ${metrics.activeJobs}`);
console.log(`Completed today: ${metrics.completedToday}`);
console.log(`Failed today: ${metrics.failedToday}`);
console.log(`Average duration: ${metrics.averageDuration}ms`);

Deadlock Detection

const scheduler = new Scheduler({
  storage,
  deadlockThreshold: 3600000,  // 1 hour
});

// Jobs running longer than 1 hour are flagged as potential deadlocks

Real-World Examples

Email Campaign Scheduler

type Schedules = {
  'email-campaign': {
    campaignId: string;
    recipients: string[];
  };
};

const scheduler = new Scheduler<Schedules>({ storage });

// Schedule campaign
await scheduler.createSchedule('email-campaign', {
  cron: '0 9 * * 1',  // Every Monday at 9 AM
  timezone: 'America/New_York',
});

// Handler
scheduler.onSchedule('email-campaign', async (job, scope) => {
  const { campaignId, recipients } = job.payload;
  
  // Send emails in parallel batches
  const batches = chunkArray(recipients, 100);
  
  await scope.parallel(
    batches.map(batch => async ({ signal }) => {
      for (const email of batch) {
        await sendEmail(campaignId, email, { signal });
      }
    }),
    { concurrency: 5 }
  );
});

Data Backup Scheduler

import { exponentialBackoff } from 'go-go-scope';

await scheduler.createSchedule('backup-database', {
  cron: '0 2 * * *',  // 2 AM daily
  timezone: 'UTC',
  maxRetries: 3,
});

scheduler.onSchedule(
  'backup-database',
  async (job, scope) => {
    const { logger } = scope;
    
    logger.info('Starting database backup');
    
    const backupPath = await createBackup();
    await uploadToS3(backupPath);
    await cleanupOldBackups();
    
    logger.info('Backup completed', { path: backupPath });
  },
  {
    retry: {
      maxRetries: 3,
      delay: exponentialBackoff({ initial: 60000 }),
    },
    timeout: 1800000,  // 30 minute timeout
  }
);

Best Practices

Use Type Safety

Define typed schedule payloads for compile-time safety

Set Timeouts

Always set reasonable timeouts for job handlers

Enable Retries

Use retries for transient failures

Monitor Metrics

Track job success rates and durations

Next Steps

Resilience Patterns

Add circuit breakers and retries to jobs

Channels

Use channels for job queuing

Build docs developers (and LLMs) love