Skip to content
cccskills
FreePublished on GitHub

convex-cron-jobs

Schedules work in Convex: cron jobs in convex/crons.ts, one off scheduled functions with runAfter and runAt, batching large jobs, and cancelling or inspecting the queue. Use when something needs to run on a timer, later, or in the background, or when a cron is not firing.

The English summary is being prepared. Showing the original description.

Run score Not tested

See how to install

Files included (6)

  • SKILL.md7.7 KB
  • agents/openai.yaml91 B
  • assets/large-logo.png3.2 KB
  • assets/small-logo.svg1.2 KB
  • references/cron-recipes.md8.4 KB
  • references/scheduling-patterns.md9.0 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Convex cron jobs and scheduling

Recurring work lives in convex/crons.ts. One off work is scheduled from inside a function with ctx.scheduler. Both must target internal.* functions, never api.*.

When to reach for this

  • Something needs to run every N minutes or at a fixed time of day
  • A mutation needs to kick off follow up work after it commits
  • A job touches more rows than one mutation should handle
  • A cron shows in the dashboard but never runs, or runs at the wrong hour
  • A pending job needs to be cancelled, debounced, or inspected

Deeper material lives in two reference files:

crons.ts skeleton

One file, one default export. crons.interval for "every N", crons.cron for calendar times. crons.daily, crons.hourly, and crons.weekly are deprecated helpers. Do not use them.

// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";

const crons = cronJobs();

// Every hour
crons.interval(
  "expire sessions",
  { hours: 1 },
  internal.sessions.expireBatch,
  {},
);

// Every day at 09:00 UTC. Cron expressions are always UTC.
crons.cron("daily digest", "0 9 * * *", internal.digest.send, {});

export default crons;

Rules for every entry:

  • Names are unique within the file. The dashboard lists jobs by this name.
  • Import internal from ./_generated/api, even when the target is defined in crons.ts.
  • Args are static and must satisfy the target function's args validator.
  • Interval units are { seconds }, { minutes }, or { hours }.

Cron expression quick reference (minute hour day-of-month month day-of-week):

ExpressionRuns
*/15 * * * *every 15 minutes
0 * * * *every hour at :00
0 0 * * *daily at 00:00 UTC
0 8 * * 1Mondays at 08:00 UTC
0 0 1 * *first of each month
0 9-17 * * 1-5hourly, 09:00 to 17:00 UTC, weekdays

Targets are internal functions

Public functions expect a client, an auth identity, and untrusted input. Cron and scheduler calls have none of that. A public target skips the auth checks you wrote and exposes the job to anyone who can reach the deployment. Register targets with internalMutation, internalAction, or internalQuery.

One batched job

A mutation is one transaction with read and write limits. Deleting fifty thousand rows in a loop hits them. Take a fixed slice, reschedule yourself with runAfter(0, ...), and let the chain finish on its own.

// convex/sessions.ts
import { internalMutation } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";

const BATCH = 100;

export const expireBatch = internalMutation({
  args: {},
  returns: v.null(),
  handler: async (ctx) => {
    // Date.now() is fine in a mutation. Never call it in a query.
    const now = Date.now();
    const expired = await ctx.db
      .query("sessions")
      .withIndex("by_expiresAt", (q) => q.lt("expiresAt", now))
      .take(BATCH);

    await Promise.all(expired.map((s) => ctx.db.delete(s._id)));

    // A full batch means more may remain. Chain the next one.
    if (expired.length === BATCH) {
      await ctx.scheduler.runAfter(0, internal.sessions.expireBatch, {});
    }
    return null;
  },
});

This shape works because each delete removes the row from the index range. If the job updates rows without moving them out of the range, use a pagination cursor instead. See the reference file.

runAfter vs runAt

// Relative: 5 minutes from now
const jobId = await ctx.scheduler.runAfter(
  5 * 60 * 1000,
  internal.reminders.send,
  { taskId: args.taskId },
);

// Absolute: a timestamp you already store (ms since epoch or a Date)
await ctx.scheduler.runAt(trial.endsAt, internal.billing.endTrial, {
  userId: trial.userId,
});
MethodUse for
runAfter(delayMs, fn, args)retries, follow ups, "in ten minutes"
runAt(timestamp, fn, args)trial ends, send dates, anything with a stored time

