Skip to content

Worker Setup

Terminal window
pnpm add @chronos.sh/sdk

Requires Node.js 20+. The SDK has zero runtime dependencies.

import { Chronos } from '@chronos.sh/sdk';
const chronos = new Chronos({
apiKey: process.env.CHRONOS_API_KEY!,
});

All options:

Option Type Default
apiKey string required
baseUrl string https://api.chronos.sh
pollWaitTimeSeconds number 20
retryDelayMs number 1000
logger ChronosLogger Console logger
fetch FetchLike globalThis.fetch
  • apiKey starts with chrns_
  • baseUrl is stripped of trailing slashes. Override for local development or proxying
  • pollWaitTimeSeconds must be an integer 0–20
  • retryDelayMs is the delay between poll retries on error (network failures, API errors). Must be non-negative. On 429 responses, the Retry-After header overrides this value

The logger interface expects (message, meta?): message first, metadata second:

const chronos = new Chronos({
apiKey: process.env.CHRONOS_API_KEY!,
logger: {
debug(message, meta) { /* ... */ },
info(message, meta) { /* ... */ },
warn(message, meta) { /* ... */ },
error(message, meta) { /* ... */ },
},
});

A handler is a function that processes one job type, identified by name:

chronos.worker.handle('sync-tenant', async (ctx) => {
await syncTenant(ctx.payload.tenantId);
return { synced: true };
});

The handler name must match the handler field on your schedules or jobs. That’s how Chronos routes work to the right function.

Register as many as you need. Calls are chainable:

chronos.worker
.handle('sync-tenant', syncTenantHandler)
.handle('send-report', sendReportHandler)
.handle('expire-trial', expireTrialHandler);

Handler names must be 1-255 characters. Duplicate names throw immediately at registration time.

Use the generic parameter on handle to type the payload:

type SyncPayload = { tenantId: string; full: boolean };
chronos.worker.handle<SyncPayload>('sync-tenant', async (ctx) => {
// ctx.payload is typed as SyncPayload
const { tenantId, full } = ctx.payload;
await syncTenant(tenantId, { full });
});

Every handler receives a ChronosContext:

Field Type Description
jobId string Stable job identifier. Same across retries. Use for idempotency keys in downstream calls.
executionId string Unique to this attempt. Use for per-attempt logs, metrics, and correlation.
handler string The handler name that matched this job.
payload TPayload Job payload.
scheduledFor Date When the job was originally scheduled to run.
attempt number Attempt number. 1 on first try, increments on retries.
timeout number Timeout for the handler in seconds. The SDK races the handler against this duration and aborts ctx.signal when it elapses.
signal AbortSignal Fires when timeout elapses. Pass to fetch, AbortSignal.any(), etc.
schedule { id, name } | null The schedule that produced this job, or null for one-off jobs.

What your handler returns determines the execution outcome:

Return Effect
{ ... } (plain object) Recorded as the execution result. Must be JSON-serializable.
undefined / no return Execution marked completed with no result data.
Throw an error Execution marked failed. Error message captured (truncated to 4KB). Retries if attempts remain.

Arrays, class instances, and non-JSON-serializable values are rejected at runtime.

await chronos.worker.start();

The returned promise resolves only when stop() is called and any in-flight work completes. This means your process stays alive as long as the worker is running.

Constraints:

  • Register at least one handler before calling start(), or it throws
  • Calling start() twice throws. A worker instance runs one loop at a time
const shutdown = async () => {
await chronos.worker.stop();
process.exit(0);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);

When stop() is called:

  1. The current long-poll is aborted immediately (no waiting for the poll to time out)
  2. If a handler is mid-execution, it runs to completion and reports its result
  3. The promise returned by start() resolves

This preserves at-least-once delivery: a job claimed is a job completed (or explicitly failed), never silently dropped.

Responsibility SDK You
Long-polling for jobs Yes
Claiming jobs Yes
Reporting results to the API Yes (retries up to 3 times)
Capturing handler errors Yes (truncated to 4KB)
Retry on network errors Yes (waits retryDelayMs, resumes poll)
Rate-limit backoff Yes (honors Retry-After header on 429)
Handler logic Yes
Idempotency Yes (use ctx.jobId for dedup across retries)
Signal handling (SIGTERM/SIGINT) Yes
Timeout abort signal Yes (fires ctx.signal when timeout elapses) Honor ctx.signal in your handler
Process lifecycle Yes
worker.ts
import { Chronos } from '@chronos.sh/sdk';
type SyncPayload = { tenantId: string; full: boolean };
type ReportPayload = { reportId: string; format: 'pdf' | 'csv' };
const chronos = new Chronos({
apiKey: process.env.CHRONOS_API_KEY!,
});
chronos.worker
.handle<SyncPayload>('sync-tenant', async (ctx) => {
const result = await syncTenant(ctx.payload.tenantId, {
full: ctx.payload.full,
});
return { records: result.count };
})
.handle<ReportPayload>('generate-report', async (ctx) => {
const url = await generateReport(ctx.payload.reportId, ctx.payload.format);
return { url };
});
const shutdown = async () => {
await chronos.worker.stop();
process.exit(0);
};
process.on('SIGTERM', shutdown);
process.on('SIGINT', shutdown);
await chronos.worker.start();