Overview¶
GPScheduler (General-Purpose Scheduler) is a cron-style Python library that turns
functions from any package into scheduled jobs through YAML config files. You mark a
function as schedulable with the @scheduled decorator, point a config file at it by
dotted path, declare when it should run with a cron expression, and gpscheduler takes
care of the rest.
Design purpose¶
Application code is littered with timers, while True: sleep(...) loops, and
hand-rolled schedulers that are hard to audit and harder to change. GPScheduler exists
to move all of that out of application code and into declarative config:
- Schedules live in config, not in code. A cron expression in a YAML file says when; the function says what. Operators can change timing without touching application code, and the full schedule is auditable at a glance.
- Any importable function can be a job. Jobs are not subclassed or registered by
hand — they are pointed at by dotted path (
mypkg.jobs.cleanup). The@scheduleddecorator is the opt-in signal: "gpscheduler may call me." - Call arguments come from config too. Positional and keyword arguments for each job are declared in its config file and validated against the function's signature at load time.
GPScheduler is a thin wrapper around APScheduler 3.x — a stable, widely used scheduler engine. It is not a distributed scheduler. It schedules jobs in a single process, and focuses on doing that well: config-driven job definitions, two execution models (thread and process), cross-platform graceful shutdown, and loud, early validation.
Architecture¶
GPScheduler has a single-direction dependency flow. Nothing depends on the engine except the scheduler module itself, so a future migration would be contained in one place.
┌──────────────┐
config files ─────▶ │ config │ GPSchedulerConfig, GPJobConfig
(gpconfig folder) └──────┬───────┘ (Pydantic-validated, extra="forbid")
│
▼
@scheduled funcs ┌──────────────┐
(imported by ───▶ │ loader │ validates every job (cron, signature,
packages) └──────┬───────┘ registry lookup, pickle preflight) …
▲ │ … returns a list of LoadedJob records
│ ▼
┌──────────────┐ ┌──────────────┐
│ registry │◀──│ cron │ dotted-path → function map
│ (process- │ │ (5/6-field) │ strict lookup, never falls back to
│ local) │ └──────────────┘ importlib for undecorated functions
└──────────────┘
│
▼
┌──────────────┐
│ scheduler │ composes APScheduler BackgroundScheduler,
│ (engine) │ registers both "thread" + "process"
│ │ executors, handles start/shutdown/signals
└──────┬───────┘
│
▼
┌──────────────┐
│ CLI │ gpscheduler run / list-jobs
└──────────────┘
How a job gets from config to execution:
- Config load — gpconfig hydrates
scheduler.yamlinto aGPSchedulerConfigand eachjobs/<name>.yamlinto aGPJobConfig. Unknown keys are rejected. - Package import — every package in
scheduler.yaml:packagesis imported, which fires the@scheduled/@worker_initregistration side-effects and populates the process-global Registry. - Per-job validation — for each job the loader looks up
funcin the Registry, parses the cron expression, checks for reserved keys, bindsargs/kwargsagainst the function signature, and (forprocessjobs) pickles the arguments as a preflight check. Any failure stops startup with a precise exception. - Registration — enabled jobs are registered with APScheduler under both a
threadand aprocessnamed executor; each job picks its pool via itsexecutorfield. - Execution — APScheduler's background thread fires jobs on schedule, in the chosen
pool. The scheduler's own logs (startup, shutdown, job-execution exceptions) go
through
gpclog.
Use cases¶
GPScheduler is a good fit when you want cron-style scheduling inside a Python process, declaratively:
- Batch housekeeping — nightly cleanup, daily report generation, weekly index
rebuilds, declared as config rather than
crontablines pointing at separate scripts. - Periodic I/O work — polling an API, pinging a database, flushing a queue on an
interval. The default thread executor and
@worker_init(reuse one connection) are designed for this. - CPU-bound periodic work — data processing, model inference, heavy computation on a schedule. The opt-in process executor gives real parallelism and process-level isolation.
- Embedding in a host application — a web app or service that needs a background
scheduler alongside its own event loop. Use the embedded API
(
create_scheduler(...)thenstart()/shutdown()), not the CLI.
GPScheduler is not the right tool for: distributed scheduling across multiple
machines, sub-second or event-driven workloads (it is cron-style, not a stream
processor), or jobs that must be async (the executors are synchronous).
Key design principles¶
Fail-Early¶
GPScheduler surfaces problems at load time, never at the first trigger. If a cron expression is malformed, a dotted path isn't registered, configured arguments don't bind to the signature, or process-job arguments aren't picklable, startup fails with a precise exception immediately. You will not discover a broken schedule three days into a production run.
This applies to disabled jobs too — a job with enable: false is still fully
validated. Disabling a job is not a way to ship broken config; it only skips trigger
registration.
Note: Fail-Early targets system-design and configuration errors. It does not mean a job that raises at runtime crashes the scheduler — runtime job failures are caught, logged, and do not affect the next trigger. The scheduler keeps running.
Single source of truth¶
The @scheduled decorator carries no scheduling information — no cron expression,
no arguments, nothing. All of that lives in config. There is one place that says when
and how a job runs, and it is the config file. The decorator only declares "gpscheduler
may call me."
Strict registry lookup¶
A func dotted path in config must resolve to a @scheduled-decorated function.
gpscheduler never falls back to importlib for an undecorated function — the decorator
is your explicit consent to be scheduled, and an unknown path fails loudly at load time
rather than silently calling arbitrary code.
Limitations¶
Be aware of the current boundaries:
- Single-process, not distributed. No APScheduler 4.x data store or event broker; no cross-machine coordination.
- No
asyncjobs. APScheduler 3.x's synchronous executors are used;async deffunctions are rejected by the decorators. - No runtime add/remove of jobs. All jobs are loaded and validated at startup from config. Changing the schedule means restarting the scheduler (no hot-reload).
- No single-job execution timeout. If a job needs a runtime cap, enforce it inside
the function (e.g.
signal.alarm, a thread timeout). The scheduler-leveltimeoutis for graceful shutdown only. - Return values are not consumed. A job that needs to hand off data must do so through an external channel (database, queue, file).
- Windows graceful shutdown is
Ctrl+C-only.SIGTERMis not reliably delivered on Windows; see Performance → cross-platform. - Cron supports only POSIX special characters (
* , - /); Quartz extensions (L,W,#) and a year field are not supported. See Configuration → cron format.