Skip to main content
Cron jobs allow you to schedule background tasks to run automatically at specified intervals. They’re perfect for maintenance tasks, data synchronization, sending scheduled emails, generating reports, and cleaning up old data.

Overview

Cron jobs in EverShop:
  • Run automatically on a schedule you define
  • Use cron syntax for flexible scheduling
  • Execute in the background without blocking the application
  • Are registered in bootstrap.ts during application startup
  • Support async/await for modern JavaScript patterns

How Cron Jobs Work

Application Starts → Bootstrap Registers Jobs → Jobs Run on Schedule
  1. Extension’s bootstrap.ts registers cron jobs
  2. Jobs are scheduled using cron expressions
  3. Jobs execute automatically at specified times
  4. Output is logged to console

Directory Structure

Cron jobs are organized in the crons folder:
extensions/[extension-name]/
├── src/
│   ├── crons/
│   │   ├── everyMinute.ts
│   │   ├── dailyCleanup.ts
│   │   └── weeklyReport.ts
│   └── bootstrap.ts
└── package.json

Creating Your First Cron Job

1
Create the Cron Function
2
Create a file in the crons directory:
3
export default function EveryMinute() {
  console.log('This cron job runs every minute');
}
4
The function name and file name can be anything descriptive.
5
Register in Bootstrap
6
Register the job in your extension’s bootstrap.ts:
7
import path from 'path';
import { registerJob } from '@evershop/evershop/lib/cronjob';

export default function () {
  registerJob({
    name: 'everyMinuteJob',
    schedule: '*/1 * * * *',  // Runs every minute
    resolve: path.resolve(import.meta.dirname, 'crons', 'everyMinute.js'),
    enabled: true
  });
}
8
Build and Run
9
Build your extension:
10
npm run build
npm run dev
11
The cron job will now run automatically based on the schedule.

Real-World Example

Here’s the actual example from the sample extension:

The Cron Function

extensions/sample/src/crons/everyMinute.ts
export default function EveryMinute() {
  console.log('This cron job runs every minute');
}

The Bootstrap Registration

extensions/sample/src/bootstrap.ts
import path from 'path';
import { registerJob } from '@evershop/evershop/lib/cronjob';

export default function () {
  registerJob({
    name: 'sampleJob',
    schedule: '*/1 * * * *', // Runs every minute
    resolve: path.resolve(import.meta.dirname, 'crons', 'everyMinute.js'),
    enabled: true
  });
}

Cron Schedule Syntax

Cron jobs use standard cron syntax with 5 fields:
┌───────────── minute (0-59)
│ ┌─────────── hour (0-23)
│ │ ┌───────── day of month (1-31)
│ │ │ ┌─────── month (1-12)
│ │ │ │ ┌───── day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *

Common Schedules

ScheduleExpressionDescription
Every minute* * * * *Runs every minute
Every 5 minutes*/5 * * * *Runs every 5 minutes
Every hour0 * * * *Runs at minute 0 of every hour
Every day at midnight0 0 * * *Runs at 00:00 every day
Every day at 3 AM0 3 * * *Runs at 03:00 every day
Every Monday at 9 AM0 9 * * 1Runs Mondays at 09:00
First day of month0 0 1 * *Runs at midnight on the 1st
Weekdays at 9 AM0 9 * * 1-5Runs Mon-Fri at 09:00
Every 15 minutes*/15 * * * *Runs every 15 minutes
Use crontab.guru to help build and understand cron expressions.

Advanced Examples

Daily Database Cleanup

src/crons/dailyCleanup.ts
import { pool } from '@evershop/evershop/lib/postgres/connection';

export default async function DailyCleanup() {
  console.log('Starting daily cleanup...');

  try {
    // Delete old sessions (older than 30 days)
    await pool.query(
      `DELETE FROM sessions WHERE updated_at < NOW() - INTERVAL '30 days'`
    );

    // Delete expired carts (older than 7 days)
    await pool.query(
      `DELETE FROM carts 
       WHERE status = 'abandoned' 
       AND updated_at < NOW() - INTERVAL '7 days'`
    );

    // Clean up old log entries
    await pool.query(
      `DELETE FROM audit_log WHERE created_at < NOW() - INTERVAL '90 days'`
    );

    console.log('Daily cleanup completed successfully');
  } catch (error) {
    console.error('Daily cleanup failed:', error);
  }
}
src/bootstrap.ts
registerJob({
  name: 'dailyCleanup',
  schedule: '0 3 * * *',  // 3 AM every day
  resolve: path.resolve(import.meta.dirname, 'crons', 'dailyCleanup.js'),
  enabled: true
});

