A stock refresh that runs every weekday belongs on a calendar, rather than
being deferred again by each request. A calendar entry combines a stable
name, your execution function, and an immutable schedule.Spec describing
matching times. It does not start a timer until the scheduler runs.
Use an existing configured scheduler for the examples below. For work triggered once by a user action, use Deferred Tasks.
Defining Entries
With your application’s refreshStock function of type
func(context.Context) error, create a weekday refresh at 06:15 UTC. Import
time and arkoren.dev/core/service/schedule:
refresh := schedule.NewEntry("refresh-stock", refreshStock, func(entry *schedule.EntryBuilder) {
entry.When(schedule.UTC().Weekdays().Hour(6).Minute(15))
})
The callback edits a fresh draft; publication copies its settings and retains the execution function without calling it. Configure entries only during construction. Retaining the draft cannot change the published definition, but the function’s captured services remain shared and must outlive executions.
When is required. An omitted schedule, nil execution function, or invalid spec
is a declaration error reported by Err and scheduler.Add, not an inferred
default. Names contain 1–64 lowercase letters, digits, or hyphens, with no edge
hyphen. They share a namespace with deferred tasks.
Calendar Fields
Start with schedule.UTC() unless the job must follow a person’s local clock;
Time Zones covers named zones and daylight saving time.
Unset time fields finer than the coarsest selected field become zero, while
coarser fields match every value. For example:
daily := schedule.UTC().Hour(6)
hourly := schedule.UTC().Minute(15)
monthly := schedule.UTC().Day(1)
These mean 06:00 daily, minute :15 of every hour, and midnight on the first day
of each month. Date-only specs use midnight; a zone with no fields is invalid.
Day(31) skips months without that date rather than rolling into the next month.
If refreshes are needed on selected weekdays, pass several values to On:
twice := schedule.UTC().On(time.Monday, time.Thursday).Hour(6)
This matches 06:00 on Monday and Thursday. Day cannot be combined with On,
Weekdays, or Weekends. Field methods return independent specs, sort and
deduplicate values, and keep the first error. Setting the same field twice is
an error rather than an override.
Aligned Cadences
For repeated maintenance within each hour, choose an aligned calendar cadence:
cadence := schedule.UTC().Every(15 * time.Minute)
This matches :00, :15, :30, and :45, not fifteen minutes after startup or the
previous completion. Every accepts positive whole seconds, minutes, or hours
under 24 hours. The step must divide its unit’s cycle evenly: 15 minutes works,
7 minutes does not. Finer fields become zero; coarser fields remain unrestricted
unless you select them, such as with Weekdays().
Calendar matches are intended run times, not guaranteed delivery times. Memory does not recover missed ticks; engine recovery differs. Check the driver comparison before using ticks as a record of business work. Persist business progress separately if every period must eventually be processed.
Cron Expressions
If an existing operational schedule is already expressed as cron, use Cron
instead of the field methods:
overnight := schedule.UTC().Cron("0 2 * * 1-5")
This matches 02:00 on weekdays. Expressions have five fields, or six with seconds first. Fields accept numeric values, lists, ranges, wildcards, and steps; weekday numbers are 0–6, starting with Sunday. Named months, weekday names, and cron macros are not part of this parser.
Cron cannot be combined with field methods, including Every. If both
day-of-month and day-of-week fields are restricted in raw cron, either can
match; that differs from combining restrictions as an AND condition.
Overlap Policy
By default, another scheduled run can start while the previous one remains
active. If overlapping refreshes would compete for the same resource, construct
the entry with WithoutOverlapping and a positive timeout:
refresh := schedule.NewEntry("refresh-stock", refreshStock, func(entry *schedule.EntryBuilder) {
entry.When(schedule.UTC().Every(5 * time.Minute))
entry.WithoutOverlapping()
entry.Timeout(4 * time.Minute)
})
This is an alternative to the earlier definition. Memory skips a tick while that entry is busy in the same process; it does not coordinate other replicas. Temporal uses its native skip policy. Restate executes inline within its serialized tick invocation, so a pending tick waits rather than overlapping that invocation. These are driver policies, not a cross-replica lock around all application effects.
RunNow bypasses those guards. A retried or duplicated delivery can repeat
effects even without concurrent runs. Make the function idempotent, and use
application-level locking or fencing when correctness requires it.
Timeout defaults to zero, meaning no facade execution limit; negative values
are invalid. A positive value cancels the supplied context after that duration.
The function must observe the context or pass it to I/O: no scheduler timeout
forcibly stops code. Engine drivers also have native execution policies on
Drivers. A function returning nil after cancellation still counts
as successful; see Error Handling.
Registering Entries
After constructing the definitions, register them before Run:
err := scheduler.Add(refresh)
Add accepts multiple entries atomically. An invalid definition or duplicate
name rejects the whole batch, leaving existing registrations unchanged. Starting
Run closes registration permanently for that facade.
Construction callbacks run synchronously once in order, and later callbacks can replace valid timing or timeout settings. The first invalid setting cannot be repaired by a later callback. A supplied nil callback or callback panic propagates; runtime handler panic recovery does not apply during construction.
To inspect a future match without starting work, use Next with an existing
time named after:
next := refresh.Next(after)
The result is strictly after after, or zero if the entry is invalid or no
match exists within five years. This uses the core calendar, not an engine
delivery prediction. Start work with the scheduler runner.
For every function and option, see the schedule API reference.