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:

OptionTypeDefault
apiKeystringrequired
baseUrlstringhttps://api.chronos.sh
pollWaitTimeSecondsnumber20
retryDelayMsnumber1000
loggerChronosLoggerConsole logger
fetchFetchLikeglobalThis.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 network error. Must be non-negative

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:

FieldTypeDescription
jobIdstringStable job identifier. Same across retries. Use for idempotency keys in downstream calls.
executionIdstringUnique to this attempt. Use for per-attempt logs, metrics, and correlation.
handlerstringThe handler name that matched this job.
payloadTPayloadJob payload.
scheduledForDateWhen the job was originally scheduled to run.
attemptnumberAttempt number. 1 on first try, increments on retries.
timeoutnumberSoft timeout in seconds. Informational. The SDK does not kill your handler.
schedule{ id, name } | nullThe schedule that produced this job, or null for one-off jobs.

What your handler returns determines the execution outcome:

ReturnEffect
{ ... } (plain object)Recorded as the execution result. Must be JSON-serializable.
undefined / no returnExecution marked completed with no result data.
Throw an errorExecution 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.

ResponsibilitySDKYou
Long-polling for jobsYes
Claiming jobsYes
Reporting results to the APIYes (retries up to 3 times)
Capturing handler errorsYes (truncated to 4KB)
Retry on network errorsYes (waits retryDelayMs, resumes poll)
Handler logicYes
IdempotencyYes (use ctx.jobId for dedup across retries)
Signal handling (SIGTERM/SIGINT)Yes
Hard timeout enforcementYes (if needed)
Process lifecycleYes
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();