Cron
Custom adapter
Wrap any scheduler in VitNode's one-method CronAdapter interface - a plain setInterval, a cron library, or a platform trigger.
A cron adapter is the thinnest interface in VitNode: one method, no return value,
called once while the API boots. Its whole job is to call handleCronJobs() on a
clock. Everything else - which jobs exist, which are due, what gets logged - is
the cron endpoint's business, not the adapter's.
Write one when the Node CRON adapter is not the scheduler you want: you already run a job runner, you need the tick to carry a trace header, or the platform hands you a trigger of its own.
Quick start
The shortest adapter that works is a timer:
import { type CronAdapter, handleCronJobs } from '@vitnode/core/api/lib/cron'
export const IntervalCronAdapter = (): CronAdapter => ({
schedule() {
setInterval(() => {
void handleCronJobs()
}, 60_000)
},
})import { buildApiConfig } from '@vitnode/core/vitnode.config'
import { IntervalCronAdapter } from './adapters/interval-cron.adapter'
export const vitNodeApiConfig = buildApiConfig({
cron: IntervalCronAdapter(),
// ...
})That is a complete, working adapter. The rest of this page is about doing it with a real scheduler, and about the ways it can go quietly wrong.
The interface
export interface CronAdapter {
schedule: () => void
}Prop
Type
handleCronJobs() comes from the same module and takes no arguments. It posts to
/api/@vitnode/core/cron on the origin in VITNODE_API_URL (falling back to
http://localhost:3000) with Content-Type: application/json and
Authorization: Bearer $CRON_SECRET. It never rejects once the URL is valid. A
non-2xx response or a network error is printed to the console as
[VitNode] Cron tick failed: <reason>, so your adapter needs no catch. After
55 seconds it stops waiting for the response, while the API keeps running the
jobs.
It is a convenience, not a contract. An adapter whose scheduler lives outside
the process - a platform trigger, a separate worker - can leave schedule()
empty and let that scheduler call the endpoint
directly. The adapter still earns its place: setting
cron is what makes the AdminCP report a scheduler as configured.
Build one with a library
Write the adapter
Keep it in your app rather than in a plugin - a cron adapter is a deployment decision, and it is registered from the API config, which only the host has.
The official package is the reference implementation, and it is short enough to read in one go:
import { type CronAdapter, handleCronJobs } from '@vitnode/core/api/lib/cron'
import { schedule } from 'node-cron'
export const NodeCronAdapter = (): CronAdapter => {
return {
schedule() {
schedule(
'*/1 * * * *',
async () => {
await handleCronJobs()
},
// Skip a tick while the previous one is still waiting on the API.
{ noOverlap: true },
)
},
}
}Give the factory options if your scheduler has any worth exposing - a tick
interval, a timezone, a logger. CronAdapter says nothing about the factory, so
its signature is yours.
Register it in the API config
import { buildApiConfig } from '@vitnode/core/vitnode.config'
import { NodeCronAdapter } from './adapters/node-cron.adapter'
export const vitNodeApiConfig = buildApiConfig({
cron: NodeCronAdapter(),
// ...
})Restart and verify the first tick
schedule() runs at boot, so a running process never picks up the change.
bun devOpen Debug Panel ā Cron & queue (/admin/core/debug/cron) and watch the
process-queue row. It is scheduled * * * * *, so a healthy adapter keeps its
last run under a minute old. If the Time column keeps saying Last Never,
your schedule() ran but your tick is not landing. The server console then shows
[VitNode] Cron tick failed: ... with the status. Check the origin, then the
secret, then whether the API can reach itself.
Gotchas
VitNode calls schedule() synchronously while building the application and
does not wrap it. An adapter that validates its own options by throwing turns
a bad cron expression into a server that will not start. Validate, log, and
return instead.
There is no stop and no teardown hook, and a dev server that re-evaluates
its server modules can build the API again. VitNode's TanStack Start app parks
the API instance on globalThis for exactly this reason - if yours does not,
an adapter that keeps a timer should be able to notice it already has one
instead of stacking a second.
The endpoint runs what is due when it is called, so the tick interval is the
floor on every job's resolution - but it is not a speed control. A tick every
ten seconds on jobs that are all */5 * * * * is five wasted requests a
minute, and a tick every five minutes silently downgrades * * * * * jobs,
queue draining included.
Each due run is claimed once, but nothing waits for a running job to finish. If your scheduler fires again before a slow handler is done, its next run can start alongside it. The operational rules cover what to do about it.
Related
Declare a cron job
buildCron, where jobs are registered, and the operational rules that come with a scheduler.
Node CRON adapter
The official in-process adapter, if a one-minute timer is all you needed.
Cron over REST
The endpoint every adapter posts: its auth, its responses and a curl call.
Queue tasks
One-off background work with retries, drained by the process-queue job.