关于 convex-cron-jobs
面向 Convex 开发者的定时任务参考,说明如何配置周期 Cron 和一次性调度、批量处理及检查执行状态。重点包括仅调用 internal 函数、UTC 时间和任务重试等规则。
主要功能
- 01配置周期 Cron 与一次性任务
- 02使用 internal 函数作为任务目标
- 03分批处理并重新调度
- 04使用相对或绝对时间调度
- 05查看任务运行状态与日志
- 06处理时区、取消和重试
适用场景
- 01按固定间隔或日历时间运行工作
- 02在变更提交后安排后续工作
- 03分批处理大量记录
- 04排查未运行或时间不正确的 Cron
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ waynesutton/ builder-skills --skill convex-cron-jobs安装后在 Claude Code 中输入/convex-cron-jobs确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/small-logo.svgreferences/cron-recipes.mdscheduling-patterns.md详细介绍
Convex Cron 任务与调度
周期性工作写在 convex/crons.ts 中。一次性工作通过函数内部的 ctx.scheduler 进行调度。两者都必须调用 internal.* 函数,不能调用 api.*。
何时使用
- 需要每隔 N 分钟运行,或在每天固定时间运行
- 某个 mutation 需要在提交后启动后续工作
- 某个任务涉及的行数超过单个 mutation 应处理的范围
- Cron 显示在仪表板中,却从未运行,或运行时间不对
- 需要取消、防抖或检查待处理任务
更多内容请参阅以下两个参考文件:
- references/scheduling-patterns.md:需要了解带退避的重试、取消任务、读取
_scheduled_functions、防抖或本地时区时打开。 - references/cron-recipes.md:需要简短且可直接复制的任务示例时打开,例如每日摘要邮件、会话过期、统计汇总、外部 API 同步。
crons.ts 骨架
一个文件,一个默认导出。对于“每隔 N”使用 crons.interval,对于日历时间使用 crons.cron。crons.daily、crons.hourly 和 crons.weekly 是已弃用的辅助函数。不要使用它们。
// 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;
每个条目都必须遵守以下规则:
- 名称在文件中必须唯一。仪表板按此名称列出任务。
- 从
./_generated/api导入internal,即使目标函数定义在crons.ts中也是如此。 - 参数是静态的,且必须符合目标函数的
args验证器。 - 间隔单位为
{ seconds }、{ minutes }或{ hours }。
Cron 表达式速查(分钟 小时 月份中的日期 月份 星期几):
| 表达式 | 运行时间 |
|---|---|
*/15 * * * * | 每 15 分钟 |
0 * * * * | 每小时的 :00 |
0 0 * * * | 每天 UTC 00:00 |
0 8 * * 1 | 每周一 UTC 08:00 |
0 0 1 * * | 每月 1 日 |
0 9-17 * * 1-5 | 工作日 UTC 09:00 至 17:00,每小时运行 |
目标必须是内部函数
公共函数需要客户端、身份验证信息和不受信任的输入。Cron 和调度器调用不具备这些内容。公共目标会跳过你编写的身份验证检查,并让任何能访问此部署的人都能触发该任务。请使用 internalMutation、internalAction 或 internalQuery 注册目标。
一个分批处理的任务
Mutation 是一个受读写限制的事务。在循环中删除五万行会触及限制。获取固定大小的一批数据,然后使用 runAfter(0, ...) 重新调度自身,让任务链自行完成。
// 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;
},
});
这种方式之所以可行,是因为每次删除都会将该行从索引范围中移除。如果任务更新行,但不会将它们移出范围,请改用分页游标。参阅参考文件。
runAfter 与 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,
});
| 方法 | 用途 |
|---|---|
runAfter(delayMs, fn, args) | 重试、后续任务、“十分钟后” |
runAt(timestamp, fn, args) | 试用期结束、发送日期,以及任何有存储时间的事项 |
两者都会返回一个 Id<"_scheduled_functions">。如果之后可能需要取消任务,请将其存储在文档中。
请记住以下两点:
- 在 mutation 中调度是事务性的。如果 mutation 抛出错误,就不会安排任何任务。在 action 中调度会立即生效,即使 action 随后失败也是如此。
- 已调度的 mutation 只运行一次。已调度的 action 可能失败且不会重试,因此请为 action 添加重试逻辑,或使用重试组件。
在仪表板中查看运行记录
- Schedules、Cron Jobs 标签页:显示
crons.ts中的每个条目,以及上次运行和下次运行时间。 - Schedules、Scheduled Functions 标签页:显示待处理的
runAfter和runAt任务。 - Logs,按函数名称筛选:显示每次执行、执行时长和任何抛出的错误。
- 使用 CLI:
npx convex logs会输出相同的日志行。 - 在代码中:
ctx.db.system.get(jobId)返回任务文档,其state.kind设置为pending、inProgress、success、failed或canceled。
Cron 未触发时
- 文件名必须准确为
convex/crons.ts,并以export default crons结尾。 npx convex dev正在运行,且最近一次推送成功。Cron 变更只有在推送后才会生效。- 目标是
internal.*,且参数与其验证器匹配。不匹配会在推送时失败。 - 表达式使用 UTC。比较之前,请先换算本地时间。
- 检查 Logs 中是否有抛出的错误。每次运行都会抛错的作业,看起来就像从未运行过。
常见错误
| 错误 | 导致的问题 | 应改为 |
|---|---|---|
crons.daily(...) | 已弃用的辅助函数 | crons.cron("...", "0 0 * * *", ...) |
目标是 api.tasks.cleanup | 绕过身份验证,可公开调用 | 注册为 internalMutation,使用 internal.tasks.cleanup |
.collect() 后循环处理数千条记录 | 超出事务限制 | 使用 take(BATCH) 并重新安排 |
.withIndex("by_x").filter(...) | 筛选会扫描整个索引 | 将范围条件放入 withIndex |
在 internalQuery 中使用 Date.now() | 破坏缓存和响应性 | 由调用方将 now 作为参数传入 |
将太平洋时间上午 9 点的 Cron 设为 "0 9 * * *" | 会在 UTC 上午 9 点运行 | 使用 UTC,或每小时运行并检查本地时间 |
runAfter 未使用 await | 作业可能不会被安排 | 始终对 ctx.scheduler.* 使用 await |
| 两个 Cron 使用相同名称 | 推送失败 | 每个文件中的名称都必须唯一 |
检查清单
-
convex/crons.ts只使用crons.interval和crons.cron,并以export default crons结尾 - 每个 Cron 和调度器目标都是
internal.* - 每个目标都有
args和returns验证器 - 处理大量行的作业会分批处理,并通过
runAfter(0, ...)重新安排 - 范围条件写在
withIndex中,而不是.filter中 - 查询中不使用
Date.now() - 每次调用
ctx.scheduler.*都使用await - Cron 小时使用 UTC 编写
- 需要取消或防抖时,将作业 ID 存储在文档中
- 已运行
npx convex dev,并在 Schedules 下看到作业