@zudojs/scheduler
Runs your code later, or over and over: after a delay, at a date, on a fixed interval, or on a cron schedule — inside one Node.js process.
OVERVIEW
Scheduling means telling the computer “run this piece of code at this time” instead of running it right now. You hand over a function and a time rule; something else watches the clock and calls the function when the moment arrives.
You can do a crude version of this yourself with setTimeout. That stops being enough quickly: you need the job to repeat, to stop cleanly when the process shuts down, to give up if it hangs, to retry when it fails, and to not pile up ten copies of itself. This package is that machinery.
Everything happens inside your own process. There is no database, no Redis and no separate worker. That keeps it simple and makes it a poor fit for some jobs — see Scheduler vs. queue.
USE IT WHEN
- • You want housekeeping on a timer — expire sessions, prune logs, refresh a cache.
- • You want a nightly or weekly task inside a service you already run.
- • You want a heartbeat or a poll every few seconds.
- • Missing a run because the process restarted is acceptable.
DO NOT USE IT WHEN
- • The work must survive a restart. Nothing is written to disk.
- • You run several copies of your service. Each copy runs the job, so it happens twice.
- • The work is triggered by a user action rather than by the clock — that is a queue.
- • You need a real IANA time zone such as
America/New_York. Only UTC and the machine's local zone are supported.
INSTALLATION
Install the package. It needs Node.js 24 or newer, and it pulls in @zudojs/errors, @zudojs/constants and @zudojs/types on its own.
These docs follow the framework source. If an export shown here is missing from the version you installed, update to the latest @zudojs release.
QUICK START
Three steps, always in this order: create a scheduler, define a job so it has a name, then schedule that name. Scheduling a name you never defined throws.
This program prints a line once per second for about three seconds, then shuts down.
What you should see — the date will be yours, one second in the future:
A STARTED SCHEDULER KEEPS YOUR PROCESS ALIVE
Since v1.2.0 a started scheduler holds the Node.js process open until you call stop(), so a script whose only work is a scheduler runs its jobs. The flip side: a script that never calls stop() never exits, even after its last one-shot has run. Pass new Scheduler({ keepAlive: false }) for a scheduler that should never hold the process open by itself (the behaviour before v1.2.0, when a scheduler-only script exited at once with code 0 and ran nothing).
IN PLAIN WORDS
A job is a named function. A schedule is a rule about when to call it. A handle is the remote control for one schedule. One job can have many schedules pointing at it.
JOBS
define() registers a job. It takes an id (a non-empty string, unique), a name for humans, a handler function, and optional options. Defining the same id twice throws SchedulerJobAlreadyExistsError.
Your handler receives one argument, the job context. It carries the ids, the time the run was scheduled for, the time it actually started, the 1-based attempt number (attempt, also available as attemptNumber), any data you attached to the schedule, and an AbortSignal.
The signal is how the scheduler asks a running job to stop — on timeout, on cancel, or on shutdown. Pass it to anything that accepts one, such as fetch, and long jobs stop promptly instead of hanging around.
The console.log prints a random UUID and the word active, for example 3f0c1a7e-9b21-4d0e-8a55-2c7f6b1e0d44 active. Every ten minutes the handler then prints job fetch-rates status 200.
COMMON MISTAKE
Calling scheduler.every("10m", "fetch-rates") before define() throws InvalidJobError. Define first, schedule second.
SCHEDULING
There are four ways to schedule a defined job. Each returns a ScheduleHandle.
| Call | When it runs | Repeats? |
|---|---|---|
after("30s", "job-id") | Once, 30 seconds from now. | No |
at(new Date("2027-01-01"), "job-id") | Once, at that instant. | No |
every("5m", "job-id") | Every five minutes, starting five minutes from now. | Yes |
cron("0 2 * * *", "job-id") | Every day at 02:00. | Yes |
Duration strings
after() and every() take a duration string, not a number. Units are ms, s, m, h, d, w, and you can stick them together. parseDuration is exported if you want the milliseconds yourself.
Anything else throws InvalidDurationError: a bare number ("5"), an unknown unit ("5x"), a decimal ("1.5h"), a negative or zero value, and anything over roughly a century.
Controlling a schedule
The handle is wired to the live schedule, so these actually change what runs.
Resuming deliberately recomputes from the current time. A schedule paused across three of its fire times does not fire three times the moment you resume it.
A time that has already passed
at() with a date in the past is a misfire. That is not an error by default: the misfire option is "run-once", so the job runs immediately. Pass { misfire: "skip" } to reject the schedule instead, which throws InvalidScheduleError.
The third misfire value, "catch-up", matters for recurring schedules. If the process is blocked past several fire times of an every() or cron() schedule, the default "run-once" runs once and then resumes from the current time, so the missed occurrences are dropped. "catch-up" computes each next time from the fire time that just ran instead of from now, so every missed occurrence is replayed, one per tick, until the schedule is current again. For a one-shot, "catch-up" behaves like "run-once".
MORE MISFIRE RULES
A one-shot resumed after its fire time follows its misfire policy: run-once/catch-up fire it immediately, skip retires it. A paused recurring schedule always resumes from now, whatever its policy. priority breaks ties between schedules due at the same instant (higher first); it never lets a schedule jump ahead of one due earlier. A schedule added after start() fires on time, because the timer is re-armed on every add.
CRON EXPRESSIONS
A cron expression is five values separated by spaces that describe a repeating calendar time, such as “02:00 on weekdays”. It comes from Unix and it is the standard way to write a recurring schedule.
The five fields are always in this order:
| # | Field | Allowed values | Notes |
|---|---|---|---|
| 1 | minute | 0–59 | Minute past the hour. |
| 2 | hour | 0–23 | 24-hour clock. 0 is midnight. |
| 3 | day of month | 1–31 | Day number in the month. |
| 4 | month | 1–12 or jan–dec | Names are case-insensitive. |
| 5 | day of week | 0–6 or sun–sat | 0 is Sunday. 7 also means Sunday. |
Each field takes one of these shapes:
- •
*— every value. “Any minute”, “any month”. - • A single number —
5means exactly 5. - • A range —
1-5means 1, 2, 3, 4, 5. The start must not be larger than the end. - • A step —
*/15means every 15th value: 0, 15, 30, 45.10-30/5means 10, 15, 20, 25, 30. - • A list —
1,15means 1 and 15. List items can themselves be ranges or steps.
One worked example
Take 30 2 * * 1-5 and read it left to right:
| Field | Value | Reads as |
|---|---|---|
| minute | 30 | at minute 30 |
| hour | 2 | of hour 2 (02:xx) |
| day of month | * | on any day number |
| month | * | in any month |
| day of week | 1-5 | if it is Monday through Friday |
Put together: 02:30 every weekday. You can check any expression without a scheduler:
Five shorthand macros are also accepted anywhere an expression is:
| Macro | Same as | Meaning |
|---|---|---|
@hourly | 0 * * * * | Top of every hour. |
@daily / @midnight | 0 0 * * * | Midnight every day. |
@weekly | 0 0 * * 0 | Midnight every Sunday. |
@monthly | 0 0 1 * * | Midnight on the 1st. |
@yearly / @annually | 0 0 1 1 * | Midnight on 1 January. |
WATCH OUT: THE TWO DAY FIELDS
When day-of-month and day-of-week are both restricted, cron fires if either matches, not both. 0 0 1 * mon means “the 1st of the month, and every Monday”. This is odd but it is what every cron does, so expressions copied from elsewhere behave the same here.
A bad expression throws CronParseError the moment you write it, not silently at 2 a.m. Wrong field count, a value out of range (99 0 * * *), a zero step (*/0 * * * *) and an inverted range (5-1 * * * *) are all rejected. The message names the expression first and the reason second, for example Invalid cron expression "99 0 * * *": Cron field "minute" value 99 is outside 0-59., and the same two strings are on error.metadata.expression and error.metadata.reason (before v1.2.0 they were swapped, which garbled the message). An expression that can never happen, such as 30 February (0 0 30 2 *), parses, but scheduler.cron() rejects it with InvalidScheduleError because it has no next fire time within the five-year search (nextCronDate returns null).
TIME ZONES
A cron expression says “02:30”, but 02:30 where? The time zone is what turns those numbers into an actual instant. The same expression fires at different moments in London and in Tokyo.
By default the fields are read in the machine's local zone, whatever the operating system is set to. Pass timezone: "UTC" to read them in UTC instead. "Etc/UTC" is accepted as the same thing.
Any other zone name is rejected rather than quietly ignored, so a schedule never runs at a time you did not ask for. If you need a specific region, set the process time zone (TZ=America/New_York) and use the local-zone form.
TIP: DAYLIGHT SAVING
In a local zone that observes daylight saving, the clock jumps. A job at 02:30 can be skipped on the spring-forward day and run twice on the fall-back day. The search steps hour by hour on the wall clock, not by adding 3,600,000 milliseconds, which limits the damage — but timezone: "UTC" is the way to avoid the question entirely.
OVERLAPPING RUNS
You schedule a job every ten seconds. One day it takes twenty-five seconds. The next fire time arrives while the previous run is still going. That is an overlap, and you have to decide what should happen.
The overlap option on the schedule decides. Set it per schedule, since two schedules of the same job can want different answers, or set a default for every schedule of a job with options.overlap in define(). The schedule's own value wins.
| Value | What happens | Use when |
|---|---|---|
"allow" (default) | The new run starts alongside the old one. Two copies run at once. | Runs are independent and safe to double up. |
"skip" | The new run is dropped. The old one continues undisturbed. | A slow run has already covered the work. |
"queue" | The new run is held and starts when the old one finishes. At most one run at a time; held runs execute one after another. | Every fire time must run, but never two at once. |
"replace" | The old run is aborted through its ctx.signal, then the new one starts. | Only the freshest result matters. |
This job takes 120 ms but is scheduled every 10 ms. With "skip", exactly one copy runs at a time.
Without overlap: "skip" that same program prints runs started: 8 or so.
WATCH OUT: A QUEUE THAT NEVER CATCHES UP
With "queue", a job that is always slower than its interval builds a backlog: in the program above it would still be working through held runs long after the schedule fired. The backlog is capped at 1,024 held runs per schedule; fire times beyond that are dropped. stop() without drain and cancel() discard held runs.
The separate global ceiling
overlap is per schedule. maxConcurrency on the scheduler caps how many job runs may be in flight in total, across every schedule. It defaults to 10. Anything over the ceiling stays on the queue and is picked up on a later tick, not dropped. Between the two sits options.concurrency on a job: the most runs of that one job in flight at once, across every schedule that fires it. A fire time at that ceiling is held and started when a run finishes.
FAILURES AND RETRIES
A job handler that throws does not crash the scheduler, and it does not disappear silently either. It is retried according to the job's retry policy, and whatever survives that is handed to the scheduler's onError callback.
A retry policy says how many attempts to make and how long to wait between them. strategy picks the shape of that wait: "fixed" always waits delay; "linear" waits delay × attempt; "exponential" doubles each time. maxDelay caps it, and jitter: true randomises it so many failing jobs do not all retry in the same instant.
This job fails twice and succeeds on the third attempt. attempts is the total number of tries, including the first, and ctx.attempt counts from 1.
You should see attempt 1, then attempt 2 up to a second later (jitter picks a random wait below the computed delay), then attempt 3, and no error line — the third attempt succeeded. Had all three failed, onError would have printed once. The execution history then prints [ [ 'completed', 3 ] ]: since v1.2.0 getExecutions() records the attempt that finished the run (it always said 1 before), and while a run is in flight the record shows the attempt in progress. ctx.attemptNumber is an alias of ctx.attempt, named as in @zudojs/queue, whose processor context carries the same 1-based number.
The error you receive tells you how it failed:
| Error | Means | Notes |
|---|---|---|
SchedulerJobTimeoutError | The handler ran past options.timeout. | Default timeout is 30,000 ms. The handler's ctx.signal is aborted too. |
SchedulerJobExecutionError | The handler threw. | The original error is kept on .cause. |
SchedulerJobCancelledError | The run was aborted — by cancel(), by stop(), or by overlap: "replace". | Retries stop as well. |
DANGER
Without onError, a permanently failing job fails invisibly — forever, if it is on an interval. Always pass onError in production.
STARTING AND STOPPING
start() begins watching the clock. Nothing fires before it. Calling it twice throws SchedulerAlreadyStartedError. You can define jobs and create schedules before or after starting.
stop() is async — always await it. By default it aborts every run in flight and waits for them to settle. Pass { drain: true } to let them finish instead, and timeoutMs to bound the wait either way.
stop() IS SAFE TO CALL TWICE
Since v1.2.0 stop() is idempotent. On a scheduler that never started, or one already stopped, it resolves (after waiting for any execution still settling) instead of rejecting with SchedulerStoppedError. Shutdown code that runs twice (SIGINT then SIGTERM, or a signal handler plus a lifecycle hook), or after a boot that failed before start(), needs no guard.
Two read-only properties let you inspect the scheduler, two methods let you inspect its schedules, and getExecutions() returns the last 100 runs.
SCHEDULER VS. QUEUE
@zudojs/scheduler and @zudojs/queue both run functions in the background, and it is easy to reach for the wrong one. The difference is what starts the work.
The scheduler is driven by the clock. You declare a rule once and it fires on time, over and over, whether or not anything else happened.
The queue is driven by events. Something happens — a user signs up — and you push one piece of work onto a list for a worker to pick up as soon as it can.
| @zudojs/scheduler | @zudojs/queue | |
|---|---|---|
| Triggered by | The clock. | Code calling add(). |
| Unit of work | A named job plus a repeating time rule. | One job with its own payload. |
| Survives a restart | No. Schedules live in memory. | With a durable adapter, yes. |
| Several service instances | Each runs the job, so it happens N times. | One worker takes each job. |
| Typical use | Nightly cleanup, hourly sync, heartbeat. | Send this email, resize this image. |
They combine well. Let the scheduler fire on time and have its handler push the real work onto a queue — you get a reliable trigger and durable, distributed execution.
DANGER: MULTIPLE INSTANCES
Deploy three copies of a service that schedules a nightly billing job and the billing runs three times. This package has no cross-process lock. SchedulerStoreError and SchedulerLockError exist in @zudojs/errors for future persistence work; this package does not re-export them, and no store or lock is implemented today.
API REFERENCE
Everything below is exported from @zudojs/scheduler. Most applications only need the Scheduler class.
Scheduler methods
| Name | What it does | Notes |
|---|---|---|
new Scheduler(options?) | Creates a scheduler. | Options: jobs, executor, queue, clock, maxConcurrency, onError, keepAlive (default true: a started scheduler holds the process open until stop()). All optional. |
define(job) | Registers a JobDefinition. | Throws InvalidJobError on a blank id, a missing handler or a non-positive timeout. |
after(duration, jobId, options?) | Runs once after a delay. | Returns a ScheduleHandle. |
at(date, jobId, options?) | Runs once at a Date. | A past date follows the misfire policy. |
every(duration, jobId, options?) | Runs repeatedly on an interval. | First run is one interval away. |
cron(expression, jobId, options?) | Runs repeatedly on a cron schedule. | Options also accept timezone. |
start() | Starts the timer loop. | Throws SchedulerAlreadyStartedError if already running. |
stop(options?) | Stops and waits for in-flight runs. | async. Options: drain, timeoutMs. Idempotent: resolves on a scheduler that never started or already stopped. |
getSchedule(scheduleId) | Snapshot of one schedule, or undefined. | Read-only copy. |
listSchedules() | Snapshots of every live schedule. | Completed and cancelled ones are gone. |
getExecutions(jobId?) | Recorded runs, oldest first, as JobExecution records. | At most 100 (MAX_EXECUTION_HISTORY). Pass a job id to filter. |
isRunning / scheduleCount | Getters for state and live schedule count. | Read-only. |
Functions
| Name | What it does | Notes |
|---|---|---|
parseDuration(text) | Turns "1h30m" into milliseconds. | Throws InvalidDurationError. |
parseCron(expression) | Parses a cron expression into a ParsedCron. | Throws CronParseError. |
nextCronDate(parsed, after, utc?) | Next fire time strictly after after, or null. | utc defaults to false (local zone). |
retryDelay(policy, attempt) | Milliseconds to wait before the next attempt. | Returns 0 when policy is undefined. |
createJobDefinition(id, name, handler, options?) | Builds a frozen JobDefinition. | An alternative to the object literal. |
createJobContext(jobId, executionId, scheduledAt, startedAt, attempt, data, signal) | Builds a frozen JobContext. | Mostly for tests; the executor builds one for you. |
createSchedule(id, jobId, type, nextRunAt, options?) | Builds a frozen Schedule. | For building a queue by hand. |
createSystemClock() | Returns a SystemClock. | Same as new SystemClock(). |
isSchedulerError(value) | Type guard for SchedulerError. | Use in catch blocks. |
createSchedulerError(message, options?) | Factory from @zudojs/errors. | Re-exported for convenience. |
Classes
| Name | What it does | Notes |
|---|---|---|
Scheduler | The one class most apps use. | See the table above. |
JobRegistry | Holds job definitions: register, get, getOrThrow, has, list, unregister, clear. | The scheduler builds one unless you pass your own. |
JobExecutor | Runs one job under a timeout, an abort signal and the retry policy. | new JobExecutor(clock); execute(job, executionId, scheduledAt, attempt, signal, data?, hooks?). hooks.onAttempt(n) reports each 1-based attempt as it starts. |
PriorityQueue | Min-heap of schedules ordered by nextRunAt. | enqueue throws RangeError on an invalid date. |
SystemClock | Real time: now() and nowMs(). | Pass any Clock (an object with those two methods) as clock to fake time in tests. Since v1.2.0 the interface is exported: import type { Clock } from "@zudojs/scheduler". |
DelayTrigger, DateTrigger, IntervalTrigger, CronTrigger | Compute the next fire time. Each has next(after). | The scheduler picks one for you; use them directly to preview times. |
ScheduleHandleImpl | The concrete ScheduleHandle. | You get an instance back from after/at/every/cron; rarely constructed by hand. |
Types
| Name | What it is | Notes |
|---|---|---|
JobDefinition | { id, name, handler, options? }. | What define() takes. |
JobHandler<T> | (context: JobContext<T>) => Promise<void> | void. | The return value is ignored. |
JobContext<T> | jobId, executionId, scheduledAt, startedAt, attempt, attemptNumber, data, signal. | The single argument your handler gets. attempt and attemptNumber are the same 1-based number. |
JobOptions | timeout, retry, concurrency, overlap. | concurrency caps this job's runs in flight; overlap is the default for its schedules. |
RetryPolicy | attempts, strategy, delay, maxDelay?, jitter?. | strategy is RetryStrategy: fixed, linear or exponential. |
ScheduleOptions | timezone, misfire, overlap, priority, data. | data reaches the handler as ctx.data. |
Schedule | id, jobId, type, expression?, nextRunAt, lastRunAt?, state, options?. | What getSchedule() returns. |
ScheduleHandle | id, state, pause, resume, cancel, nextRun. | Returned by every scheduling method. |
Trigger | { next(after: Date): Date | null }. | Implement it for a custom timing rule. |
ParsedCron | The value sets for each cron field. | Returned by parseCron. |
SchedulerOptions, SchedulerErrorEvent | Constructor options, and the { scheduleId, jobId, executionId, error } given to onError. | |
ScheduleType, ScheduleState, JobState, OverlapPolicy, MisfirePolicy | String unions used above. | Executions are recorded with a status and returned by getExecutions() (last 100). |
SchedulerJobId, ScheduleId, ExecutionId | Aliases for string. | Documentation only. |
JobExecution, JobExecutionResult | Shapes for an execution record and its result. | JobExecutionResult is what JobExecutor.execute resolves to on success (it rejects on failure). getExecutions() returns JobExecution records. |
ScheduleHandleBinding, SchedulerErrorOptions | The wiring a handle needs; the options bag for scheduler errors. | Only needed if you build a handle yourself. |
Errors
| Name | Thrown when | Notes |
|---|---|---|
SchedulerError | Base class for every error here. | All are re-exported from @zudojs/errors. |
InvalidJobError | define() gets a bad job, or you schedule an unregistered id. | The most common one you will hit. |
InvalidScheduleError | A bad trigger, an unsupported time zone, or misfire: "skip" on a past time. | Also for a non-positive or absurd interval. |
CronParseError | The cron expression is malformed. | Thrown at cron(), not at fire time. |
InvalidDurationError | The duration string is malformed or out of range. | From parseDuration, after and every. |
SchedulerAlreadyStartedError / SchedulerStoppedError | start() while running. SchedulerStoppedError is still exported, but since v1.2.0 stop() no longer rejects with it. | start() throws synchronously. |
SchedulerJobAlreadyExistsError / SchedulerJobNotFoundError | Duplicate define(); JobRegistry.getOrThrow on an unknown id. | |
SchedulerJobExecutionError / SchedulerJobTimeoutError / SchedulerJobCancelledError | A run threw, ran too long, or was aborted. | These reach you through onError. |
SchedulerNotStartedError, ScheduleNotFoundError, ScheduleAlreadyExistsError, SchedulerStoreError, SchedulerLockError | Never thrown by this package. | Not exported from @zudojs/scheduler; import them from @zudojs/errors if you build a durable scheduler on top. |
Constants
| Name | Value | Notes |
|---|---|---|
DEFAULT_JOB_TIMEOUT | 30,000 | Used when a job sets no timeout. |
DEFAULT_MAX_CONCURRENCY | 10 | Default maxConcurrency. |
DEFAULT_RETRY_DELAY | 1,000 | Used when a policy omits delay. |
DEFAULT_MAX_RETRIES | 3 | Caps the exponent growth inside retryDelay. |
DEFAULT_MISFIRE_POLICY | "run-once" | Applied when misfire is omitted. |
DEFAULT_OVERLAP_POLICY | "allow" | Applied when overlap is omitted. |
MAX_TIMER_DELAY | 2,147,483,647 | Longest single setTimeout the loop will arm. |
MAX_JOBS / MAX_SCHEDULES | 4,096 each | Hard caps; exceeding either throws. |
MAX_EXECUTION_HISTORY | 100 | How many runs getExecutions() keeps. There is no fixed tick interval: the loop sleeps until the next fire time. |
COMMON MISTAKES
- Scheduling before defining. →
InvalidJobError: Job "x" is not registered.→ Calldefine()first; the scheduler checks the registry at scheduling time. - Never calling
stop()in a script. → The jobs run, but the process never exits, because a started scheduler keeps Node alive. →await scheduler.stop()when the script is done, or passkeepAlive: falseif something else owns the process lifetime. - Forgetting
start(). → Schedules exist,nextRun()returns a date, nothing ever runs. → Callscheduler.start()once at boot. - Not awaiting
stop(). → The process exits mid-job, or an unhandled rejection appears on shutdown. →await scheduler.stop({ drain: true, timeoutMs: 30_000 }). A second call is harmless. - Leaving out
onError. → A job that throws every run fails in total silence. → PassonErrorto the constructor and log the event. - Passing a number where a duration string goes. →
every(5000, "job")is a type error, andevery("5", "job")throwsInvalidDurationError. → Write the unit:"5s". - Ignoring
ctx.signalin a long handler. → A timeout orstop()reports the job as cancelled while the work keeps running. → Pass the signal tofetch, database drivers and anything else that accepts one. - Expecting a real IANA time zone. →
{ timezone: "Europe/Paris" }throwsInvalidScheduleError. → Use"UTC", or set the processTZand omit the option.
COMPLETE EXPORT INDEX
Every name @zudojs/scheduler exports from its package root at v1.1.2 — 67 in total, generated from the package’s own entry point rather than written by hand. The sections above explain the ones you reach for most; this is the exhaustive list, so nothing shipped is undocumented. Names not covered above are typically internal helpers and supporting types.
Show all 67 exports
CronParseError CronTrigger DateTrigger DelayTrigger IntervalTrigger InvalidDurationError InvalidJobError InvalidScheduleError JobExecutor JobRegistry PriorityQueue ScheduleHandleImpl Scheduler SchedulerAlreadyStartedError SchedulerError SchedulerJobAlreadyExistsError SchedulerJobCancelledError SchedulerJobExecutionError SchedulerJobNotFoundError SchedulerJobTimeoutError SchedulerStoppedError SystemClockcreateJobContext createJobDefinition createSchedule createSchedulerError createSystemClock isSchedulerError nextCronDate parseCron parseDuration retryDelayJobContext JobDefinition JobExecution JobExecutionResult JobOptions ParsedCron RetryPolicy Schedule ScheduleHandle ScheduleHandleBinding ScheduleOptions SchedulerErrorEvent SchedulerErrorOptions SchedulerOptions TriggerExecutionId JobHandler JobState MisfirePolicy OverlapPolicy RetryStrategy ScheduleId SchedulerJobId ScheduleState ScheduleTypeDEFAULT_JOB_TIMEOUT DEFAULT_MAX_CONCURRENCY DEFAULT_MAX_RETRIES DEFAULT_MISFIRE_POLICY DEFAULT_OVERLAP_POLICY DEFAULT_RETRY_DELAY MAX_EXECUTION_HISTORY MAX_JOBS MAX_SCHEDULES MAX_TIMER_DELAY