Files
convex-monorepo/.claude/skills/convex-cron-jobs/SKILL.md
T

14 KiB

name, displayName, description, version, author, tags
name displayName description version author tags
convex-cron-jobs Convex Cron Jobs Scheduled function patterns for background tasks including interval scheduling, cron expressions, job monitoring, retry strategies, and best practices for long-running tasks 1.0.0 Convex
convex
cron
scheduling
background-jobs
automation

Convex Cron Jobs

Schedule recurring functions for background tasks, cleanup jobs, data syncing, and automated workflows in Convex applications.

Documentation Sources

Before implementing, do not assume; fetch the latest documentation:

Instructions

Cron Jobs Overview

Convex cron jobs allow you to schedule functions to run at regular intervals or specific times. Key features:

  • Run functions on a fixed schedule
  • Support for interval-based and cron expression scheduling
  • Automatic retries on failure
  • Monitoring via the Convex dashboard

Basic Cron Setup

// convex/crons.ts
import { cronJobs } from 'convex/server';

import { internal } from './_generated/api';

const crons = cronJobs();

// Run every hour
crons.interval(
  'cleanup expired sessions',
  { hours: 1 },
  internal.tasks.cleanupExpiredSessions,
  {},
);

// Run every day at midnight UTC
crons.cron(
  'daily report',
  '0 0 * * *',
  internal.reports.generateDailyReport,
  {},
);

export default crons;

Interval-Based Scheduling

Use crons.interval for simple recurring tasks:

// convex/crons.ts
import { cronJobs } from 'convex/server';

import { internal } from './_generated/api';

const crons = cronJobs();

// Every 5 minutes
crons.interval(
  'sync external data',
  { minutes: 5 },
  internal.sync.fetchExternalData,
  {},
);

// Every 2 hours
crons.interval(
  'cleanup temp files',
  { hours: 2 },
  internal.files.cleanupTempFiles,
  {},
);

// Every 30 seconds (minimum interval)
crons.interval(
  'health check',
  { seconds: 30 },
  internal.monitoring.healthCheck,
  {},
);

export default crons;

Cron Expression Scheduling

Use crons.cron for precise scheduling with cron expressions:

// convex/crons.ts
import { cronJobs } from 'convex/server';

import { internal } from './_generated/api';

const crons = cronJobs();

// Every day at 9 AM UTC
crons.cron(
  'morning notifications',
  '0 9 * * *',
  internal.notifications.sendMorningDigest,
  {},
);

// Every Monday at 8 AM UTC
crons.cron(
  'weekly summary',
  '0 8 * * 1',
  internal.reports.generateWeeklySummary,
  {},
);

// First day of every month at midnight
crons.cron(
  'monthly billing',
  '0 0 1 * *',
  internal.billing.processMonthlyBilling,
  {},
);

// Every 15 minutes
crons.cron('frequent sync', '*/15 * * * *', internal.sync.syncData, {});

export default crons;

Cron Expression Reference

┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)
│ │ │ │ │
* * * * *

Common patterns:

  • * * * * * - Every minute
  • 0 * * * * - Every hour
  • 0 0 * * * - Every day at midnight
  • 0 0 * * 0 - Every Sunday at midnight
  • 0 0 1 * * - First day of every month
  • */5 * * * * - Every 5 minutes
  • 0 9-17 * * 1-5 - Every hour from 9 AM to 5 PM, Monday through Friday

Internal Functions for Crons

Cron jobs should call internal functions for security:

// convex/tasks.ts
import { v } from 'convex/values';

import { internalMutation, internalQuery } from './_generated/server';

// Cleanup expired sessions
export const cleanupExpiredSessions = internalMutation({
  args: {},
  returns: v.number(),
  handler: async (ctx) => {
    const oneHourAgo = Date.now() - 60 * 60 * 1000;

    const expiredSessions = await ctx.db
      .query('sessions')
      .withIndex('by_lastActive')
      .filter((q) => q.lt(q.field('lastActive'), oneHourAgo))
      .collect();

    for (const session of expiredSessions) {
      await ctx.db.delete(session._id);
    }

    return expiredSessions.length;
  },
});

