Skip to main content
skill CX Codex

convex-cron-jobs — Claude Code skill: what it does, how to install

Sets up recurring jobs in convex/crons.ts and delayed execution with ctx.scheduler. Helps batch heavy mutations and debug failing queues in Convex.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add waynesutton/builder-skills --skill convex-cron-jobs -a claude-code
Open on GitHub

What convex-cron-jobs does

The skill guides an agent through scheduling recurring and one-off background work in Convex. It configures convex/crons.ts using crons.interval or UTC cron expressions. One-off delayed jobs are scheduled right from application functions using ctx.scheduler.runAfter or ctx.scheduler.runAt.

All targets must point to internal.* functions rather than api.*. Calling a public endpoint bypasses authorization checks and leaves the job exposed to anyone reaching your deployment. For large datasets, the skill builds chained runs. A mutation grabs a slice of 100 items with take(100) and calls runAfter(0, ...) for the next batch. This prevents hitting transaction limits.

Scheduling within mutations is transactional. If a mutation throws, nothing enters the queue. Actions behave differently, so the skill reminds you to handle retries explicitly.

What it helps with

  • schedule recurring tasks in convex/crons.ts
  • trigger delayed functions using runAfter or runAt
  • batch row deletions to avoid transaction limits
  • diagnose why a Convex cron job fails to trigger

Example prompts for the agent

  • Set up a daily digest cron job in convex/crons.ts running at 09:00 UTC
  • Batch session expiry in Convex so it processes 100 rows per run
  • Debug why my scheduled function in Convex is not firing

How to install convex-cron-jobs

1Skills CLI (Vercel)

npx skills add waynesutton/builder-skills --skill convex-cron-jobs -a claude-code

2Manually: put the files in place

~/.claude/skills/convex-cron-jobs/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/convex-cron-jobs/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about convex-cron-jobs

How to install convex-cron-jobs in Claude Code?

Run npx skills add waynesutton/builder-skills --skill convex-cron-jobs -a claude-code.

What does convex-cron-jobs do?

It provides patterns and guidelines for Convex background scheduling. The skill covers recurring jobs in convex/crons.ts, one-off jobs via ctx.scheduler, batching, and queue inspection.

Why must scheduled tasks target internal functions?

Public functions expect client credentials and untrusted input. Targeting public endpoints skips auth checks and exposes your background jobs to direct external calls.

Source text

SKILL.md · Apache-2.0
---
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.

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

```typescript
// 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

```typescript
// 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 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

| Mistake | Why it breaks | Do instead |
| --- | --- | --- |
| `crons.daily(...)` | deprecated helper | `crons.cron("...", "0 0 * * *", ...)` |
| Target is `api.tasks.cleanup` | skips auth, publicly callable | register as `internalMutation`, use `internal.tasks.cleanup` |
| `.collect()` then loop over thousands | hits transaction limits | `take(BATCH)` and reschedule |
| `.withIndex("by_x").filter(...)` | filter scans the whole index | put the range in `withIndex` |
| `Date.now()` in an `internalQuery` | breaks caching and reactivity | pass `now` as an arg from the caller |
| Cron at `"0 9 * * *"` for 9am Pacific | runs at 9am UTC | use UTC, or run hourly and check local hour |
| Missing `await` on `runAfter` | job may not be scheduled | always `await ctx.scheduler.*` |
| Two crons with the same name | push fails | unique 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

- https://docs.convex.dev/llms.txt
- https://docs.convex.dev/scheduling/cron-jobs
- https://docs.convex.dev/scheduling/scheduled-functions
- https://docs.convex.dev/database/advanced/system-tables

© waynesutton · Apache-2.0 · waynesutton/builder-skills

Similar skills

Everything for Backend and APIs →

From the same set14

convex-best-practices Guides your agent through production Convex patterns, from index design to OCC conflict prevention. Essential for clean mutations, schema validation, and fast queries. CX Codex +1 3,900installs convex-functions Writes Convex queries, mutations and actions in the object form, with args and returns validators, correct ctx usage and runtime boundaries. CX Codex +1 3,200installs convex-schema-validator Designs Convex tables, validators, indexes, and document relationships in convex/schema.ts. Use it when a schema changes or validation fails during development. CX Codex +1 3,000installs convex-realtime A skill for wiring React to Convex subscriptions: useQuery patterns, optimistic updates, pagination that stays live, and cutting subscription churn. CX Codex +1 2,900installs convex-security-check Checks a Convex backend before merge for exposed functions, missing auth, weak validation, ownership gaps, and secrets in code. CX Codex +1 2,700installs convex-http-actions Adds HTTP endpoints in convex/http.ts: webhook receivers with signature checks, REST routes, CORS, streaming and file uploads. CX Codex +1 2,700installs convex-migrations Walks a live Convex schema change through six steps: optional field, batched backfill, validator flip, cleanup. Useful when npx convex dev rejects existing documents. CX Codex +1 2,600installs convex-file-storage Handles Convex file storage operations: upload URLs, blob persistence in actions, and serving with getUrl. Useful when building user uploads or fetching external assets. CX Codex +1 2,600installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor