convex-cron-jobs について
Convex 開発者向けのスケジューリングリファレンスです。定期 Cron、一回限りのジョブ、バッチ処理、実行状況の確認を扱い、internal 関数の指定、UTC 時刻、再試行の規則も説明します。
主な機能
- 01定期 Cron と一回限りのジョブ
- 02ジョブ対象としての internal 関数
- 03バッチ処理と再スケジュール
- 04相対時刻・絶対時刻でのスケジュール
- 05ジョブ状態とログの確認
- 06タイムゾーン、キャンセル、再試行
活用シーン
- 01一定間隔または日時指定での処理実行
- 02コミット後の後続処理のスケジュール
- 03大量レコードの分割処理
- 04Cron の未実行や時刻ずれの調査
インストール
コマンドをコピーしてターミナルで実行します。
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 分ごと、または決まった時刻に処理を実行する必要がある
- ミューテーションのコミット後に、後続処理を開始する必要がある
- 1 回のミューテーションで処理するには行数が多いジョブがある
- ダッシュボードに cron が表示されているのに実行されない、または時刻がずれている
- 保留中のジョブをキャンセル、デバウンス、または確認する必要がある
詳しい内容は、次の 2 つのリファレンスファイルを参照してください。
- references/scheduling-patterns.md:バックオフ付きの再試行、ジョブのキャンセル、
_scheduled_functionsの読み取り、デバウンス、またはローカルのタイムゾーンが必要な場合に参照します。 - references/cron-recipes.md:日次ダイジェストメール、セッションの有効期限切れ、統計情報の集計、外部 API 同期など、短くコピーして使えるジョブを参照します。
crons.ts のひな形
ファイルは 1 つで、デフォルトエクスポートも 1 つです。「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;
各エントリのルール:
- 名前はファイル内で一意にします。ダッシュボードには、この名前でジョブが表示されます。
- 対象が
crons.tsで定義されている場合でも、./_generated/apiからinternalをインポートします。 - 引数は静的で、対象関数の
argsバリデーターを満たす必要があります。 - インターバルの単位は
{ seconds }、{ minutes }、または{ hours }です。
Cron 式のクイックリファレンス(分 時 日 月 曜日):
| 式 | 実行タイミング |
|---|---|
*/15 * * * * | 15 分ごと |
0 * * * * | 毎時 00 分 |
0 0 * * * | 毎日 00:00 UTC |
0 8 * * 1 | 毎週月曜日 08:00 UTC |
0 0 1 * * | 毎月 1 日 |
0 9-17 * * 1-5 | 平日の 09:00 から 17:00 UTC まで、毎時 |
対象は内部関数にする
公開関数は、クライアント、認証 ID、信頼できない入力を前提としています。Cron とスケジューラーから呼び出す場合、そのいずれもありません。公開関数を対象にすると、記述した認証チェックがスキップされ、デプロイメントにアクセスできる人なら誰でもジョブを実行できるようになります。対象は internalMutation、internalAction、または internalQuery で登録してください。
バッチ処理のジョブ
ミューテーションは、読み取りと書き込みの上限がある 1 つのトランザクションです。ループ内で 5 万件の行を削除すると、上限に達します。固定件数を処理し、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) | 再試行、後続処理、「10 分後」 |
runAt(timestamp, fn, args) | トライアルの終了、送信日、保存済みの時刻に関する処理 |
どちらも Id<"_scheduled_functions"> を返します。キャンセルする可能性がある場合は、ドキュメントに保存してください。
覚えておくべき動作が 2 つあります。
- ミューテーション内でのスケジューリングはトランザクションに含まれます。ミューテーションがエラーを投げると、何もスケジュールされません。アクション内でのスケジューリングはすぐに実行され、後からアクションが失敗しても取り消されません。
- スケジュールされたミューテーションは 1 回だけ実行されます。スケジュールされたアクションは再試行なしで失敗することがあるため、アクションに再試行ロジックを追加するか、再試行コンポーネントを使ってください。
ダッシュボードで実行状況を確認する
- 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 を使用するか、1 時間ごとに実行して現地時刻を確認します |
runAfter に await を付け忘れる | ジョブがスケジュールされない場合があります | 常に ctx.scheduler.* を await します |
| 同じ名前の cron が 2 つある | プッシュに失敗します | ファイル内で名前が重複しないようにします |
チェックリスト
-
convex/crons.tsではcrons.intervalとcrons.cronのみを使用し、末尾がexport default cronsになっている - すべての cron とスケジューラーの対象が
internal.*になっている - すべての対象に
argsとreturnsのバリデーターがある - 多数の行にアクセスするジョブは、バッチ処理を行い、
runAfter(0, ...)で再スケジュールする - 範囲条件は
.filterではなくwithIndexに指定する - クエリ内で
Date.now()を使用していない - すべての
ctx.scheduler.*呼び出しをawaitしている - cron の時刻を UTC で指定している
- キャンセルまたはデバウンスが必要な場合、ジョブ ID をドキュメントに保存している
-
npx convex devを実行し、Schedules にジョブが表示されることを確認した