Skip to content
Backend
Skill

/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.

BOOST
From plugin
builder-skills
40619 skills
Install
$ npx -y skills add waynesutton/builder-skills --skill convex-cron-jobs --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/convex-cron-jobs

Context preview

The summary Claude sees to decide when to auto-load this skill.

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.

SKILL.md

convex-cron-jobs.SKILL.md
name: convex-cron-jobs
description: 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.

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:

  • [references/scheduling-patterns.md](references/scheduling-patterns.md): open when you need retry with backoff, cancelling a job, reading `_scheduled_functions`, debouncing, or local time zones.
  • [references/cron-recipes.md](references/cron-recipes.md): open for short copyable jobs: daily digest email, session expiry, stats rollup, external API sync.

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):

| Expression | Runs | | --- | --- | | `*/15 * * * *` | every 15 minutes | | `0 * * * *` | every hour at :00 | | `0 0 * * *` | daily at 00:00 UTC | | `0 8 * * 1` | Mondays at 08:00 UTC | | `0 0 1 * *` | first of each month | | `0 9-17 * * 1-5` | hourly, 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,
});

| Method | Use 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

Read more
Ships withbuilder-skills

Agent skills for builders shipping Convex apps. By Wayne Sutton. Seventeen skills.

Get the whole plugin

Other skills on builder-skills.