Development Guide

Cron Jobs

Scheduled tasks in appDir/cron, file naming, schedule and run exports, ScheduleDef, and triggering from routes with ctx.schedule.

Cron Jobs

Legonode can run code on a schedule (e.g. every minute, daily at midnight) or on demand from a route via ctx.schedule(name, payload). Schedule definitions live in appDir/cron/ as *.cron.ts files. This guide covers file naming, how task names are derived, what to export, and how to define schedules and run logic.


Where cron jobs live

  • Directory: Only appDir/cron/ is scanned for scheduled tasks. No subfolders are scanned; all task files sit directly in appDir/cron/.
  • File names: *.cron.ts, *.cron.js, *.cron.mts, *.cron.mjs

Each file defines one task. The task name (used when you call ctx.schedule(name, payload)) is derived from the filename (before the .cron.* suffix) by converting camelCase to dot.case:

File nameTask name (for ctx.schedule)
cleanup.cron.tscleanup
intervalExample.cron.tsinterval.example
reportDaily.cron.tsreport.daily

What to export

Each cron file must export:

  1. schedule — A ScheduleDef (or array of ScheduleDef) that describes when the task runs. See Schedule definitions below.
  2. run — A function (ctx: TaskContext) => void \| Promise<void> that runs when the schedule fires (or when you call ctx.schedule(name, payload)).

Optional: name — Override the task name (default is derived from the filename as above).


Schedule definitions

ScheduleDef is a JSON-friendly object. Supported shapes:

DefinitionMeaning
{ every: "minute" }Every minute
{ every: "hour" }Every hour
{ every: "day", at?: string }Every day; at is time (e.g. "00:00", "09:30")
{ every: "week", day?: string, at?: string }Every week; optional day and time
{ every: "month", date?: number, at?: string }Every month; optional date (1–31) and time
{ every: string }Interval: "5m", "10s", "1h", etc.

You can export a single ScheduleDef or an array of them if the same run should run on multiple schedules.

Example: daily cleanup at midnight

appDir/cron/cleanup.cron.ts
import type { ScheduleDef, TaskContext } from "legonode";

export const schedule: ScheduleDef = {
  every: "day",
  at: "00:00"
};

export async function run(ctx: TaskContext) {
  console.log("[cron] cleanup: running daily at 00:00");
  // e.g. delete old sessions, prune logs
  void ctx;
}

Example: interval (every 2 seconds) — useful for dev or frequent jobs:

appDir/cron/intervalExample.cron.ts
import type { TaskContext } from "legonode";

export const schedule = {
  every: "2s"
};

export async function run(ctx: TaskContext) {
  console.log("[cron] intervalExample: every 2s");
  void ctx;
}

TaskContext

The run function receives ctx: TaskContext:

  • payload — Set when the task is triggered manually via ctx.schedule(name, payload) from a route. When the task runs on its schedule, payload may be undefined.
  • services — Optional; reserved for app-specific dependencies (e.g. db, reportService) if you pass them when building the runner.

Triggering a task from a route

From any route, call ctx.schedule(taskName, payload) to run the same run function that the scheduler would run. The task name must match the dot.case name (e.g. interval.example for intervalExample.cron.ts).

router/api/admin/runReport/route.ts
import type { Context } from "legonode";

export default async function POST(ctx: Context) {
  ctx.schedule("report.daily", { triggeredBy: "admin", at: new Date().toISOString() });
  ctx.res.status(202).json({ message: "Report job scheduled" });
}
  • ctx.schedule("report.daily", payload) — Invokes the run function from appDir/cron/reportDaily.cron.ts with ctx.payload set to the object you passed. The route returns immediately; the task runs asynchronously.

Use this for “run this job now” or “run this job with this payload” without waiting for the next cron tick.


Full example: daily report and manual trigger

Cron file — runs on a schedule and can be triggered by name:

appDir/cron/reportDaily.cron.ts
import type { ScheduleDef, TaskContext } from "legonode";

export const schedule: ScheduleDef = {
  every: "day",
  at: "09:00"
};

export async function run(ctx: TaskContext) {
  const payload = ctx.payload as { triggeredBy?: string; at?: string } | undefined;
  console.log("[cron] reportDaily", payload ?? "scheduled run");
  // Generate report, send email, etc.
}
  • Scheduled: Every day at 09:00 the runner calls run(ctx) with no payload.
  • Manual: From a route, ctx.schedule("report.daily", { triggeredBy: "admin" }) calls the same run with ctx.payload set.

Route — triggers the same task on demand:

router/api/admin/runReport/route.ts
import type { Context } from "legonode";

export default async function POST(ctx: Context) {
  ctx.schedule("report.daily", { triggeredBy: "admin" });
  ctx.res.status(202).json({ ok: true });
}

Task name from filename

Same rule as events: camelCase → dot.case.

  • cleanupcleanup
  • intervalExampleinterval.example
  • reportDailyreport.daily

Use this name when calling ctx.schedule("task.name", payload). If no handler is found, the framework logs a warning and does nothing.


Summary

TopicDetail
LocationOnly appDir/cron/. No subfolders.
File names*.cron.ts (or .js, .mts, .mjs).
Task nameFrom filename: camelCase → dot.case (e.g. reportDailyreport.daily).
Exportsschedule: ScheduleDef or ScheduleDef[]; run: (ctx: TaskContext) => void | Promise<void>.
ScheduleDef`every: "minute"
Trigger from routectx.schedule("task.name", payload) runs the same run with ctx.payload set.

For events (fire-and-forget from routes), see Events. For route structure, see Folder Structure.