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 inappDir/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 name | Task name (for ctx.schedule) |
|---|---|
cleanup.cron.ts | cleanup |
intervalExample.cron.ts | interval.example |
reportDaily.cron.ts | report.daily |
What to export
Each cron file must export:
schedule— AScheduleDef(or array ofScheduleDef) that describes when the task runs. See Schedule definitions below.run— A function(ctx: TaskContext) => void \| Promise<void>that runs when the schedule fires (or when you callctx.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:
| Definition | Meaning |
|---|---|
{ 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
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:
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 viactx.schedule(name, payload)from a route. When the task runs on its schedule,payloadmay 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).
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 therunfunction fromappDir/cron/reportDaily.cron.tswithctx.payloadset 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:
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 samerunwithctx.payloadset.
Route — triggers the same task on demand:
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.
cleanup→cleanupintervalExample→interval.examplereportDaily→report.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
| Topic | Detail |
|---|---|
| Location | Only appDir/cron/. No subfolders. |
| File names | *.cron.ts (or .js, .mts, .mjs). |
| Task name | From filename: camelCase → dot.case (e.g. reportDaily → report.daily). |
| Exports | schedule: ScheduleDef or ScheduleDef[]; run: (ctx: TaskContext) => void | Promise<void>. |
| ScheduleDef | `every: "minute" |
| Trigger from route | ctx.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.