// Process pending tasks
export const processPendingTasks = internalMutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    const pendingTasks = await ctx.db
      .query('tasks')
      .withIndex('by_status', (q) => q.eq('status', 'pending'))
      .take(100);

    for (const task of pendingTasks) {
      await ctx.db.patch(task._id, {
        status: 'processing',
        startedAt: Date.now(),
      });

      // Schedule the actual processing
      await ctx.scheduler.runAfter(0, internal.tasks.processTask, {
        taskId: task._id,
      });
    }

    return null;
  },
});

Cron Jobs with Arguments

Pass static arguments to cron jobs:

// convex/crons.ts
import { cronJobs } from 'convex/server';

import { internal } from './_generated/api';

const crons = cronJobs();

// Different cleanup intervals for different types
crons.interval(
  'cleanup temp files',
  { hours: 1 },
  internal.cleanup.cleanupByType,
  { fileType: 'temp', maxAge: 3600000 },
);

crons.interval(
  'cleanup cache files',
  { hours: 24 },
  internal.cleanup.cleanupByType,
  { fileType: 'cache', maxAge: 86400000 },
);

export default crons;
// convex/cleanup.ts
import { v } from 'convex/values';

import { internalMutation } from './_generated/server';

export const cleanupByType = internalMutation({
  args: {
    fileType: v.string(),
    maxAge: v.number(),
  },
  returns: v.number(),
  handler: async (ctx, args) => {
    const cutoff = Date.now() - args.maxAge;

    const oldFiles = await ctx.db
      .query('files')
      .withIndex('by_type_and_created', (q) =>
        q.eq('type', args.fileType).lt('createdAt', cutoff),
      )
      .collect();

    for (const file of oldFiles) {
      await ctx.storage.delete(file.storageId);
      await ctx.db.delete(file._id);
    }

    return oldFiles.length;
  },
});

Monitoring and Logging

Add logging to track cron job execution:

// convex/tasks.ts
import { v } from 'convex/values';

import { internalMutation } from './_generated/server';

export const cleanupWithLogging = internalMutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    const startTime = Date.now();
    let processedCount = 0;
    let errorCount = 0;

    try {
      const expiredItems = await ctx.db
        .query('items')
        .withIndex('by_expiresAt')
        .filter((q) => q.lt(q.field('expiresAt'), Date.now()))
        .collect();

      for (const item of expiredItems) {
        try {
          await ctx.db.delete(item._id);
          processedCount++;
        } catch (error) {
          errorCount++;
          console.error(`Failed to delete item ${item._id}:`, error);
        }
      }

      // Log job completion
      await ctx.db.insert('cronLogs', {
        jobName: 'cleanup',
        startTime,
        endTime: Date.now(),
        duration: Date.now() - startTime,
        processedCount,
        errorCount,
        status: errorCount === 0 ? 'success' : 'partial',
      });
    } catch (error) {
      // Log job failure
      await ctx.db.insert('cronLogs', {
        jobName: 'cleanup',
        startTime,
        endTime: Date.now(),
        duration: Date.now() - startTime,
        processedCount,
        errorCount,
        status: 'failed',
        error: String(error),
      });
      throw error;
    }

    return null;
  },
});

Batching for Large Datasets

Handle large datasets in batches to avoid timeouts:

// convex/tasks.ts
import { v } from 'convex/values';

import { internal } from './_generated/api';
import { internalMutation } from './_generated/server';

const BATCH_SIZE = 100;

export const processBatch = internalMutation({
  args: {
    cursor: v.optional(v.string()),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    const result = await ctx.db
      .query('items')
      .withIndex('by_status', (q) => q.eq('status', 'pending'))
      .paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });

    for (const item of result.page) {
      await ctx.db.patch(item._id, {
        status: 'processed',
        processedAt: Date.now(),
      });
    }

    // Schedule next batch if there are more items
    if (!result.isDone) {
      await ctx.scheduler.runAfter(0, internal.tasks.processBatch, {
        cursor: result.continueCursor,
      });
    }

    return null;
  },
});

External API Calls in Crons

Use actions for external API calls:

// convex/sync.ts
'use node';

import { v } from 'convex/values';

import { internal } from './_generated/api';
import { internalAction } from './_generated/server';

