@chronos.sh/sdk
Official Node.js SDK for Chronos — reliable job scheduling as a service.
import { Chronos } from '@chronos.sh/sdk';
const chronos = new Chronos({ apiKey: process.env.CHRONOS_API_KEY! });
chronos.worker.handle('send-email', async (ctx) => {
await sendEmail(ctx.payload);
return { sent: true };
});
await chronos.worker.start();
Zero dependencies. Runs anywhere with fetch — Node.js 20+, Bun, Deno.
Install
npm install @chronos.sh/sdk
Quickstart
import { Chronos } from '@chronos.sh/sdk';
const chronos = new Chronos({ apiKey: process.env.CHRONOS_API_KEY! });
chronos.worker.handle<{ to: string }>('send-email', async (ctx) => {
await sendEmail(ctx.payload.to);
return { sent: true };
});
await chronos.worker.start();
worker.start() long-polls for jobs and dispatches them to registered handlers. It runs until you call worker.stop(). If any request gets a 401, the worker stops and the returned promise rejects with ChronosAuthenticationError, so a misconfigured or revoked key surfaces immediately instead of retrying forever. A restricted key needs the workers:run scope to run a worker. Without it, the worker stops and the returned promise rejects with a ChronosApiError whose code is insufficient_scope. Worker routes accept API keys only. Passing another credential as apiKey, such as an OAuth token, stops the worker the same way, with code set to credential_not_accepted.
Create schedules and jobs
The same client manages schedules, jobs, executions and monitors, and reads the API key's workspace:
await chronos.schedules.create({
name: 'Nightly report',
handler: 'send-report',
cron: '0 2 * * *',
timezone: 'Europe/London',
});
await chronos.jobs.create({
name: 'Welcome email',
handler: 'send-email',
delay: { value: 10, unit: 'minute' },
payload: { to: '[email protected]' },
});
for await (const job of chronos.jobs.iterate({ status: 'failed' })) {
console.log(job.name, job.last_error);
}
Fields use the API's snake_case names. list() returns one page as { data, paging }, and iterate() walks every page. Reads retry on network errors, timeouts, 429 and 5xx responses. Writes retry only on 429, so a lost response never sends a create twice. A response body larger than 64 MiB is never retried. Set maxRequestRetries to change the retry count.
Monitor a job that runs elsewhere
A monitor watches a job that runs outside Chronos, such as a cron job on your own server. Create one and give its ping_url to the job:
const monitor = await chronos.monitors.create({
name: 'Nightly backup',
cron: '0 2 * * *',
});
monitor.ping_url is a secret. Keep it out of logs, and store it where the job reads its other credentials.
The job reports with one HTTP request and needs no SDK: curl -fsS "$PING_URL" on success, or curl -fsS "$PING_URL/fail" on failure. listPings() and iteratePings() read what it reported.
Chronos alerts when the job reports a failure, or when no success or fail ping arrives within grace_seconds (default 300) of when it was due.
Check the API key
To check an API key at startup, call account.get(). It returns the workspace the key belongs to, and throws ChronosAuthenticationError if the key is unknown or revoked:
const workspace = await chronos.account.get();
console.log(`Connected to ${workspace.name}`);
Wait for a job
To block until a job finishes, call jobs.wait(). It waits a second between checks and returns the job once it is completed, failed, cancelled or skipped. A failed job is returned, not thrown. After 60 seconds it throws ChronosWaitTimeoutError, whose job holds the last state seen. It rethrows any other error from a check, such as ChronosApiError for an unknown id. To change either default, set pollIntervalMs or maxWaitMs:
const created = await chronos.jobs.create({ name: 'Welcome email', handler: 'send-email' });
const job = await chronos.jobs.wait(created.id, { maxWaitMs: 120_000 });
if (job.status === 'failed') {
console.error(job.last_error);
}
A pull job finishes only while a worker with its handler is running.
Graceful shutdown
The SDK does not install signal handlers — your application owns its process lifecycle. Wire SIGTERM/SIGINT to worker.stop() so in-flight jobs and their result reports finish before the process exits:
let stopping = false;
for (const sig of ['SIGTERM', 'SIGINT'] as const) {
process.on(sig, () => {
if (stopping) process.exit(1);
stopping = true;
chronos.worker.stop().then(() => process.exit(143));
});
}
A second signal force-exits as an escape hatch. This matters for any long-running worker: standalone scripts, Docker containers, systemd services, PM2, cloud VMs, or CI runners.