A fluent, Laravel-inspired scheduler for Go that wraps gocron with expressive APIs for defining, filtering, and controlling scheduled jobs.
- Fluent, chainable API for intervals, cron strings, and calendar helpers (daily/weekly/monthly).
- Overlap protection with optional distributed locking plus per-job tags and metadata.
- Filters (weekdays/weekends/time windows) and hooks (before/after/success/failure) keep jobs predictable.
- Command execution helper for running CLI tasks with background mode and env-aware tagging.
- Auto-generated, compile-tested examples ensure docs and behavior stay in sync.
Go has excellent low-level scheduling libraries, but defining real-world schedules often turns into a maze of cron strings, conditionals, and glue code.
scheduler provides a Laravel-style fluent API on top of gocron that lets you describe when, how, and under what conditions a job should run - without hiding what’s actually happening.
Everything remains explicit, testable, and inspectable, while staying pleasant to read and maintain.
go get github.com/goforj/schedulerIf you use distributed locking, there are two paths:
- Bring your own
gocron.Lockerimplementation and install whatever backend client it needs. - Use
NewCacheLockerwithgithub.com/goforj/cacheand install only the cache driver you plan to use.
For example, with a cache-backed Redis locker:
go get github.com/goforj/cache
go get github.com/goforj/cache/driver/rediscache
go get github.com/redis/go-redis/v9s := scheduler.New()
defer s.Stop()
s.EveryMinute().Name("cleanup").Do(func() { runCleanup() }) // run in-process cleanup every minute
s.DailyAt("09:00").Weekdays().Name("reports:morning").Do(func() { sendMorningReport() }) // weekdays at 09:00
s.Cron("0 0 * * *").Command("reports:purge", "--force") // run app subcommand nightly
s.Cron("*/15 * * * *").Exec("/usr/bin/env", "echo", "heartbeat") // run external executable every 15 minutes
s.EveryFiveMinutes().WithoutOverlapping().Name("sync:inventory").Do(func() { syncInventory() }) // prevent overlapping runs
s.Cron("0 * * * *").When(func() bool { return isPrimaryNode() }).Name("rebalance").Do(func() { rebalance() }) // run only when condition passess := scheduler.New()
defer s.Stop()
s.
Name("reports:generate").
Timezone("America/New_York").
Weekdays().
Between("09:00", "17:00").
WithoutOverlapping().
Before(func() { markJobStart("reports:generate") }).
OnSuccess(func() { notifySuccess("reports:generate") }).
OnFailure(func() { notifyFailure("reports:generate") }).
DailyAt("10:30").
Do(func() { generateReports() })
s.
Name("reconcile:daily").
RunInBackground().
Cron("0 3 * * *").
Command("billing:reconcile", "--retry=3")package main
import (
"github.com/goforj/scheduler"
)
func main() {
s := scheduler.New()
defer s.Stop()
s.EveryMinute().Name("cleanup").Do(func() {}) // run cleanup every minute
s.DailyAt("10:30").Weekdays().Name("reports:generate").Do(func() {}) // run reports on weekdays at 10:30
s.Cron("0 0 * * *").Command("reports:purge", "--force") // run app subcommand nightly at midnight
s.PrintJobsList()
}Example output:
+------------------------------------------------------------------------------------------------------------------------+
| Scheduler Jobs › (3) |
+------------------+----------+----------------+-----------------------+----------------------+--------------------------+
| Name | Type | Schedule | Handler | Next Run | Tags |
+------------------+----------+----------------+-----------------------+----------------------+--------------------------+
| cleanup | function | every 1m | main.main (anon func) | in 1m Mar 3 2:16AM | env=local |
| reports:generate | function | cron 30 10 * * * | main.main (anon func) | in 8h Mar 3 10:30AM | env=local |
| reports:purge | command | cron 0 0 * * * | - | in 21h Mar 4 12:00AM | env=local, args="--force" |
+------------------+----------+----------------+-----------------------+----------------------+--------------------------+
Every function has a corresponding runnable example under ./examples.
These examples are generated directly from the documentation blocks of each function, ensuring the docs and code never drift. These are the same examples you see here in the README and GoDoc.
An automated test executes every example to verify it builds and runs successfully.
This guarantees all examples are valid, up-to-date, and remain functional as the API evolves.
Lock invokes the underlying function.
client := redis.NewClient(&redis.Options{})
locker := scheduler.NewRedisLocker(client, 10*time.Minute)
lock, _ := locker.Lock(context.Background(), "job")
_ = lock.Unlock(context.Background())Run executes the underlying function.
runner := scheduler.CommandRunnerFunc(func(ctx context.Context, exe string, args []string) error {
return nil
})
_ = runner.Run(context.Background(), "echo", []string{"hi"})Unlock invokes the underlying function.
lock := scheduler.LockFunc(func(context.Context) error { return nil })
_ = lock.Unlock(context.Background())Daily schedules the job to run once per day at midnight.
scheduler.New().Daily()DailyAt schedules the job to run daily at a specific time (e.g., "13:00").
scheduler.New().DailyAt("12:30")DaysOfMonth schedules the job to run on specific days of the month at a given time.
scheduler.New().DaysOfMonth([]int{5, 20}, "07:15")LastDayOfMonth schedules the job to run on the last day of each month at a specific time.
scheduler.New().LastDayOfMonth("23:30")Monthly schedules the job to run on the first day of each month at midnight.
scheduler.New().Monthly()MonthlyOn schedules the job to run on a specific day of the month at a given time.
scheduler.New().MonthlyOn(15, "09:30")Quarterly schedules the job to run on the first day of each quarter at midnight.
scheduler.New().Quarterly()QuarterlyOn schedules the job to run on a specific day of each quarter at a given time.
scheduler.New().QuarterlyOn(3, "12:00")TwiceDaily schedules the job to run daily at two specified hours (e.g., 1 and 13).
scheduler.New().TwiceDaily(1, 13)TwiceDailyAt schedules the job to run daily at two specified times (e.g., 1:15 and 13:15).
scheduler.New().TwiceDailyAt(1, 13, 15)TwiceMonthly schedules the job to run on two specific days of the month at the given time.
scheduler.New().TwiceMonthly(1, 15, "10:00")Weekly schedules the job to run once per week on Sunday at midnight.
scheduler.New().Weekly()WeeklyOn schedules the job to run weekly on a specific day of the week and time. Day uses 0 = Sunday through 6 = Saturday.
scheduler.New().WeeklyOn(1, "8:00")Yearly schedules the job to run on January 1st every year at midnight.
scheduler.New().Yearly()YearlyOn schedules the job to run every year on a specific month, day, and time.
scheduler.New().YearlyOn(12, 25, "06:45")Command executes the current binary with the given subcommand and variadic args. It does not run arbitrary system executables; use Exec for that.
scheduler.New().Cron("0 0 * * *").Command("jobs:purge", "--force")Exec runs an external executable with variadic args.
scheduler.New().Cron("0 0 * * *").Exec("/usr/bin/env", "echo", "hello")WithoutOverlapping ensures the job does not run concurrently.
scheduler.New().
WithoutOverlapping().
EveryFiveSeconds().
Do(func() { time.Sleep(7 * time.Second) })WithoutOverlappingWithLocker ensures the job does not run concurrently across distributed systems using the provided locker.
locker := scheduler.LockerFunc(func(ctx context.Context, key string) (gocron.Lock, error) {
return scheduler.LockFunc(func(context.Context) error { return nil }), nil
})
scheduler.New().
WithoutOverlappingWithLocker(locker).
EveryMinute().
Do(func() {})Timezone sets a timezone string for the job (not currently applied to gocron Scheduler).
scheduler.New().Timezone("America/New_York").Daily()WithCommandRunner overrides command execution (default: exec.CommandContext).
runner := scheduler.CommandRunnerFunc(func(_ context.Context, exe string, args []string) error {
_ = exe
_ = args
return nil
})
builder := scheduler.New().WithCommandRunner(runner)
_ = builderWithNowFunc overrides current time (default: time.Now). Useful for tests.
fixed := func() time.Time { return time.Unix(0, 0) }
scheduler.New().WithNowFunc(fixed)New creates and starts a scheduler facade. It panics only if gocron scheduler construction fails.
s := scheduler.New()
defer s.Stop()
s.Every(15).Seconds().Do(func() {})NewWithError creates and starts a scheduler facade and returns setup errors.
s, err := scheduler.NewWithError()
if err != nil {
panic(err)
}
defer s.Stop()CronExpr returns the cron expression string configured for this job.
builder := scheduler.New().Cron("0 9 * * *")
fmt.Println(builder.CronExpr())
// Output: 0 9 * * *Error returns the error if any occurred during job scheduling.
builder := scheduler.New().DailyAt("bad")
fmt.Println(builder.Error())
// Output: invalid DailyAt time format: invalid time format (expected HH:MM): "bad"Job returns the last scheduled gocron.Job instance, if available.
b := scheduler.New().EverySecond().Do(func() {})
fmt.Println(b.Job() != nil)
// Output: trueJobs returns scheduled jobs from the underlying scheduler.
PrintJobsList renders and prints the scheduler job table to stdout.
s := scheduler.New()
defer s.Stop()
s.EverySecond().Name("heartbeat").Do(func() {})
s.PrintJobsList()
// Output:
// +------------------------------------------------------------------------------------------+
// | Scheduler Jobs › (1)
// +-----------+----------+----------+-----------------------+--------------------+-----------+
// | Name | Type | Schedule | Handler | Next Run | Tags |
// +-----------+----------+----------+-----------------------+--------------------+-----------+
// | heartbeat | function | every 1s | main.main (anon func) | in 1s Mar 3 2:15AM | env=local |
// +-----------+----------+----------+-----------------------+--------------------+-----------+RunInBackground runs command/exec tasks in a goroutine.
scheduler.New().RunInBackground().Command("noop")Between limits the job to run between the provided HH:MM times (inclusive).
scheduler.New().Between("09:00", "17:00").EveryMinute()Days limits the job to a specific set of weekdays.
scheduler.New().Days(time.Monday, time.Wednesday, time.Friday).DailyAt("07:00")Environments restricts job registration to specific environment names (e.g. "production", "staging").
scheduler.New().Environments("production").Daily()Fridays limits the job to Fridays.
scheduler.New().Fridays().DailyAt("09:00")Mondays limits the job to Mondays.
scheduler.New().Mondays().DailyAt("09:00")Saturdays limits the job to Saturdays.
scheduler.New().Saturdays().DailyAt("09:00")Skip prevents scheduling the job if the provided condition returns true.
enabled := false
scheduler.New().Skip(func() bool { return !enabled }).Daily()Sundays limits the job to Sundays.
scheduler.New().Sundays().DailyAt("09:00")Thursdays limits the job to Thursdays.
scheduler.New().Thursdays().DailyAt("09:00")Tuesdays limits the job to Tuesdays.
scheduler.New().Tuesdays().DailyAt("09:00")UnlessBetween prevents the job from running between the provided HH:MM times.
scheduler.New().UnlessBetween("22:00", "06:00").EveryMinute()Wednesdays limits the job to Wednesdays.
scheduler.New().Wednesdays().DailyAt("09:00")Weekdays limits the job to run only on weekdays (Mon-Fri).
scheduler.New().Weekdays().DailyAt("09:00")Weekends limits the job to run only on weekends (Sat-Sun).
scheduler.New().Weekends().DailyAt("10:00")When only schedules the job if the provided condition returns true.
flag := true
scheduler.New().When(func() bool { return flag }).Daily()After sets a hook to run after task execution.
scheduler.New().After(func() {}).Daily()Before sets a hook to run before task execution.
scheduler.New().Before(func() {}).Daily()OnFailure sets a hook to run after failed task execution.
scheduler.New().OnFailure(func() {}).Daily()OnSuccess sets a hook to run after successful task execution.
scheduler.New().OnSuccess(func() {}).Daily()GocronScheduler returns the underlying gocron scheduler for advanced integration. Prefer the fluent scheduler API for typical use-cases.
Every schedules a job to run every X seconds, minutes, or hours.
scheduler.New().Every(10).Minutes()EveryDuration schedules a duration-based interval job builder.
EveryFifteenMinutes schedules the job to run every 15 minutes.
scheduler.New().EveryFifteenMinutes().Do(func() {})EveryFifteenSeconds schedules the job to run every 15 seconds.
scheduler.New().EveryFifteenSeconds().Do(func() {})EveryFiveMinutes schedules the job to run every 5 minutes.
scheduler.New().EveryFiveMinutes().Do(func() {})EveryFiveSeconds schedules the job to run every 5 seconds.
scheduler.New().EveryFiveSeconds().Do(func() {})EveryFourHours schedules the job to run every four hours at the specified minute.
scheduler.New().EveryFourHours(25)EveryFourMinutes schedules the job to run every 4 minutes.
scheduler.New().EveryFourMinutes().Do(func() {})EveryMinute schedules the job to run every 1 minute.
scheduler.New().EveryMinute().Do(func() {})EveryOddHour schedules the job to run every odd-numbered hour at the specified minute.
scheduler.New().EveryOddHour(10)EverySecond schedules the job to run every 1 second.
scheduler.New().EverySecond().Do(func() {})EverySixHours schedules the job to run every six hours at the specified minute.
scheduler.New().EverySixHours(30)EveryTenMinutes schedules the job to run every 10 minutes.
scheduler.New().EveryTenMinutes().Do(func() {})EveryTenSeconds schedules the job to run every 10 seconds.
scheduler.New().EveryTenSeconds().Do(func() {})EveryThirtyMinutes schedules the job to run every 30 minutes.
scheduler.New().EveryThirtyMinutes().Do(func() {})EveryThirtySeconds schedules the job to run every 30 seconds.
scheduler.New().EveryThirtySeconds().Do(func() {})EveryThreeHours schedules the job to run every three hours at the specified minute.
scheduler.New().EveryThreeHours(20)EveryThreeMinutes schedules the job to run every 3 minutes.
scheduler.New().EveryThreeMinutes().Do(func() {})EveryTwentySeconds schedules the job to run every 20 seconds.
scheduler.New().EveryTwentySeconds().Do(func() {})EveryTwoHours schedules the job to run every two hours at the specified minute.
scheduler.New().EveryTwoHours(15)EveryTwoMinutes schedules the job to run every 2 minutes.
scheduler.New().EveryTwoMinutes().Do(func() {})EveryTwoSeconds schedules the job to run every 2 seconds.
scheduler.New().EveryTwoSeconds().Do(func() {})Hourly schedules the job to run every hour.
scheduler.New().Hourly().Do(func() {})HourlyAt schedules the job to run every hour at the specified minute.
scheduler.New().HourlyAt(5)Hours schedules the job to run every X hours.
scheduler.New().Every(6).Hours()Minutes schedules the job to run every X minutes.
scheduler.New().Every(15).Minutes()Seconds schedules the job to run every X seconds.
scheduler.New().Every(3).Seconds().Do(func() {})Shutdown gracefully shuts down the underlying scheduler.
s := scheduler.New()
_ = s.Shutdown()Start starts the underlying scheduler.
s := scheduler.New()
s.Start()Stop gracefully shuts down the scheduler.
s := scheduler.New()
_ = s.Stop()NewCacheLocker creates a CacheLocker with a cache lock client and TTL. The ttl is a lease duration: when it expires, another worker may acquire the same lock key. For long-running jobs, choose ttl >= worst-case runtime plus a safety buffer. If your runtime can exceed ttl, prefer a renewing/heartbeat lock strategy.
Example: use an in-memory cache driver
client := cache.NewCache(cache.NewMemoryStore(context.Background()))
locker := scheduler.NewCacheLocker(client, 10*time.Minute)
_, _ = locker.Lock(context.Background(), "job")Example: use the Redis cache driver
redisStore := rediscache.New(rediscache.Config{
Addr: "127.0.0.1:6379",
})
redisClient := cache.NewCache(redisStore)
redisLocker := scheduler.NewCacheLocker(redisClient, 10*time.Minute)
_, _ = redisLocker.Lock(context.Background(), "job")NewRedisLocker creates a RedisLocker with a client and TTL. The ttl is a lease duration: when it expires, another worker may acquire the same lock key. For long-running jobs, choose ttl >= worst-case runtime plus a safety buffer. If your runtime can exceed ttl, prefer a renewing/heartbeat lock strategy.
client := redis.NewClient(&redis.Options{}) // replace with your client
locker := scheduler.NewRedisLocker(client, 10*time.Minute)
_, _ = locker.Lock(context.Background(), "job")JobMetadata returns a copy of the tracked job metadata keyed by job ID.
b := scheduler.New().EverySecond().Do(func() {})
for id, meta := range b.JobMetadata() {
_ = id
_ = meta.Name
}JobsInfo returns a stable, sorted snapshot of all known job metadata. This is a facade-friendly list form of JobMetadata including paused state.
s := scheduler.New()
s.EverySecond().Name("heartbeat").Do(func() {})
for _, job := range s.JobsInfo() {
_ = job.ID
_ = job.Name
_ = job.Paused
}Name sets an explicit job name.
scheduler.New().Name("cache:refresh").HourlyAt(15)IsJobPaused reports whether a specific job is paused.
IsPausedAll reports whether global pause is enabled.
Observe registers a lifecycle observer for all scheduled jobs. Events are emitted consistently across Do, Command, and Exec jobs.
s := scheduler.New()
s.Observe(scheduler.JobObserverFunc(func(event scheduler.JobEvent) {
if event.Type == scheduler.JobSkipped && event.Reason == "paused" {
fmt.Println("skipped: paused")
}
}))PauseAll pauses execution for all scheduled jobs without removing them. This is universal across Do, Command, and Exec jobs. RunNow calls are skipped while pause is active.
s := scheduler.New()
_ = s.PauseAll()PauseJob pauses execution for a specific scheduled job. RunNow calls for that job are skipped while paused.
s := scheduler.New()
b := s.EverySecond().Name("heartbeat").Do(func() {})
_ = s.PauseJob(b.Job().ID())ResumeAll resumes execution for all paused jobs.
s := scheduler.New()
_ = s.ResumeAll()ResumeJob resumes a paused job by ID.
s := scheduler.New()
b := s.EverySecond().Name("heartbeat").Do(func() {})
_ = s.ResumeJob(b.Job().ID())RetainState allows the job to retain its state after execution.
builder := scheduler.New().EverySecond().RetainState()
builder.Do(func() {})
builder.Do(func() {})Cron sets the cron expression for the job.
builder := scheduler.New().Cron("15 3 * * *")
fmt.Println(builder.CronExpr())
// Output: 15 3 * * *Do schedules the job with the provided task function.
scheduler.New().Name("cleanup").Cron("0 0 * * *").Do(func() {})