Skip to main content
Scheduled tasks run without an HTTP request, but under the same tracing, cost-governor, and guardrail machinery as request-driven routes — background work isn’t a governance blind spot.

Defining a scheduled task

Create a schedule.ts file anywhere under a background-tasks directory (conventionally server/schedule/):
server/schedule/nightly-report/schedule.ts
defineSchedule is an identity function — it exists purely for type inference and discovery, the same pattern as defineAgent.
string
required
Unique task name. Defaults to the folder path if omitted and the task is loaded via loadBackgroundTasks.
string
required
A 5-field cron expression (minute hour day-of-month month day-of-week). Supports *, comma lists (1,15), ranges (9-17), and step values (*/15).
(ctx: TaskContext) => unknown | Promise<unknown>
required
The task body. Receives a TaskContext with now, payload (unused for scheduled tasks), and trace.

Running the scheduler

loadBackgroundTasks(dir) walks the directory tree, imports every schedule.ts file’s default export, and derives each task’s name from its folder path if name wasn’t set explicitly.

Scheduler API

(task: ScheduledTask) => this
Register a task. Chainable.
(now: Date) => Promise<string[]>
Run every task due at now. Idempotent per calendar minute — calling tick twice within the same minute only fires each task once. Returns the names of the tasks that ran.
(intervalMs?: number) => () => void
Poll on an interval, default 60_000ms. Returns a stop function that clears the interval.
Each task run is wrapped in a trace span named schedule <name>, tagged with the task’s cron expression. A task that throws is caught, the span closes with status error, and the failure is passed to onError (default: console.error) — one failing task never stops the scheduler or affects other tasks.
Scheduler runs in-process on a setInterval — it fires as long as your server process is alive. For multi-instance deployments, either run the scheduler on a single dedicated instance, or add your own leader-election/locking around tick() so the same task doesn’t fire once per instance.

Testing a schedule without waiting

Call tick() directly with a specific Date to simulate a firing time in tests, instead of waiting for the real clock:

See also

  • Triggered agents — the event-driven counterpart, fired by webhooks and queue messages instead of a clock
  • Observability — every scheduled run appears in the same trace dashboard as request-driven agents
  • Durability & safety — checkpoint long-running scheduled agent loops so a crash mid-run resumes cleanly
Scheduled tasks run without an HTTP request, but under the same tracing, cost-governor, and guardrail machinery as request-driven routes — background work isn’t a governance blind spot.

Defining a scheduled task

Create a schedule.ts file anywhere under a background-tasks directory (conventionally server/schedule/):
server/schedule/nightly-report/schedule.ts
defineSchedule is an identity function — it exists purely for type inference and discovery, the same pattern as defineAgent.
string
required
Unique task name. Defaults to the folder path if omitted and the task is loaded via loadBackgroundTasks.
string
required
A 5-field cron expression (minute hour day-of-month month day-of-week). Supports *, comma lists (1,15), ranges (9-17), and step values (*/15).
(ctx: TaskContext) => unknown | Promise<unknown>
required
The task body. Receives a TaskContext with now, payload (unused for scheduled tasks), and trace.

Running the scheduler

loadBackgroundTasks(dir) walks the directory tree, imports every schedule.ts file’s default export, and derives each task’s name from its folder path if name wasn’t set explicitly.

Scheduler API

(task: ScheduledTask) => this
Register a task. Chainable.
(now: Date) => Promise<string[]>
Run every task due at now. Idempotent per calendar minute — calling tick twice within the same minute only fires each task once. Returns the names of the tasks that ran.
(intervalMs?: number) => () => void
Poll on an interval, default 60_000ms. Returns a stop function that clears the interval.
Each task run is wrapped in a trace span named schedule <name>, tagged with the task’s cron expression. A task that throws is caught, the span closes with status error, and the failure is passed to onError (default: console.error) — one failing task never stops the scheduler or affects other tasks.
Scheduler runs in-process on a setInterval — it fires as long as your server process is alive. For multi-instance deployments, either run the scheduler on a single dedicated instance, or add your own leader-election/locking around tick() so the same task doesn’t fire once per instance.

Testing a schedule without waiting

Call tick() directly with a specific Date to simulate a firing time in tests, instead of waiting for the real clock:

See also

  • Triggered agents — the event-driven counterpart, fired by webhooks and queue messages instead of a clock
  • Observability — every scheduled run appears in the same trace dashboard as request-driven agents
  • Durability & safety — checkpoint long-running scheduled agent loops so a crash mid-run resumes cleanly