Worker Setup
Installation
Section titled “Installation”pnpm add @chronos.sh/sdknpm install @chronos.sh/sdkyarn add @chronos.sh/sdkRequires Node.js 20+. The SDK has zero runtime dependencies.
Configuration
Section titled “Configuration”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 |
apiKeystarts withchrns_baseUrlis stripped of trailing slashes. Override for local development or proxyingpollWaitTimeSecondsmust be an integer 0–20retryDelayMsis the delay between poll retries on network error. Must be non-negative
Custom logger
Section titled “Custom logger”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) { /* ... */ }, },});Registering handlers
Section titled “Registering handlers”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.
Multiple handlers
Section titled “Multiple handlers”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.
Type-safe payloads
Section titled “Type-safe payloads”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 });});The context object
Section titled “The context object”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 | Soft timeout in seconds. Informational. The SDK does not kill your handler. |
schedule | { id, name } | null | The schedule that produced this job, or null for one-off jobs. |
Return values
Section titled “Return values”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.
Starting the worker
Section titled “Starting the worker”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
Graceful shutdown
Section titled “Graceful shutdown”const shutdown = async () => { await chronos.worker.stop(); process.exit(0);};
process.on('SIGTERM', shutdown);process.on('SIGINT', shutdown);When stop() is called:
- The current long-poll is aborted immediately (no waiting for the poll to time out)
- If a handler is mid-execution, it runs to completion and reports its result
- 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.
What the SDK handles vs what you handle
Section titled “What the SDK handles vs what you handle”| 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) | — |
| Handler logic | — | Yes |
| Idempotency | — | Yes (use ctx.jobId for dedup across retries) |
| Signal handling (SIGTERM/SIGINT) | — | Yes |
| Hard timeout enforcement | — | Yes (if needed) |
| Process lifecycle | — | Yes |
Complete production example
Section titled “Complete production example”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();