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, not procedure.mutation. Internal procedures are not exposed on /api/mutation and 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 minutes

Options: { 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:00

Options: { 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:00

Options: { 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 minutes

Inline 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.internal for 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_URL is 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 dev console 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

  1. Register independent cron defs with defineApi({ crons }).
  2. Cron-only handlers should be procedure.internal; user-callable wrappers should use separate procedure.mutation names.
  3. Inline async functions are local-runtime only and are skipped from cloud manifests.
  4. All times are in server timezone (UTC in cloud deployments)
  5. Cron jobs run even when no users are connected

Next Steps