Skip to content

Scheduling

ZinTrust includes a lightweight, flexible schedule runner built directly into the framework. It handles recurrent background tasks, cron jobs, and interval-based execution without requiring external daemon processes.

The scheduler supports:

  • Registering schedules by name
  • Fixed interval execution (intervalMs)
  • Cron expressions with minute-resolution
  • Timezone-aware cron evaluation (IANA timezone strings)
  • Jitter (jitterMs) to spread execution loads
  • Failure backoff scheduling
  • Optional execution on process start
  • In-process overlap prevention (prevents concurrent runs in the same process)
  • Distributed locking (withoutOverlapping()) across multiple instances
  • Manual invocation

Location and Registration

Project schedules live in your application space. You export them from a central entry file at app/Schedules/index.ts.

Note: If a schedule defined in your application uses the same name as a built-in core schedule, the application schedule will override the core schedule.

Use the Schedule builder imported from @zintrust/core to define your tasks. In app/Schedules/index.ts you typically begin with:

ts
import { Logger, Schedule } from '@zintrust/core';

Available Builder Methods

  • Schedule.define(name, handler): Starts the definition of a new schedule.
  • everyMinute() / everyMinutes(n): Run on a fixed minute cadence.
  • everyHour() / everyHours(n): Run on a fixed hourly cadence.
  • intervalMs(ms): Run on a strictly defined millisecond interval.
  • cron(expr, { timezone? }): Run on a standard 5-field cron schedule.
  • timezone(tz): Set the timezone for the execution.
  • jitterMs(ms): Adds a random delay between 0 and ms before running, spreading out load.
  • backoff({ initialMs, maxMs, factor? }): Exponentially slows down retries if the schedule throws an error.
  • leaderOnly(): Only runs if the current instance holds the schedule leader lease.
  • enabledWhen(bool): Conditionally enables or disables the schedule.
  • runOnStart(): Runs immediately when the application boots, and then follows its standard schedule.
  • withoutOverlapping({ provider, ttlMs }): Prevents overlapping runs across multiple server instances using a distributed lock.

Enabling the Scheduler

Schedules require two things to run: the SCHEDULES_ENABLED environment variable set to true, and an explicit zin schedule:start command. The two are kept separate by design — zin s runs the HTTP server and zin schedule:start runs the schedule daemon as its own long-running process.

bash
# Terminal 1 — HTTP server
zin s

# Terminal 2 (or a separate container/process) — schedule daemon
zin schedule:start

Set the environment variable to allow zin schedule:start to proceed:

env
SCHEDULES_ENABLED=true

When SCHEDULES_ENABLED=false (the default):

  • zin schedule:start exits immediately without starting any timers.
  • Schedules are still registered and can still be triggered manually via the CLI (zin schedule:run) or the HTTP RPC gateway (POST /api/_sys/schedule/rpc).
  • zin schedule:list continues to work.

When SCHEDULES_ENABLED=true:

  • zin schedule:start proceeds to register and run all schedules on their defined cadence.
  • The daemon stays alive until it receives SIGTERM or SIGINT, then shuts down gracefully.
  • Only supported in long-running runtimes: Node.js and Fargate. Cloudflare Workers do not support zin schedule:start — use the HTTP RPC gateway instead.

Typical production setup: Keep SCHEDULES_ENABLED=false in your web/API process and set SCHEDULES_ENABLED=true only in a dedicated schedules container. The generated docker-compose.schedules.yml already reflects this split.


Examples

Below are practical examples you can drop into individual files (e.g., app/Schedules/MySchedule.ts) and then export from app/Schedules/index.ts.

1) Every minute (cron, UTC)

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.everyMinute', async () => {
  Logger.info('demo.everyMinute fired', { at: new Date().toISOString() });
})
  .cron('* * * * *', { timezone: 'UTC' })
  .build();

2) Every 5 minutes (cron)

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.every5Minutes', async () => {
  Logger.info('demo.every5Minutes fired');
})
  .cron('*/5 * * * *', { timezone: 'UTC' })
  .build();

3) Daily at midnight (timezone-aware)

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.midnightNy', async () => {
  Logger.info('demo.midnightNy fired');
})
  .cron('0 0 * * *', { timezone: 'America/New_York' })
  .build();

4) Weekdays at 09:30 (Mon–Fri)

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.weekdays0930', async () => {
  Logger.info('demo.weekdays0930 fired');
})
  .cron('30 9 * * 1-5', { timezone: 'UTC' })
  .build();

5) Interval scheduling (every 10 minutes)

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.every10MinInterval', async () => {
  Logger.info('demo.every10MinInterval fired');
})
  .everyMinutes(10)
  .build();

6) Run on process start (then continue on interval)

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.runOnStartThenHourly', async () => {
  Logger.info('demo.runOnStartThenHourly fired');
})
  .runOnStart()
  .everyHour()
  .build();

7) Add jitter (spread load)

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.jitteredCron', async () => {
  Logger.info('demo.jitteredCron fired');
})
  .cron('*/1 * * * *', { timezone: 'UTC' })
  .jitterMs(15_000) // add 0..15s random delay to each run
  .build();

8) Backoff on failure (retry slower when failing)

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.backoffOnFailure', async () => {
  Logger.info('demo.backoffOnFailure fired');
  // throw new Error('simulate failure');
})
  .everyMinute()
  .backoff({ initialMs: 5_000, maxMs: 60_000, factor: 2 })
  .build();

9) Prevent overlap across instances (distributed lock)

If your application runs across multiple instances, use withoutOverlapping to ensure a task only runs on one server at a time.

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.noOverlap', async () => {
  Logger.info('demo.noOverlap fired');
})
  .everyMinutes(5)
  .withoutOverlapping({ provider: 'redis', ttlMs: 5 * 60_000 })
  .build();

10) Manual-only schedule

This schedule will NOT auto-run. You must invoke it manually via CLI or RPC.

ts
import { Logger, Schedule } from '@zintrust/core';

export default Schedule.define('demo.manualOnly', async () => {
  Logger.info('demo.manualOnly fired');
}).build();

HTTP Schedule Gateway

When deploying to serverless or containerized environments (like Cloudflare Workers or Docker), the system needs a way to remotely trigger schedules.

The API exposes a signed endpoint for managing and triggering schedules:

  • POST /api/_sys/schedule/rpc

Supported actions:

  • list: Retrieve registered schedules.
  • run: Trigger a schedule by name.

Requests to this endpoint must be signed using the SignedRequest scheme (the same signature implementation used by the queue gateway).

CLI Management

You can inspect and execute schedules from the command line:

To view all schedules and their state:

bash
zin schedule:list

Note: schedule:list includes best-effort runtime states, including lastSuccessAt, lastErrorAt, nextRunAt, and consecutiveFailures.

To run a specific schedule immediately:

bash
zin schedule:run --name demo.manualOnly

Multi-Instance Leader Gating

If you run multiple instances of your application, you can ensure only one instance actively dispatches scheduled timers by enabling leader lease gating in your environment configuration:

env
SCHEDULE_LEADER_ENABLED=true

Running the schedule daemon in Docker

ZinTrust provides a ready-made isolated runtime under docker/schedule-runtime/ that acts as the build context for a dedicated schedule container — separate from the main app image and with its own lean package.json.

See docker-schedule-trace-runtimes.md for:

  • Why a separate container is recommended
  • The full folder tree and a snapshot of every file
  • The Dockerfile to create
  • The docker-compose service snippet to add
  • The new-developer setup checklist

Released under the MIT License.