Weekly Sales Report

src/crons/weeklySalesReport.ts
import { pool } from '@evershop/evershop/lib/postgres/connection';
import { sendEmail } from '../services/emailService';

export default async function WeeklySalesReport() {
  console.log('Generating weekly sales report...');

  try {
    // Get sales data for the past week
    const result = await pool.query(
      `SELECT 
         COUNT(*) as total_orders,
         SUM(grand_total) as total_revenue,
         AVG(grand_total) as average_order_value
       FROM orders
       WHERE created_at >= NOW() - INTERVAL '7 days'
       AND status = 'completed'`
    );

    const stats = result.rows[0];

    // Get top selling products
    const topProducts = await pool.query(
      `SELECT 
         p.name,
         SUM(oi.quantity) as quantity_sold,
         SUM(oi.total) as revenue
       FROM order_items oi
       JOIN products p ON oi.product_id = p.product_id
       JOIN orders o ON oi.order_id = o.order_id
       WHERE o.created_at >= NOW() - INTERVAL '7 days'
       AND o.status = 'completed'
       GROUP BY p.product_id, p.name
       ORDER BY quantity_sold DESC
       LIMIT 10`
    );

    // Send email report
    await sendEmail({
      to: 'admin@mystore.com',
      subject: 'Weekly Sales Report',
      template: 'weekly-report',
      data: {
        totalOrders: stats.total_orders,
        totalRevenue: stats.total_revenue,
        averageOrderValue: stats.average_order_value,
        topProducts: topProducts.rows
      }
    });

    console.log('Weekly sales report sent successfully');
  } catch (error) {
    console.error('Failed to generate weekly report:', error);
  }
}
src/bootstrap.ts
registerJob({
  name: 'weeklySalesReport',
  schedule: '0 9 * * 1',  // Every Monday at 9 AM
  resolve: path.resolve(import.meta.dirname, 'crons', 'weeklySalesReport.js'),
  enabled: true
});

Inventory Sync

src/crons/syncInventory.ts
import fetch from 'node-fetch';
import { pool } from '@evershop/evershop/lib/postgres/connection';

export default async function SyncInventory() {
  console.log('Syncing inventory with warehouse system...');

  try {
    // Fetch inventory data from external system
    const response = await fetch(
      `${process.env.WAREHOUSE_API_URL}/inventory`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.WAREHOUSE_API_KEY}`
        }
      }
    );

    if (!response.ok) {
      throw new Error(`Warehouse API returned ${response.status}`);
    }

    const inventoryData = await response.json();

    // Update inventory in database
    for (const item of inventoryData) {
      await pool.query(
        `UPDATE products 
         SET stock_quantity = $1, updated_at = NOW()
         WHERE sku = $2`,
        [item.quantity, item.sku]
      );
    }

    console.log(`Synced inventory for ${inventoryData.length} products`);
  } catch (error) {
    console.error('Inventory sync failed:', error);
  }
}
src/bootstrap.ts
registerJob({
  name: 'syncInventory',
  schedule: '*/30 * * * *',  // Every 30 minutes
  resolve: path.resolve(import.meta.dirname, 'crons', 'syncInventory.js'),
  enabled: true
});

Abandoned Cart Recovery

src/crons/abandonedCartReminder.ts
import { pool } from '@evershop/evershop/lib/postgres/connection';
import { sendEmail } from '../services/emailService';

export default async function AbandonedCartReminder() {
  console.log('Checking for abandoned carts...');

  try {
    // Find carts abandoned 24 hours ago
    const result = await pool.query(
      `SELECT 
         c.cart_id,
         c.customer_email,
         c.items,
         c.total
       FROM carts c
       WHERE c.status = 'active'
       AND c.updated_at BETWEEN NOW() - INTERVAL '25 hours' 
                           AND NOW() - INTERVAL '24 hours'
       AND c.customer_email IS NOT NULL
       AND NOT EXISTS (
         SELECT 1 FROM cart_recovery_emails 
         WHERE cart_id = c.cart_id
       )`
    );

    for (const cart of result.rows) {
      // Send recovery email
      await sendEmail({
        to: cart.customer_email,
        subject: 'You left items in your cart!',
        template: 'cart-recovery',
        data: {
          items: cart.items,
          total: cart.total,
          cartLink: `https://mystore.com/cart/${cart.cart_id}`
        }
      });

      // Log that we sent the email
      await pool.query(
        `INSERT INTO cart_recovery_emails (cart_id, sent_at) 
         VALUES ($1, NOW())`,
        [cart.cart_id]
      );
    }

    console.log(`Sent ${result.rows.length} cart recovery emails`);
  } catch (error) {
    console.error('Cart recovery failed:', error);
  }
}
src/bootstrap.ts
registerJob({
  name: 'abandonedCartReminder',
  schedule: '0 */6 * * *',  // Every 6 hours
  resolve: path.resolve(import.meta.dirname, 'crons', 'abandonedCartReminder.js'),
  enabled: true
});

