convex-cron-jobs

Schedule recurring background tasks with interval and cron expression patterns in Convex. Supports two scheduling modes: fixed intervals (seconds, minutes, hours) and precise cron expressions (UTC timezone) Cron jobs call internal functions for security and can accept static arguments for parameterized tasks Includes patterns for cleanup operations, data syncing, batch processing, and external API calls via actions Built-in logging and monitoring via the Convex dashboard; handle large datasets through pagination and batching to avoid timeouts

INSTALLATION
npx skills add https://github.com/waynesutton/convexskills --skill convex-cron-jobs
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2a

  • 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 { internalMutation, internalQuery } from "./_generated/server";

import { v } from "convex/values";

// 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 { internalMutation } from "./_generated/server";

import { v } from "convex/values";

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 { internalMutation } from "./_generated/server";

import { v } from "convex/values";

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 { internalMutation } from "./_generated/server";

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

import { v } from "convex/values";

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 { internalAction } from "./_generated/server";

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

import { v } from "convex/values";

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

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

References

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card