[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6X75F898H476GFFSSHFRMA8":3,"skill-same:01M6X75F898H476GFFSSHFRMA8":80},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":58,"commitSha":59,"skillDirectory":60,"forksCount":61,"repositoryHtmlUrl":62,"repositoryHomepageUrl":6,"ownerHtmlUrl":63,"ownerBlogUrl":64,"ownerDescription":65,"updatedAt":66,"install":67,"resources":79},"01M6X75F898H476GFFSSHFRMA8","convex-cron-jobs",null,"Scheduled function patterns for background tasks including interval scheduling, cron expressions, job monitoring, retry strategies, and best practices for long-running tasks","Cheggin\u002Frequest-for-startups","Cheggin","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F79483996?v=4","01M6X6MGT5512GJHE8DY5B1ZQJ","Reagan Hsu","01M6X6MGY03X7ZJNVRFM3R1040",1,"GPL-3.0","https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups\u002Fblob\u002Fmain\u002Fskills\u002Fconvex-cron-jobs\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"cron jobs","cron","convex cron jobs","jobs","convex","patterns","running","for","scheduled","true","retry","long","full","background tasks","strategies","monitoring","best practices","tasks","automation","security","2026-11-10 10:12:30",false,"---\nname: convex-cron-jobs\ndisplayName: Convex Cron Jobs\ndescription: Scheduled function patterns for background tasks including interval scheduling, cron expressions, job monitoring, retry strategies, and best practices for long-running tasks\nuser-invocable: true\nversion: 1.0.0\nauthor: Convex\ntags: [convex, cron, scheduling, background-jobs, automation]\ngroup: build\nprerequisites: [convex-functions]\nnext: [convex-security-check]\nworkflows: [full-startup]\n---\n\n# Convex Cron Jobs\n\nSchedule recurring functions for background tasks, cleanup jobs, data syncing, and automated workflows in Convex applications.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\u002Fcron-jobs\n- Scheduling Overview: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\n- Scheduled Functions: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\u002Fscheduled-functions\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### Cron Jobs Overview\n\nConvex cron jobs allow you to schedule functions to run at regular intervals or specific times. Key features:\n\n- Run functions on a fixed schedule\n- Support for interval-based and cron expression scheduling\n- Automatic retries on failure\n- Monitoring via the Convex dashboard\n\n### Basic Cron Setup\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Run every hour\ncrons.interval(\n  \"cleanup expired sessions\",\n  { hours: 1 },\n  internal.tasks.cleanupExpiredSessions,\n  {}\n);\n\n\u002F\u002F Run every day at midnight UTC\ncrons.cron(\n  \"daily report\",\n  \"0 0 * * *\",\n  internal.reports.generateDailyReport,\n  {}\n);\n\nexport default crons;\n```\n\n### Interval-Based Scheduling\n\nUse `crons.interval` for simple recurring tasks:\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Every 5 minutes\ncrons.interval(\n  \"sync external data\",\n  { minutes: 5 },\n  internal.sync.fetchExternalData,\n  {}\n);\n\n\u002F\u002F Every 2 hours\ncrons.interval(\n  \"cleanup temp files\",\n  { hours: 2 },\n  internal.files.cleanupTempFiles,\n  {}\n);\n\n\u002F\u002F Every 30 seconds (minimum interval)\ncrons.interval(\n  \"health check\",\n  { seconds: 30 },\n  internal.monitoring.healthCheck,\n  {}\n);\n\nexport default crons;\n```\n\n### Cron Expression Scheduling\n\nUse `crons.cron` for precise scheduling with cron expressions:\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Every day at 9 AM UTC\ncrons.cron(\n  \"morning notifications\",\n  \"0 9 * * *\",\n  internal.notifications.sendMorningDigest,\n  {}\n);\n\n\u002F\u002F Every Monday at 8 AM UTC\ncrons.cron(\n  \"weekly summary\",\n  \"0 8 * * 1\",\n  internal.reports.generateWeeklySummary,\n  {}\n);\n\n\u002F\u002F First day of every month at midnight\ncrons.cron(\n  \"monthly billing\",\n  \"0 0 1 * *\",\n  internal.billing.processMonthlyBilling,\n  {}\n);\n\n\u002F\u002F Every 15 minutes\ncrons.cron(\n  \"frequent sync\",\n  \"*\u002F15 * * * *\",\n  internal.sync.syncData,\n  {}\n);\n\nexport default crons;\n```\n\n### Cron Expression Reference\n\n```\n┌───────────── minute (0-59)\n│ ┌───────────── hour (0-23)\n│ │ ┌───────────── day of month (1-31)\n│ │ │ ┌───────────── month (1-12)\n│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)\n│ │ │ │ │\n* * * * *\n```\n\nCommon patterns:\n- `* * * * *` - Every minute\n- `0 * * * *` - Every hour\n- `0 0 * * *` - Every day at midnight\n- `0 0 * * 0` - Every Sunday at midnight\n- `0 0 1 * *` - First day of every month\n- `*\u002F5 * * * *` - Every 5 minutes\n- `0 9-17 * * 1-5` - Every hour from 9 AM to 5 PM, Monday through Friday\n\n### Internal Functions for Crons\n\nCron jobs should call internal functions for security:\n\n```typescript\n\u002F\u002F convex\u002Ftasks.ts\nimport { internalMutation, internalQuery } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\n\u002F\u002F Cleanup expired sessions\nexport const cleanupExpiredSessions = internalMutation({\n  args: {},\n  returns: v.number(),\n  handler: async (ctx) => {\n    const oneHourAgo = Date.now() - 60 * 60 * 1000;\n    \n    const expiredSessions = await ctx.db\n      .query(\"sessions\")\n      .withIndex(\"by_lastActive\")\n      .filter((q) => q.lt(q.field(\"lastActive\"), oneHourAgo))\n      .collect();\n\n    for (const session of expiredSessions) {\n      await ctx.db.delete(session._id);\n    }\n\n    return expiredSessions.length;\n  },\n});\n\n\u002F\u002F Process pending tasks\nexport const processPendingTasks = internalMutation({\n  args: {},\n  returns: v.null(),\n  handler: async (ctx) => {\n    const pendingTasks = await ctx.db\n      .query(\"tasks\")\n      .withIndex(\"by_status\", (q) => q.eq(\"status\", \"pending\"))\n      .take(100);\n\n    for (const task of pendingTasks) {\n      await ctx.db.patch(task._id, {\n        status: \"processing\",\n        startedAt: Date.now(),\n      });\n      \n      \u002F\u002F Schedule the actual processing\n      await ctx.scheduler.runAfter(0, internal.tasks.processTask, {\n        taskId: task._id,\n      });\n    }\n\n    return null;\n  },\n});\n```\n\n### Cron Jobs with Arguments\n\nPass static arguments to cron jobs:\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Different cleanup intervals for different types\ncrons.interval(\n  \"cleanup temp files\",\n  { hours: 1 },\n  internal.cleanup.cleanupByType,\n  { fileType: \"temp\", maxAge: 3600000 }\n);\n\ncrons.interval(\n  \"cleanup cache files\",\n  { hours: 24 },\n  internal.cleanup.cleanupByType,\n  { fileType: \"cache\", maxAge: 86400000 }\n);\n\nexport default crons;\n```\n\n```typescript\n\u002F\u002F convex\u002Fcleanup.ts\nimport { internalMutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport const cleanupByType = internalMutation({\n  args: {\n    fileType: v.string(),\n    maxAge: v.number(),\n  },\n  returns: v.number(),\n  handler: async (ctx, args) => {\n    const cutoff = Date.now() - args.maxAge;\n    \n    const oldFiles = await ctx.db\n      .query(\"files\")\n      .withIndex(\"by_type_and_created\", (q) => \n        q.eq(\"type\", args.fileType).lt(\"createdAt\", cutoff)\n      )\n      .collect();\n\n    for (const file of oldFiles) {\n      await ctx.storage.delete(file.storageId);\n      await ctx.db.delete(file._id);\n    }\n\n    return oldFiles.length;\n  },\n});\n```\n\n### Monitoring and Logging\n\nAdd logging to track cron job execution:\n\n```typescript\n\u002F\u002F convex\u002Ftasks.ts\nimport { internalMutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport const cleanupWithLogging = internalMutation({\n  args: {},\n  returns: v.null(),\n  handler: async (ctx) => {\n    const startTime = Date.now();\n    let processedCount = 0;\n    let errorCount = 0;\n\n    try {\n      const expiredItems = await ctx.db\n        .query(\"items\")\n        .withIndex(\"by_expiresAt\")\n        .filter((q) => q.lt(q.field(\"expiresAt\"), Date.now()))\n        .collect();\n\n      for (const item of expiredItems) {\n        try {\n          await ctx.db.delete(item._id);\n          processedCount++;\n        } catch (error) {\n          errorCount++;\n          console.error(`Failed to delete item ${item._id}:`, error);\n        }\n      }\n\n      \u002F\u002F Log job completion\n      await ctx.db.insert(\"cronLogs\", {\n        jobName: \"cleanup\",\n        startTime,\n        endTime: Date.now(),\n        duration: Date.now() - startTime,\n        processedCount,\n        errorCount,\n        status: errorCount === 0 ? \"success\" : \"partial\",\n      });\n    } catch (error) {\n      \u002F\u002F Log job failure\n      await ctx.db.insert(\"cronLogs\", {\n        jobName: \"cleanup\",\n        startTime,\n        endTime: Date.now(),\n        duration: Date.now() - startTime,\n        processedCount,\n        errorCount,\n        status: \"failed\",\n        error: String(error),\n      });\n      throw error;\n    }\n\n    return null;\n  },\n});\n```\n\n### Batching for Large Datasets\n\nHandle large datasets in batches to avoid timeouts:\n\n```typescript\n\u002F\u002F convex\u002Ftasks.ts\nimport { internalMutation } from \".\u002F_generated\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\nimport { v } from \"convex\u002Fvalues\";\n\nconst BATCH_SIZE = 100;\n\nexport const processBatch = internalMutation({\n  args: {\n    cursor: v.optional(v.string()),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const result = await ctx.db\n      .query(\"items\")\n      .withIndex(\"by_status\", (q) => q.eq(\"status\", \"pending\"))\n      .paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });\n\n    for (const item of result.page) {\n      await ctx.db.patch(item._id, {\n        status: \"processed\",\n        processedAt: Date.now(),\n      });\n    }\n\n    \u002F\u002F Schedule next batch if there are more items\n    if (!result.isDone) {\n      await ctx.scheduler.runAfter(0, internal.tasks.processBatch, {\n        cursor: result.continueCursor,\n      });\n    }\n\n    return null;\n  },\n});\n```\n\n### External API Calls in Crons\n\nUse actions for external API calls:\n\n```typescript\n\u002F\u002F convex\u002Fsync.ts\n\"use node\";\n\nimport { internalAction } from \".\u002F_generated\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport const syncExternalData = internalAction({\n  args: {},\n  returns: v.null(),\n  handler: async (ctx) => {\n    \u002F\u002F Fetch from external API\n    const response = await fetch(\"https:\u002F\u002Fapi.example.com\u002Fdata\", {\n      headers: {\n        Authorization: `Bearer ${process.env.API_KEY}`,\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`API request failed: ${response.status}`);\n    }\n\n    const data = await response.json();\n\n    \u002F\u002F Store the data using a mutation\n    await ctx.runMutation(internal.sync.storeExternalData, {\n      data,\n      syncedAt: Date.now(),\n    });\n\n    return null;\n  },\n});\n\nexport const storeExternalData = internalMutation({\n  args: {\n    data: v.any(),\n    syncedAt: v.number(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    await ctx.db.insert(\"externalData\", {\n      data: args.data,\n      syncedAt: args.syncedAt,\n    });\n    return null;\n  },\n});\n```\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\ncrons.interval(\n  \"sync external data\",\n  { minutes: 15 },\n  internal.sync.syncExternalData,\n  {}\n);\n\nexport default crons;\n```\n\n## Examples\n\n### Schema for Cron Job Logging\n\n```typescript\n\u002F\u002F convex\u002Fschema.ts\nimport { defineSchema, defineTable } from \"convex\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport default defineSchema({\n  cronLogs: defineTable({\n    jobName: v.string(),\n    startTime: v.number(),\n    endTime: v.number(),\n    duration: v.number(),\n    processedCount: v.number(),\n    errorCount: v.number(),\n    status: v.union(\n      v.literal(\"success\"),\n      v.literal(\"partial\"),\n      v.literal(\"failed\")\n    ),\n    error: v.optional(v.string()),\n  })\n    .index(\"by_job\", [\"jobName\"])\n    .index(\"by_status\", [\"status\"])\n    .index(\"by_startTime\", [\"startTime\"]),\n\n  sessions: defineTable({\n    userId: v.id(\"users\"),\n    token: v.string(),\n    lastActive: v.number(),\n    expiresAt: v.number(),\n  })\n    .index(\"by_user\", [\"userId\"])\n    .index(\"by_lastActive\", [\"lastActive\"])\n    .index(\"by_expiresAt\", [\"expiresAt\"]),\n\n  tasks: defineTable({\n    type: v.string(),\n    status: v.union(\n      v.literal(\"pending\"),\n      v.literal(\"processing\"),\n      v.literal(\"completed\"),\n      v.literal(\"failed\")\n    ),\n    data: v.any(),\n    createdAt: v.number(),\n    startedAt: v.optional(v.number()),\n    completedAt: v.optional(v.number()),\n  })\n    .index(\"by_status\", [\"status\"])\n    .index(\"by_type_and_status\", [\"type\", \"status\"]),\n});\n```\n\n### Complete Cron Configuration Example\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Cleanup jobs\ncrons.interval(\n  \"cleanup expired sessions\",\n  { hours: 1 },\n  internal.cleanup.expiredSessions,\n  {}\n);\n\ncrons.interval(\n  \"cleanup old logs\",\n  { hours: 24 },\n  internal.cleanup.oldLogs,\n  { maxAgeDays: 30 }\n);\n\n\u002F\u002F Sync jobs\ncrons.interval(\n  \"sync user data\",\n  { minutes: 15 },\n  internal.sync.userData,\n  {}\n);\n\n\u002F\u002F Report jobs\ncrons.cron(\n  \"daily analytics\",\n  \"0 1 * * *\",\n  internal.reports.dailyAnalytics,\n  {}\n);\n\ncrons.cron(\n  \"weekly summary\",\n  \"0 9 * * 1\",\n  internal.reports.weeklySummary,\n  {}\n);\n\n\u002F\u002F Health checks\ncrons.interval(\n  \"service health check\",\n  { minutes: 5 },\n  internal.monitoring.healthCheck,\n  {}\n);\n\nexport default crons;\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Only use `crons.interval` or `crons.cron` methods, not deprecated helpers\n- Always call internal functions from cron jobs for security\n- Import `internal` from `_generated\u002Fapi` even for functions in the same file\n- Add logging and monitoring for production cron jobs\n- Use batching for operations that process large datasets\n- Handle errors gracefully to prevent job failures\n- Use meaningful job names for dashboard visibility\n- Consider timezone when using cron expressions (Convex uses UTC)\n\n## Common Pitfalls\n\n1. **Using public functions** - Cron jobs should call internal functions only\n2. **Long-running mutations** - Break large operations into batches\n3. **Missing error handling** - Unhandled errors will fail the entire job\n4. **Forgetting timezone** - All cron expressions use UTC\n5. **Using deprecated helpers** - Avoid `crons.hourly`, `crons.daily`, etc.\n6. **Not logging execution** - Makes debugging production issues difficult\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- Cron Jobs: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\u002Fcron-jobs\n- Scheduling Overview: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\n- Scheduled Functions: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\u002Fscheduled-functions\n","# Convex Cron Jobs\n\nSchedule recurring functions for background tasks, cleanup jobs, data syncing, and automated workflows in Convex applications.\n\n## Documentation Sources\n\nBefore implementing, do not assume; fetch the latest documentation:\n\n- Primary: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\u002Fcron-jobs\n- Scheduling Overview: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\n- Scheduled Functions: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\u002Fscheduled-functions\n- For broader context: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n\n## Instructions\n\n### Cron Jobs Overview\n\nConvex cron jobs allow you to schedule functions to run at regular intervals or specific times. Key features:\n\n- Run functions on a fixed schedule\n- Support for interval-based and cron expression scheduling\n- Automatic retries on failure\n- Monitoring via the Convex dashboard\n\n### Basic Cron Setup\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Run every hour\ncrons.interval(\n  \"cleanup expired sessions\",\n  { hours: 1 },\n  internal.tasks.cleanupExpiredSessions,\n  {}\n);\n\n\u002F\u002F Run every day at midnight UTC\ncrons.cron(\n  \"daily report\",\n  \"0 0 * * *\",\n  internal.reports.generateDailyReport,\n  {}\n);\n\nexport default crons;\n```\n\n### Interval-Based Scheduling\n\nUse `crons.interval` for simple recurring tasks:\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Every 5 minutes\ncrons.interval(\n  \"sync external data\",\n  { minutes: 5 },\n  internal.sync.fetchExternalData,\n  {}\n);\n\n\u002F\u002F Every 2 hours\ncrons.interval(\n  \"cleanup temp files\",\n  { hours: 2 },\n  internal.files.cleanupTempFiles,\n  {}\n);\n\n\u002F\u002F Every 30 seconds (minimum interval)\ncrons.interval(\n  \"health check\",\n  { seconds: 30 },\n  internal.monitoring.healthCheck,\n  {}\n);\n\nexport default crons;\n```\n\n### Cron Expression Scheduling\n\nUse `crons.cron` for precise scheduling with cron expressions:\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Every day at 9 AM UTC\ncrons.cron(\n  \"morning notifications\",\n  \"0 9 * * *\",\n  internal.notifications.sendMorningDigest,\n  {}\n);\n\n\u002F\u002F Every Monday at 8 AM UTC\ncrons.cron(\n  \"weekly summary\",\n  \"0 8 * * 1\",\n  internal.reports.generateWeeklySummary,\n  {}\n);\n\n\u002F\u002F First day of every month at midnight\ncrons.cron(\n  \"monthly billing\",\n  \"0 0 1 * *\",\n  internal.billing.processMonthlyBilling,\n  {}\n);\n\n\u002F\u002F Every 15 minutes\ncrons.cron(\n  \"frequent sync\",\n  \"*\u002F15 * * * *\",\n  internal.sync.syncData,\n  {}\n);\n\nexport default crons;\n```\n\n### Cron Expression Reference\n\n```\n┌───────────── minute (0-59)\n│ ┌───────────── hour (0-23)\n│ │ ┌───────────── day of month (1-31)\n│ │ │ ┌───────────── month (1-12)\n│ │ │ │ ┌───────────── day of week (0-6, Sunday=0)\n│ │ │ │ │\n* * * * *\n```\n\nCommon patterns:\n- `* * * * *` - Every minute\n- `0 * * * *` - Every hour\n- `0 0 * * *` - Every day at midnight\n- `0 0 * * 0` - Every Sunday at midnight\n- `0 0 1 * *` - First day of every month\n- `*\u002F5 * * * *` - Every 5 minutes\n- `0 9-17 * * 1-5` - Every hour from 9 AM to 5 PM, Monday through Friday\n\n### Internal Functions for Crons\n\nCron jobs should call internal functions for security:\n\n```typescript\n\u002F\u002F convex\u002Ftasks.ts\nimport { internalMutation, internalQuery } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\n\u002F\u002F Cleanup expired sessions\nexport const cleanupExpiredSessions = internalMutation({\n  args: {},\n  returns: v.number(),\n  handler: async (ctx) => {\n    const oneHourAgo = Date.now() - 60 * 60 * 1000;\n    \n    const expiredSessions = await ctx.db\n      .query(\"sessions\")\n      .withIndex(\"by_lastActive\")\n      .filter((q) => q.lt(q.field(\"lastActive\"), oneHourAgo))\n      .collect();\n\n    for (const session of expiredSessions) {\n      await ctx.db.delete(session._id);\n    }\n\n    return expiredSessions.length;\n  },\n});\n\n\u002F\u002F Process pending tasks\nexport const processPendingTasks = internalMutation({\n  args: {},\n  returns: v.null(),\n  handler: async (ctx) => {\n    const pendingTasks = await ctx.db\n      .query(\"tasks\")\n      .withIndex(\"by_status\", (q) => q.eq(\"status\", \"pending\"))\n      .take(100);\n\n    for (const task of pendingTasks) {\n      await ctx.db.patch(task._id, {\n        status: \"processing\",\n        startedAt: Date.now(),\n      });\n      \n      \u002F\u002F Schedule the actual processing\n      await ctx.scheduler.runAfter(0, internal.tasks.processTask, {\n        taskId: task._id,\n      });\n    }\n\n    return null;\n  },\n});\n```\n\n### Cron Jobs with Arguments\n\nPass static arguments to cron jobs:\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Different cleanup intervals for different types\ncrons.interval(\n  \"cleanup temp files\",\n  { hours: 1 },\n  internal.cleanup.cleanupByType,\n  { fileType: \"temp\", maxAge: 3600000 }\n);\n\ncrons.interval(\n  \"cleanup cache files\",\n  { hours: 24 },\n  internal.cleanup.cleanupByType,\n  { fileType: \"cache\", maxAge: 86400000 }\n);\n\nexport default crons;\n```\n\n```typescript\n\u002F\u002F convex\u002Fcleanup.ts\nimport { internalMutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport const cleanupByType = internalMutation({\n  args: {\n    fileType: v.string(),\n    maxAge: v.number(),\n  },\n  returns: v.number(),\n  handler: async (ctx, args) => {\n    const cutoff = Date.now() - args.maxAge;\n    \n    const oldFiles = await ctx.db\n      .query(\"files\")\n      .withIndex(\"by_type_and_created\", (q) => \n        q.eq(\"type\", args.fileType).lt(\"createdAt\", cutoff)\n      )\n      .collect();\n\n    for (const file of oldFiles) {\n      await ctx.storage.delete(file.storageId);\n      await ctx.db.delete(file._id);\n    }\n\n    return oldFiles.length;\n  },\n});\n```\n\n### Monitoring and Logging\n\nAdd logging to track cron job execution:\n\n```typescript\n\u002F\u002F convex\u002Ftasks.ts\nimport { internalMutation } from \".\u002F_generated\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport const cleanupWithLogging = internalMutation({\n  args: {},\n  returns: v.null(),\n  handler: async (ctx) => {\n    const startTime = Date.now();\n    let processedCount = 0;\n    let errorCount = 0;\n\n    try {\n      const expiredItems = await ctx.db\n        .query(\"items\")\n        .withIndex(\"by_expiresAt\")\n        .filter((q) => q.lt(q.field(\"expiresAt\"), Date.now()))\n        .collect();\n\n      for (const item of expiredItems) {\n        try {\n          await ctx.db.delete(item._id);\n          processedCount++;\n        } catch (error) {\n          errorCount++;\n          console.error(`Failed to delete item ${item._id}:`, error);\n        }\n      }\n\n      \u002F\u002F Log job completion\n      await ctx.db.insert(\"cronLogs\", {\n        jobName: \"cleanup\",\n        startTime,\n        endTime: Date.now(),\n        duration: Date.now() - startTime,\n        processedCount,\n        errorCount,\n        status: errorCount === 0 ? \"success\" : \"partial\",\n      });\n    } catch (error) {\n      \u002F\u002F Log job failure\n      await ctx.db.insert(\"cronLogs\", {\n        jobName: \"cleanup\",\n        startTime,\n        endTime: Date.now(),\n        duration: Date.now() - startTime,\n        processedCount,\n        errorCount,\n        status: \"failed\",\n        error: String(error),\n      });\n      throw error;\n    }\n\n    return null;\n  },\n});\n```\n\n### Batching for Large Datasets\n\nHandle large datasets in batches to avoid timeouts:\n\n```typescript\n\u002F\u002F convex\u002Ftasks.ts\nimport { internalMutation } from \".\u002F_generated\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\nimport { v } from \"convex\u002Fvalues\";\n\nconst BATCH_SIZE = 100;\n\nexport const processBatch = internalMutation({\n  args: {\n    cursor: v.optional(v.string()),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    const result = await ctx.db\n      .query(\"items\")\n      .withIndex(\"by_status\", (q) => q.eq(\"status\", \"pending\"))\n      .paginate({ numItems: BATCH_SIZE, cursor: args.cursor ?? null });\n\n    for (const item of result.page) {\n      await ctx.db.patch(item._id, {\n        status: \"processed\",\n        processedAt: Date.now(),\n      });\n    }\n\n    \u002F\u002F Schedule next batch if there are more items\n    if (!result.isDone) {\n      await ctx.scheduler.runAfter(0, internal.tasks.processBatch, {\n        cursor: result.continueCursor,\n      });\n    }\n\n    return null;\n  },\n});\n```\n\n### External API Calls in Crons\n\nUse actions for external API calls:\n\n```typescript\n\u002F\u002F convex\u002Fsync.ts\n\"use node\";\n\nimport { internalAction } from \".\u002F_generated\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport const syncExternalData = internalAction({\n  args: {},\n  returns: v.null(),\n  handler: async (ctx) => {\n    \u002F\u002F Fetch from external API\n    const response = await fetch(\"https:\u002F\u002Fapi.example.com\u002Fdata\", {\n      headers: {\n        Authorization: `Bearer ${process.env.API_KEY}`,\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`API request failed: ${response.status}`);\n    }\n\n    const data = await response.json();\n\n    \u002F\u002F Store the data using a mutation\n    await ctx.runMutation(internal.sync.storeExternalData, {\n      data,\n      syncedAt: Date.now(),\n    });\n\n    return null;\n  },\n});\n\nexport const storeExternalData = internalMutation({\n  args: {\n    data: v.any(),\n    syncedAt: v.number(),\n  },\n  returns: v.null(),\n  handler: async (ctx, args) => {\n    await ctx.db.insert(\"externalData\", {\n      data: args.data,\n      syncedAt: args.syncedAt,\n    });\n    return null;\n  },\n});\n```\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\ncrons.interval(\n  \"sync external data\",\n  { minutes: 15 },\n  internal.sync.syncExternalData,\n  {}\n);\n\nexport default crons;\n```\n\n## Examples\n\n### Schema for Cron Job Logging\n\n```typescript\n\u002F\u002F convex\u002Fschema.ts\nimport { defineSchema, defineTable } from \"convex\u002Fserver\";\nimport { v } from \"convex\u002Fvalues\";\n\nexport default defineSchema({\n  cronLogs: defineTable({\n    jobName: v.string(),\n    startTime: v.number(),\n    endTime: v.number(),\n    duration: v.number(),\n    processedCount: v.number(),\n    errorCount: v.number(),\n    status: v.union(\n      v.literal(\"success\"),\n      v.literal(\"partial\"),\n      v.literal(\"failed\")\n    ),\n    error: v.optional(v.string()),\n  })\n    .index(\"by_job\", [\"jobName\"])\n    .index(\"by_status\", [\"status\"])\n    .index(\"by_startTime\", [\"startTime\"]),\n\n  sessions: defineTable({\n    userId: v.id(\"users\"),\n    token: v.string(),\n    lastActive: v.number(),\n    expiresAt: v.number(),\n  })\n    .index(\"by_user\", [\"userId\"])\n    .index(\"by_lastActive\", [\"lastActive\"])\n    .index(\"by_expiresAt\", [\"expiresAt\"]),\n\n  tasks: defineTable({\n    type: v.string(),\n    status: v.union(\n      v.literal(\"pending\"),\n      v.literal(\"processing\"),\n      v.literal(\"completed\"),\n      v.literal(\"failed\")\n    ),\n    data: v.any(),\n    createdAt: v.number(),\n    startedAt: v.optional(v.number()),\n    completedAt: v.optional(v.number()),\n  })\n    .index(\"by_status\", [\"status\"])\n    .index(\"by_type_and_status\", [\"type\", \"status\"]),\n});\n```\n\n### Complete Cron Configuration Example\n\n```typescript\n\u002F\u002F convex\u002Fcrons.ts\nimport { cronJobs } from \"convex\u002Fserver\";\nimport { internal } from \".\u002F_generated\u002Fapi\";\n\nconst crons = cronJobs();\n\n\u002F\u002F Cleanup jobs\ncrons.interval(\n  \"cleanup expired sessions\",\n  { hours: 1 },\n  internal.cleanup.expiredSessions,\n  {}\n);\n\ncrons.interval(\n  \"cleanup old logs\",\n  { hours: 24 },\n  internal.cleanup.oldLogs,\n  { maxAgeDays: 30 }\n);\n\n\u002F\u002F Sync jobs\ncrons.interval(\n  \"sync user data\",\n  { minutes: 15 },\n  internal.sync.userData,\n  {}\n);\n\n\u002F\u002F Report jobs\ncrons.cron(\n  \"daily analytics\",\n  \"0 1 * * *\",\n  internal.reports.dailyAnalytics,\n  {}\n);\n\ncrons.cron(\n  \"weekly summary\",\n  \"0 9 * * 1\",\n  internal.reports.weeklySummary,\n  {}\n);\n\n\u002F\u002F Health checks\ncrons.interval(\n  \"service health check\",\n  { minutes: 5 },\n  internal.monitoring.healthCheck,\n  {}\n);\n\nexport default crons;\n```\n\n## Best Practices\n\n- Never run `npx convex deploy` unless explicitly instructed\n- Never run any git commands unless explicitly instructed\n- Only use `crons.interval` or `crons.cron` methods, not deprecated helpers\n- Always call internal functions from cron jobs for security\n- Import `internal` from `_generated\u002Fapi` even for functions in the same file\n- Add logging and monitoring for production cron jobs\n- Use batching for operations that process large datasets\n- Handle errors gracefully to prevent job failures\n- Use meaningful job names for dashboard visibility\n- Consider timezone when using cron expressions (Convex uses UTC)\n\n## Common Pitfalls\n\n1. **Using public functions** - Cron jobs should call internal functions only\n2. **Long-running mutations** - Break large operations into batches\n3. **Missing error handling** - Unhandled errors will fail the entire job\n4. **Forgetting timezone** - All cron expressions use UTC\n5. **Using deprecated helpers** - Avoid `crons.hourly`, `crons.daily`, etc.\n6. **Not logging execution** - Makes debugging production issues difficult\n\n## References\n\n- Convex Documentation: https:\u002F\u002Fdocs.convex.dev\u002F\n- Convex LLMs.txt: https:\u002F\u002Fdocs.convex.dev\u002Fllms.txt\n- Cron Jobs: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\u002Fcron-jobs\n- Scheduling Overview: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\n- Scheduled Functions: https:\u002F\u002Fdocs.convex.dev\u002Fscheduling\u002Fscheduled-functions\n",{"name":5,"next":44,"tags":46,"group":49,"author":50,"version":51,"workflows":52,"description":7,"displayName":54,"prerequisites":55,"user-invocable":57},[45],"convex-security-check",[23,20,47,48,37],"scheduling","background-jobs","build","Convex","1.0.0",[53],"full-startup","Convex Cron Jobs",[56],"convex-functions",true,"main","92dddf7f6e7b55d1424795ce11586b05dba9d8c8","skills\u002Fconvex-cron-jobs",3,"https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups","https:\u002F\u002Fgithub.com\u002FCheggin","reaganhsu.com","MTS @datacurve-ai, prev. founding eng @browser-use ","2026-04-24 08:33:47",{"source":8,"skillName":5,"commands":68,"prompt":78,"zipAvailable":57},[69,72,75],{"tool":70,"command":71},"npx","npx skills add Cheggin\u002Frequest-for-startups --skill convex-cron-jobs",{"tool":73,"command":74},"bunx","bunx skills add Cheggin\u002Frequest-for-startups --skill convex-cron-jobs",{"tool":76,"command":77},"pnpm","pnpm dlx skills add Cheggin\u002Frequest-for-startups --skill convex-cron-jobs","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups\u002Fblob\u002Fmain\u002Fskills\u002Fconvex-cron-jobs\u002FSKILL.md",[],[81,104,124,166,181,204],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":85,"categories":86,"keywords":87,"publishedAt":39},"01M6X7PHK069RZH25A3RXN5049","writer-memory","Agentic memory system for writers - track characters, relationships, scenes, and themes","https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups\u002Fblob\u002Fmain\u002Fskills\u002Fwriter-memory\u002FSKILL.md",[],[88,89,90,91,92,93,26,94,95,96,97,98,99,100,101,102,103],"memory","writer","writer memory","system","characters","track","themes","memory system","scenes","relationships","query","export","status","validate","init","agentic",{"skillNo":105,"name":106,"nameZh":6,"description":107,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":108,"categories":109,"keywords":110,"publishedAt":39},"01M6X7PC3G9228Z323X5YAKTVW","wiki","LLM Wiki — persistent markdown knowledge base that compounds across sessions (Karpathy model)","https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups\u002Fblob\u002Fmain\u002Fskills\u002Fwiki\u002FSKILL.md",[],[106,111,112,113,114,115,116,117,118,119,120,121,122,98,123],"wiki lint","knowledge","model","llm wiki","markdown","wiki query","knowledge base","lint","sessions","karpathy","add","across","base",{"skillNo":125,"name":126,"nameZh":6,"description":127,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":128,"categories":129,"keywords":146,"publishedAt":39},"01M6X7P5XFW5C16H3JE4NFQ7MF","website-creation","Build production-quality SaaS websites with opinionated design presets. Use when creating any startup website. The user MUST pick a design style before building. Enforces shadcn\u002Fui, Figma design principles, specific CSS values per style, and anti-AI-writing. Load alongside anti-ai-writing skill. LIGHT MODE ONLY.","https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups\u002Fblob\u002Fmain\u002Fskills\u002Fwebsite-creation\u002FSKILL.md",[130,135,138,143],{"categoryNo":131,"categoryName":132,"taxonomyType":133,"taxonomyTypeDesc":134},"01JBSEEDCAT000000000000015","视觉设计","SCENARIO","使用场景",{"categoryNo":136,"categoryName":137,"taxonomyType":133,"taxonomyTypeDesc":134},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":139,"categoryName":140,"taxonomyType":141,"taxonomyTypeDesc":142},"01JBSEEDCAT000000000000032","设计师","PROFESSION","适用职业",{"categoryNo":144,"categoryName":145,"taxonomyType":141,"taxonomyTypeDesc":142},"01JBSEEDCAT000000000000036","内容创作者",[147,148,149,150,151,152,153,31,154,155,156,157,158,159,160,161,162,163,164,165],"website creation","website","creation","when","design style","mode","brand-guidelines","ui","writing","design principles","only","pick","polish","audit","shadcn","figma","css","Anti-AI","design",{"skillNo":167,"name":168,"nameZh":6,"description":169,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":170,"categories":171,"keywords":172,"publishedAt":39},"01M6X7P3PCEAKA2SBETXHHTNBA","visual-verdict","Structured visual QA verdict for screenshot-to-reference comparisons","https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups\u002Fblob\u002Fmain\u002Fskills\u002Fvisual-verdict\u002FSKILL.md",[],[173,174,175,26,176,177,178,179,180],"visual verdict","verdict","visual","visual qa","structured","reference","screenshot","qa",{"skillNo":182,"name":183,"nameZh":6,"description":184,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":185,"categories":186,"keywords":195,"publishedAt":39},"01M6X7NXEWWC4NFT0ZK1RRSGES","visual-qa-pipeline","Screenshot every page with Playwright, feed to visual QA agent for design evaluation, report results to Slack. Use after every build phase to verify UI matches the chosen design preset. Integrates with the Playwright skill and the website-creation preset system.","https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups\u002Fblob\u002Fmain\u002Fskills\u002Fvisual-qa-pipeline\u002FSKILL.md",[187,190,193,194],{"categoryNo":188,"categoryName":189,"taxonomyType":133,"taxonomyTypeDesc":134},"01JBSEEDCAT000000000000005","邮件与沟通",{"categoryNo":191,"categoryName":192,"taxonomyType":133,"taxonomyTypeDesc":134},"01JBSEEDCAT000000000000008","测试与质量",{"categoryNo":131,"categoryName":132,"taxonomyType":133,"taxonomyTypeDesc":134},{"categoryNo":139,"categoryName":140,"taxonomyType":141,"taxonomyTypeDesc":142},[175,196,176,180,197,198,126,149,31,199,159,200,154,148,179,26,201,165,202,203],"pipeline","visual qa pipeline","slack","page","Evaluation","review","verify ui","phase",{"skillNo":205,"name":206,"nameZh":6,"description":207,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":208,"categories":209,"keywords":210,"publishedAt":39},"01M6X7NSNW00RQ8J531F52YHV9","verify","Verify that a change really works before you claim completion","https:\u002F\u002Fgithub.com\u002FCheggin\u002Frequest-for-startups\u002Fblob\u002Fmain\u002Fskills\u002Fverify\u002FSKILL.md",[],[206,211,28,212,213,214,196,215,216],"deploy-pipeline","you","claim","quality","deploy","change"]