Tempus.jl is a lightweight, Quartz-inspired job scheduling library for Julia. It provides an easy-to-use API for defining cron-like schedules and executing jobs at specified times.
- Define jobs using cron-style scheduling expressions
- Support for job execution policies (overlap handling, retries, failure strategies)
- Backend-neutral state through AbstractStores.jl
- Thread-safe scheduling with concurrency-aware execution
- Dynamic job control (enable, disable, unschedule jobs)
- Supports retry policies with exponential backoff
Tempus.jl is registered in the Julia General registry:
using Pkg
Pkg.add("Tempus")using Tempus
# Define a job that prints a message every minute
job = Tempus.Job(
() -> println("Hello from Tempus!"),
"example_job",
"* * * * *",
)
# Create an in-memory scheduler
scheduler = Tempus.Scheduler()
# Add the job to the scheduler
push!(scheduler, job)
# Start the scheduler (runs in a background thread)
Tempus.run!(scheduler)# runs once, as soon as the scheduler picks it up, then is disabled;
# failed attempts are re-run until success or max_failed_executions
job = Tempus.OneShotJob(send_report, "send_report_now")
push!(scheduler, job)Tempus.disable!(job) # Prevents the job from running
Tempus.enable!(job) # Allows it to run againThese mutate the Job object. To disable a job in a persisting store so it stays
disabled across restarts, use Tempus.disableJob!(store, job).
Tempus.unschedule!(scheduler, job)Tempus stores job records and bounded execution history as two namespaced
views over one AbstractStores backend. The backend must accept heterogeneous
values, i.e. be an AbstractStore{Any}; any codec works, including
FileStore(dir; codec=JSONCodec()) (needs using JSON). The default scheduler
uses process memory:
using AbstractStores, Tempus
backend = MemoryStore()
store = Tempus.Store(backend)
scheduler = Tempus.Scheduler(store)Change only the backend to persist jobs and execution history across restarts:
backend = FileStore(joinpath(homedir(), ".tempus", "state"))
scheduler = Tempus.Scheduler(backend)The same form works with RedisStore (needs using Redis) and SQLStore
(needs using DBInterface plus a driver), whose methods live in AbstractStores
package extensions. One backend can also hold state for several libraries and
the application because each user gets a separate prefix:
backend = SQLStore{Any}(connection; table="service_state")
tempus = Tempus.Store(backend; prefix="tempus/")Tempus.InMemoryStore(), Tempus.FileStore(directory), and
Tempus.SQLiteStore(connection) remain as convenience constructors. The
Tempus 2 file store used one JSON file; the new file constructor takes a
directory and writes one atomic file per key, so state written by Tempus 2 is
not read or migrated — a scheduler started against an old jobs.dat starts
empty. Execution history is now persisted too, which Tempus 2's file store did
not do.
A store persists each job's definition (name, schedule, options, disabled
state) and execution history, but not its action: the action lives in the
Tempus.Store object the job was added to with Tempus.addJob! (or
push!(scheduler, job)). Add every job at startup; any function works,
including closures over application state. A scheduler skips stored jobs that
have no action in this process. To run jobs a previous process stored without
adding them again, call Tempus.restoreJobs!(store), which resolves each named
function by its module path.
Treat a persistent store's contents as trusted input: the default backends use
Julia's Serialization, and deserializing attacker-controlled data can execute
arbitrary code — whoever can write to the store's directory, database, or Redis
keyspace can run code in any process that reopens it. Protect the backing
storage with filesystem or database permissions accordingly.
The scheduler and cron parsing compile into trimmed binaries
(juliac --trim=safe) with a MemoryStore backend on Julia 1.13 and later, and
with a JSONCodec store on Julia 1.14 and later (Julia 1.13's trim verifier
rejects JSON.jl's typed decoding of nested structs). Other backends, such as
the default SerializedCodec, are not trim-safe. In a trimmed binary:
- create jobs at run time and add them at every startup;
restoreJobs!andresolve_functionuse reflection and are unavailable; - pass job parameters by capturing them in the action instead of
job_params, which are converted to keyword arguments from JSON at every call; - declare a
retry_check's exception argument@nospecialize:retry_check=(state, @nospecialize(e)) -> e isa MyError.
Timezone-aware schedules load TimeZones' database, which is not trim-safe;
that code is only compiled into programs that create jobs with a timezone.
Tempus.jl uses standard cron syntax, with an optional leading seconds field:
* * * * * → Every minute
0 12 * * * → Every day at noon
*/5 * * * * → Every 5 minutes
30 4 1,15 * * → 4:30 AM on the 1st and 15th
0 22 * * 1-5 → 10 PM on weekdays
# a 6th trailing field is never needed; a leading seconds field is optional
* * * * * * → Every second
Fields are minute hour day-of-month month day-of-week (or with seconds
first when six fields are given). Supported field forms: *, single values,
ranges (1-5), lists (1,3,5), and steps (*/15, 10-30/7 — steps count
from the range start, so 10-30/7 fires at 10, 17, and 24). Day-of-week runs
Sunday to Saturday as 0–6, with 7 also accepted for Sunday. Three-letter,
case-insensitive names work for months and weekdays (0 0 1 JAN *,
0 0 * * MON-FRI), and when both day-of-month and day-of-week are
restricted, a day matching either fires (standard cron behavior). The common
aliases @yearly/@annually, @monthly, @weekly, @daily/@midnight,
and @hourly are also accepted.
All schedules are evaluated in UTC unless the job is given a timezone
option (an IANA name like "America/Denver"), in which case the schedule is
interpreted in that timezone: spring-forward gaps are skipped and fall-back
ambiguities use the first occurrence.
Contributions are welcome! To contribute:
- Fork the repository.
- Create a new branch with your feature or bugfix.
- Submit a pull request with your changes.
Make sure to include tests for any new functionality.
Tempus.jl is licensed under the MIT License.