Both return an Id<"_scheduled_functions">. Store it on a document if you may need to cancel.

Two behaviors to remember:

  • Scheduling inside a mutation is transactional. If the mutation throws, nothing is scheduled. Scheduling inside an action happens right away, even if the action fails later.
  • Scheduled mutations run exactly once. Scheduled actions may fail without retry, so add retry logic to actions or use a retry component.

Seeing runs in the dashboard

  • Schedules, Cron Jobs tab: every entry from crons.ts, with last run and next run.
  • Schedules, Scheduled Functions tab: pending runAfter and runAt jobs.
  • Logs, filtered by function name: each execution, its duration, and any thrown error.
  • From the CLI: npx convex logs streams the same log lines.
  • From code: ctx.db.system.get(jobId) returns the job document with state.kind set to pending, inProgress, success, failed, or canceled.

When a cron is not firing

  1. The file is exactly convex/crons.ts and ends with export default crons.
  2. npx convex dev is running and the last push succeeded. Cron changes only apply on push.
  3. The target is internal.* and the args match its validator. A mismatch fails at push time.
  4. The expression is UTC. Convert your local hour before comparing.
  5. Check Logs for a thrown error. A job that throws every run looks like a job that never runs.

Common mistakes

MistakeWhy it breaksDo instead
crons.daily(...)deprecated helpercrons.cron("...", "0 0 * * *", ...)
Target is api.tasks.cleanupskips auth, publicly callableregister as internalMutation, use internal.tasks.cleanup
.collect() then loop over thousandshits transaction limitstake(BATCH) and reschedule
.withIndex("by_x").filter(...)filter scans the whole indexput the range in withIndex
Date.now() in an internalQuerybreaks caching and reactivitypass now as an arg from the caller
Cron at "0 9 * * *" for 9am Pacificruns at 9am UTCuse UTC, or run hourly and check local hour
Missing await on runAfterjob may not be scheduledalways await ctx.scheduler.*
Two crons with the same namepush failsunique names per file

Checklist

  • convex/crons.ts uses only crons.interval and crons.cron, ends with export default crons
  • Every cron and scheduler target is internal.*
  • Every target has args and returns validators
  • Jobs that touch many rows take a batch and reschedule with runAfter(0, ...)
  • Range conditions live in withIndex, not .filter
  • No Date.now() inside queries
  • Every ctx.scheduler.* call is awaited
  • Cron hours are written in UTC
  • Job ids are stored on documents when cancel or debounce is needed
  • Ran npx convex dev and saw the job listed under Schedules

Docs

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a suggestions list instead of the diff. Use when a request is small and the agent is about to expand it, when a PRD grows beyond its problem statement, or when the user says 'just do what I asked'.

The English summary is being prepared. Showing the original description.

waynesutton/builder-skills405Updated Sep 28, 2026

convex

Free

Routes Convex work to the right builder skill and covers the handful of rules that apply everywhere: validators on every function, withIndex over filter, idempotent mutations, internal functions for scheduling. Use when a task mentions Convex and no more specific skill matches, or as the first stop before picking one.

The English summary is being prepared. Showing the original description.

waynesutton/builder-skills405Updated Sep 28, 2026

Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with vector search, and workflows for multi step jobs. Use when adding a chat assistant, tool calling agent, or retrieval feature to a Convex app.

The English summary is being prepared. Showing the original description.

waynesutton/builder-skills405Updated Sep 28, 2026

Production patterns for Convex apps and the rules the @convex-dev/eslint-plugin enforces: validators, indexes, idempotent mutations, avoiding OCC conflicts, thin function wrappers, error handling. Use when reviewing Convex code, asking whether a pattern is right, setting up ESLint, or fixing write conflicts and slow queries.

The English summary is being prepared. Showing the original description.

waynesutton/builder-skills405Updated Sep 28, 2026

Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup. Use when extracting a feature into a package, building something for the Convex component directory, or when a component's functions are not showing up in the parent app.

The English summary is being prepared. Showing the original description.

waynesutton/builder-skills405Updated Sep 28, 2026

Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the _storage table, and deletion. Use when users upload images or documents, when an action fetches a file from a third party, or when a file URL expires unexpectedly.

The English summary is being prepared. Showing the original description.

waynesutton/builder-skills405Updated Sep 28, 2026

See all skills by waynesutton

Report a problem with this skill