export const syncExternalData = internalAction({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    // Fetch from external API
    const response = await fetch('https://api.example.com/data', {
      headers: {
        Authorization: `Bearer ${process.env.API_KEY}`,
      },
    });

    if (!response.ok) {
      throw new Error(`API request failed: ${response.status}`);
    }

    const data = await response.json();

    // Store the data using a mutation
    await ctx.runMutation(internal.sync.storeExternalData, {
      data,
      syncedAt: Date.now(),
    });

    return null;
  },
});

export const storeExternalData = internalMutation({
  args: {
    data: v.any(),
    syncedAt: v.number(),
  },
  returns: v.null(),
  handler: async (ctx, args) => {
    await ctx.db.insert('externalData', {
      data: args.data,
      syncedAt: args.syncedAt,
    });
    return null;
  },
});
// convex/crons.ts
import { cronJobs } from 'convex/server';

import { internal } from './_generated/api';

const crons = cronJobs();

crons.interval(
  'sync external data',
  { minutes: 15 },
  internal.sync.syncExternalData,
  {},
);

export default crons;

Examples

Schema for Cron Job Logging

// convex/schema.ts
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';

export default defineSchema({
  cronLogs: defineTable({
    jobName: v.string(),
    startTime: v.number(),
    endTime: v.number(),
    duration: v.number(),
    processedCount: v.number(),
    errorCount: v.number(),
    status: v.union(
      v.literal('success'),
      v.literal('partial'),
      v.literal('failed'),
    ),
    error: v.optional(v.string()),
  })
    .index('by_job', ['jobName'])
    .index('by_status', ['status'])
    .index('by_startTime', ['startTime']),

  sessions: defineTable({
    userId: v.id('users'),
    token: v.string(),
    lastActive: v.number(),
    expiresAt: v.number(),
  })
    .index('by_user', ['userId'])
    .index('by_lastActive', ['lastActive'])
    .index('by_expiresAt', ['expiresAt']),

  tasks: defineTable({
    type: v.string(),
    status: v.union(
      v.literal('pending'),
      v.literal('processing'),
      v.literal('completed'),
      v.literal('failed'),
    ),
    data: v.any(),
    createdAt: v.number(),
    startedAt: v.optional(v.number()),
    completedAt: v.optional(v.number()),
  })
    .index('by_status', ['status'])
    .index('by_type_and_status', ['type', 'status']),
});

Complete Cron Configuration Example

// convex/crons.ts
import { cronJobs } from 'convex/server';

import { internal } from './_generated/api';

const crons = cronJobs();

// Cleanup jobs
crons.interval(
  'cleanup expired sessions',
  { hours: 1 },
  internal.cleanup.expiredSessions,
  {},
);

crons.interval('cleanup old logs', { hours: 24 }, internal.cleanup.oldLogs, {
  maxAgeDays: 30,
});

// Sync jobs
crons.interval('sync user data', { minutes: 15 }, internal.sync.userData, {});

// Report jobs
crons.cron('daily analytics', '0 1 * * *', internal.reports.dailyAnalytics, {});

crons.cron('weekly summary', '0 9 * * 1', internal.reports.weeklySummary, {});

// Health checks
crons.interval(
  'service health check',
  { minutes: 5 },
  internal.monitoring.healthCheck,
  {},
);

export default crons;

Best Practices

  • Never run npx convex deploy unless explicitly instructed
  • Never run any git commands unless explicitly instructed
  • Only use crons.interval or crons.cron methods, not deprecated helpers
  • Always call internal functions from cron jobs for security
  • Import internal from _generated/api even for functions in the same file
  • Add logging and monitoring for production cron jobs
  • Use batching for operations that process large datasets
  • Handle errors gracefully to prevent job failures
  • Use meaningful job names for dashboard visibility
  • Consider timezone when using cron expressions (Convex uses UTC)

Common Pitfalls

  1. Using public functions - Cron jobs should call internal functions only
  2. Long-running mutations - Break large operations into batches
  3. Missing error handling - Unhandled errors will fail the entire job
  4. Forgetting timezone - All cron expressions use UTC
  5. Using deprecated helpers - Avoid crons.hourly, crons.daily, etc.
  6. Not logging execution - Makes debugging production issues difficult

References