avoid-feature-creep
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
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.
$ npx -y skills add waynesutton/builder-skills --skill convex-cron-jobs --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/convex-cron-jobsContext 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.
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.
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.*`.
Deeper material lives in two reference files:
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:
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 |
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`.
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.
// 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:
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
Agent skills for builders shipping Convex apps. By Wayne Sutton. Seventeen skills.
Keeps a change scoped to what was asked. Flags extra features, refactors, and 'while we are here' edits, and moves them to a…
Builds AI agents on the Convex agent component: threads, messages, tools that call queries and mutations, streaming, RAG with…
Production patterns for Convex apps and the rules the @convex-dev/eslint-plugin enforces: validators, indexes, idempotent…
Creates reusable Convex components with defineComponent, a clean client wrapper, their own schema, and an npm publish setup. Use…
Handles files in Convex: upload URLs, storing blobs from actions, serving with getUrl or an HTTP action, metadata from the…
Writes Convex queries, mutations, actions, and internal functions in the object form with args and returns validators, correct…