Cron Jobs
Schedule tasks with cron — interval, daily, weekly, cron
Gencow provides a built-in scheduler for cron jobs. Define job handlers as procedure.internal, attach schedules with defineApi({ crons }), and Gencow runs them automatically.
Setup
Add scheduled jobs in gencow/index.ts:
import { cron, defineApi } from "@gencow/core";
import { procedure } from "./runtime";
export const syncData = procedure.internal
.name("data.sync")
.handler(async ({ context: ctx }) => {
// no user session is required; this runs through trusted scheduler transport
});
export const syncDataCron = cron.name("sync-data").interval({ minutes: 30 }).handler(syncData);
export default defineApi({
procedures: { syncData },
crons: { syncDataCron },
});Critical: Cron-only handlers should be
procedure.internal, notprocedure.mutation. Internal procedures are not exposed on/api/mutationand do not generate frontend mutation hooks.
Scheduling Methods
interval — Run Every N Minutes/Hours
export const syncDataCron = cron.name("syncData").interval({ minutes: 30 }).handler(syncData);
// Runs data.sync internal procedure every 30 minutesOptions: { minutes?: number, hours?: number, seconds?: number }
daily — Run Once Per Day
export const morningReportCron = cron.name("morningReport").daily({ hour: 9, minute: 0 }).handler(morningReport);
// Runs reports.daily internal procedure every day at 09:00Options: { hour: number, minute?: number }
weekly — Run Once Per Week
export const weeklyDigestCron = cron.name("weeklyDigest").weekly({ dayOfWeek: 1, hour: 10 }).handler(weeklyDigest);
// Runs reports.weekly internal procedure every Monday at 10:00Options: { dayOfWeek: number (0=Sun, 1=Mon, ..., 6=Sat), hour: number, minute?: number }
cron — Standard Cron Expression
export const cleanupCron = cron.name("customSchedule").cron("*/15 * * * *").handler(cleanupExpired);
// Standard cron: every 15 minutesInline Handlers
Cloud deployments register only named procedure cron jobs in the platform scheduler. Inline async handlers can run in local app runtime paths, but they are skipped from the cloud cron manifest because functions cannot be safely serialized into the platform control plane.
Instead of referencing an internal procedure, you can pass an inline async function:
export const healthCheckCron = cron.name("healthCheck").interval({ minutes: 5 }).handler(async () => {
const result = await fetch(`${process.env.GENCOW_INTERNAL_URL}/health`);
if (!result.ok) {
console.error("Health check failed!");
}
});Complete Example
// gencow/index.ts
import { cron, defineApi } from "@gencow/core";
import { syncNews, generateDailyReport, generateWeeklySummary, cleanupExpired } from "./jobs";
export const syncNewsCron = cron.name("syncNews").interval({ minutes: 30 }).handler(syncNews);
export const dailyReportCron = cron.name("dailyReport").daily({ hour: 9 }).handler(generateDailyReport);
export const weeklySummaryCron = cron
.name("weeklySummary")
.weekly({ dayOfWeek: 1, hour: 10 })
.handler(generateWeeklySummary);
export const cleanupCron = cron.name("cleanup").cron("*/15 * * * *").handler(cleanupExpired);
export default defineApi({
procedures: { syncNews, generateDailyReport, generateWeeklySummary, cleanupExpired },
crons: { syncNewsCron, dailyReportCron, weeklySummaryCron, cleanupCron },
});The Internal Handler
// gencow/news.ts
import { procedure } from "./runtime";
import { news } from "./schema";
const BATCH_LIMIT = 50;
const AI_FANOUT_BUDGET = 4;
export const syncNews = procedure.internal
.name("news.sync")
.handler(async ({ context: ctx }) => {
const response = await fetch("https://api.example.com/news");
const articles = (await response.json()).slice(0, BATCH_LIMIT);
// Avoid unbounded Promise.all over crawling/AI work. Keep a small
// fanout budget and schedule the next batch when there is more work.
for (let i = 0; i < articles.length; i += AI_FANOUT_BUDGET) {
const chunk = articles.slice(i, i + AI_FANOUT_BUDGET);
await Promise.all(chunk.map(async (article) => {
await ctx.db.insert(news).values({
title: article.title,
url: article.url,
fetchedAt: new Date(),
}).onConflictDoNothing();
}));
}
console.log(`Synced ${articles.length} articles`);
});Resource Guardrails
Cron actions run without a user sitting in front of the app, so they need fixed limits:
- Set a batch limit for every crawler, importer, cleanup, and backfill action.
- Set an AI fanout budget before using parallel model calls. Keep concurrency bounded and schedule the next batch with
ctx.scheduler.runAfter()if there is more work. - Avoid unbounded
Promise.all, recursive self-scheduling loops, and loading thousands of rows into memory at once. - Do not expose cron actions as anonymous public mutations. Use
procedure.internalfor scheduler-only work. - Public mutations should not trigger large writes, crawling, or AI fanout without auth, ownership scope, and rate/credit limits.
Self-Fetch Pattern
When a cron job needs to call its own server's API:
export const healthPingCron = cron.name("healthPing").interval({ minutes: 5 }).handler(async () => {
// Use GENCOW_INTERNAL_URL (auto-injected at server boot)
const url = process.env.GENCOW_INTERNAL_URL;
const res = await fetch(`${url}/api/health`);
console.log("Health:", res.status);
});Note:
GENCOW_INTERNAL_URLis automatically set by the server in both cloud and local modes. Don't hardcode URLs or ports.
Cloud Registration
When you run gencow deploy or cloud dev packaging, Gencow registers your
defineApi({ crons }) schedules with the platform scheduler. Job identities and
transport authentication are managed by the platform and rotate with deploys.
Legacy gencow/crons.ts and cronJobs() schedules are still read during
migration, but new apps should use independent cron defs in
defineApi({ crons }) with procedure.internal handlers.
Viewing Cron Status
- Dashboard: Admin Dashboard → Scheduler tab
- Logs: Cron execution results appear in
gencow devconsole output
Cloud Deployment
When you run gencow deploy or cloud dev packaging, the CLI includes
dependency-free schedule metadata in the deploy bundle. The platform scheduler
uses that metadata to run your registered procedure.internal handlers without
importing your TypeScript file in the control-plane process.
Important Notes
- Register independent
crondefs withdefineApi({ crons }). - Cron-only handlers should be
procedure.internal; user-callable wrappers should use separateprocedure.mutationnames. - Inline async functions are local-runtime only and are skipped from cloud manifests.
- All times are in server timezone (UTC in cloud deployments)
- Cron jobs run even when no users are connected
Next Steps
- Deployment — Deploy your app with cron jobs
- AI Engine — Add AI capabilities