Registration Options

The registerJob function accepts these options:
registerJob({
  name: 'myJob',              // Unique job name
  schedule: '*/5 * * * *',    // Cron expression
  resolve: path.resolve(...), // Path to job file
  enabled: true               // Enable/disable the job
});

Dynamic Enable/Disable

Use environment variables to control jobs:
registerJob({
  name: 'expensiveJob',
  schedule: '0 2 * * *',
  resolve: path.resolve(import.meta.dirname, 'crons', 'expensiveJob.js'),
  enabled: process.env.NODE_ENV === 'production'
});

Multiple Cron Jobs

Register multiple jobs in the same bootstrap:
src/bootstrap.ts
import path from 'path';
import { registerJob } from '@evershop/evershop/lib/cronjob';

export default function () {
  // Daily cleanup at 3 AM
  registerJob({
    name: 'dailyCleanup',
    schedule: '0 3 * * *',
    resolve: path.resolve(import.meta.dirname, 'crons', 'dailyCleanup.js'),
    enabled: true
  });

  // Hourly inventory sync
  registerJob({
    name: 'syncInventory',
    schedule: '0 * * * *',
    resolve: path.resolve(import.meta.dirname, 'crons', 'syncInventory.js'),
    enabled: true
  });

  // Weekly report on Mondays
  registerJob({
    name: 'weeklyReport',
    schedule: '0 9 * * 1',
    resolve: path.resolve(import.meta.dirname, 'crons', 'weeklyReport.js'),
    enabled: true
  });
}

Best Practices

Error Handling: Always wrap cron job logic in try-catch blocks to prevent crashes.
  • Logging - Log start, success, and failure of each job
  • Error Handling - Use try-catch to handle errors gracefully
  • Idempotency - Design jobs to be safe to run multiple times
  • Timeouts - Set reasonable timeouts for external API calls
  • Performance - Optimize queries and batch operations
  • Monitoring - Track job execution and failures
  • Testing - Test jobs in development before production

Testing Cron Jobs

Manual Execution

Create a test script to run jobs manually:
scripts/testCron.ts
import myJob from '../extensions/my-extension/src/crons/myJob';

// Run the job
await myJob();
console.log('Job completed');
Run it:
ts-node scripts/testCron.ts

Development Testing

Use a short interval for testing:
// For development
registerJob({
  name: 'testJob',
  schedule: '*/1 * * * *',  // Every minute
  resolve: path.resolve(import.meta.dirname, 'crons', 'testJob.js'),
  enabled: process.env.NODE_ENV === 'development'
});

Troubleshooting

Job Not Running

  1. Verify job is registered in bootstrap.ts
  2. Check enabled is set to true
  3. Verify cron expression syntax
  4. Check application is running: npm run dev
  5. Look for errors in console logs

Job Running Multiple Times

  1. Ensure job name is unique
  2. Check you’re not registering the same job twice
  3. Verify only one instance of the app is running

Job Errors

  1. Add try-catch blocks
  2. Check database connections
  3. Verify external API credentials
  4. Test the job function in isolation

Common Use Cases

Maintenance Tasks

  • Database cleanup
  • Log rotation
  • Cache invalidation
  • Session cleanup

Data Synchronization

  • Inventory updates
  • Price synchronization
  • Product imports
  • Order exports

Reporting

  • Daily sales reports
  • Weekly analytics
  • Monthly summaries
  • Performance metrics

Customer Engagement

  • Abandoned cart emails
  • Re-engagement campaigns
  • Birthday emails
  • Product recommendations

Next Steps

Event Subscribers

React to real-time events

Creating Extensions

Learn extension architecture