aws-eventbridge-cron
Parse AWS EventBridge cron, rate, and one-time expressions and compute their
future run times.
Status: early preview. Expect small API tweaks before a 1.0.0 release.
Features
- Single entry point:
AWS.EventBridge.Cron exports the CronExprT type,
parseCronText parser, and nextRunTimes scheduler.
- Full support for EventBridge-specific syntax such as
? wildcards, L, LW,
weekday ranges, and nth-weekday modifiers (2#1).
rate(...) and at(...) expressions share the same API, so callers do not
need to branch on expression variants.
- Schedule introspection helpers:
scheduleKind returns a ScheduleKind, and
isRecurring distinguishes recurring (cron/rate) expressions from
at(...) one-time schedules.
- Timezone-aware helpers:
AWS.EventBridge.Schedule pairs expressions with an
IANA timezone (via the tz/tzdata packages) and exposes nextRunTimes
variants for UTC, local, or fully-zoned outputs.
- Convenience constructors accept canonical IANA names (
"America/New_York")
in addition to the generated TZLabel constructors.
- Extensive property-based test suite that mirrors the behaviour documented by
AWS.
Installation
cabal install aws-eventbridge-cron
Or add the package to your component:
build-depends:
aws-eventbridge-cron >= 0.1 && < 0.2
Quick Start
import AWS.EventBridge.Cron
import Data.Time (UTCTime(..), fromGregorian)
import Data.Time.LocalTime (TimeOfDay(..), timeOfDayToTime)
base :: UTCTime
base = UTCTime (fromGregorian 2025 11 16) (timeOfDayToTime (TimeOfDay 9 0 0))
example :: Either String [UTCTime]
example = do
expr <- parseCronText "cron(0/15 9 ? NOV SUN 2025)"
nextRunTimes expr base 4
-- Right [2025-11-16 09:00:00 UTC, 2025-11-16 09:15:00 UTC, ...]
The parser also accepts rate(...) and at(...) expressions:
rateExample :: Either String [UTCTime]
rateExample = do
expr <- parseCronText "rate(10 minutes)"
nextRunTimes expr base 3
atExample :: Either String [UTCTime]
atExample = do
expr <- parseCronText "at(2025-11-16T09:30:00)"
nextRunTimes expr base 5
-- Introspect the parsed expression without re-parsing downstream.
kindExample :: Either String ScheduleKind
kindExample = scheduleKind <$> parseCronText "rate(5 minutes)"
-- Right RateSchedule
isRecurringExample :: Either String Bool
isRecurringExample = isRecurring <$> parseCronText "at(2025-11-16T09:30:00)"
-- Right False
Timezone-Aware Schedules
EventBridge rules can set a schedule timezone. Use AWS.EventBridge.Schedule to
bind an expression to a TZLabel so you can request run times in UTC, as local
wall-clock values, or as ZonedTimes tagged with the appropriate offset.
import AWS.EventBridge.Schedule
import Data.Time (UTCTime(..), LocalTime, ZonedTime, fromGregorian)
import Data.Time.Zones.All (TZLabel(..))
baseUTC :: UTCTime
baseUTC = UTCTime (fromGregorian 2025 11 1) 0
zonedSchedule :: Either String Schedule
zonedSchedule = scheduleFromText America__New_York "cron(0 9 * NOV ? 2025)"
utcRuns :: Either String [UTCTime]
utcRuns = nextRunTimesUTC <$> zonedSchedule <*> pure baseUTC <*> pure 2
-- Right [2025-11-01 13:00:00 UTC,2025-11-02 14:00:00 UTC]
localRuns :: Either String [LocalTime]
localRuns = nextRunTimesLocalFromUTC <$> zonedSchedule <*> pure baseUTC <*> pure 2
-- Right [2025-11-01 09:00:00,2025-11-02 09:00:00]
zonedRuns :: Either String [ZonedTime]
zonedRuns = nextRunTimesZonedFromUTC <$> zonedSchedule <*> pure baseUTC <*> pure 2
-- Right [2025-11-01 09:00:00 EDT,2025-11-02 09:00:00 EST]
Every combination of base input (UTCTime, LocalTime, ZonedTime) and output
form is available, so you can normalize timestamps at the edges of your system
and avoid comparing values that silently belong to different timezones.
API Overview
- Parse using
parseCronText (UTC) or scheduleFromText (timezone-aware).
- Wrap with
scheduleFromExpr/scheduleFromText if you need timezone metadata.
- Choose an evaluation helper based on the base input you have and the output
you need. Prefer the primary trio (
nextRunTimesUTC, nextRunTimesLocal,
nextRunTimesZoned) and reach for the conversion helpers when you want to
avoid manual conversions.
| Base input |
Output |
Function |
UTCTime | UTCTime | nextRunTimesUTC |
LocalTime | UTCTime | nextRunTimesUTCFromLocal |
ZonedTime | UTCTime | nextRunTimesUTCFromZoned |
UTCTime | LocalTime | nextRunTimesLocalFromUTC |
LocalTime | LocalTime | nextRunTimesLocal |
ZonedTime | LocalTime | nextRunTimesLocalFromZoned |
UTCTime | ZonedTime | nextRunTimesZonedFromUTC |
LocalTime | ZonedTime | nextRunTimesZonedFromLocal |
ZonedTime | ZonedTime | nextRunTimesZoned |
Use the conversion helpers when you already have a base timestamp in a specific
representation and want the library to handle the translation for you (for
example, UI-provided LocalTime that needs to be compared against UTC events).
Working With IANA Names
Prefer scheduleFromText/scheduleFromExpr when compiled code can depend on
TZLabel constructors (they offer total coverage at compile time). When
configurations or API payloads give you canonical timezone names, switch to the
IANA-aware wrappers:
scheduleFromExprIANA :: Text -> CronExprT -> Either String Schedule
scheduleFromTextIANA :: Text -> Text -> Either String Schedule
parseCronTextWithIANA :: Text -> Text -> Either String Schedule
These helpers validate the provided timezone name against the bundled tz
database and return Left if it is unknown, preventing silent fallbacks.
Example: parse config payloads with canonical names
import AWS.EventBridge.Schedule
import Data.Text (Text)
import Data.Time (LocalTime)
mkScheduleFromConfig :: Text -> Text -> Either String Schedule
mkScheduleFromConfig tzName exprText =
scheduleFromTextIANA tzName exprText
example :: Either String [LocalTime]
example = do
sched <- mkScheduleFromConfig "Asia/Kolkata" "cron(0 9 * * ? *)"
nextRunTimesLocal sched (read "2025-11-15 09:00:00" :: LocalTime) 2
Example: fall back to a default timezone
import AWS.EventBridge.Schedule
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Time (UTCTime)
resolveSchedule :: Maybe Text -> Text -> Either String Schedule
resolveSchedule maybeName exprText = do
let tzName = fromMaybe "UTC" maybeName
scheduleFromTextIANA tzName exprText
fromApi :: Either String [UTCTime]
fromApi = do
sched <- resolveSchedule (Just "America/Los_Angeles") "cron(0 9 * * ? *)"
nextRunTimesUTC sched (read "2025-11-01 17:00:00 UTC" :: UTCTime) 1
Error Reporting
Parser and evaluator failures return Left String with human-readable error
messages:
Left "day-of-month and day-of-week fields must use '?' in exactly one position"
The messages mirror the constraints enforced by EventBridge when you create
scheduled rules.
Design Notes
CronExprT is intentionally opaque. Construct values with parseCronText and
feed them into nextRunTimes.
- Scheduling honours the EventBridge rule that exactly one of day-of-month or
day-of-week must be
?.
- Results are monotonic, capped at the requested limit, and never fall before
the supplied base time.
See test/AWS/EventBridge/CronSpec.hs for more examples and edge cases.
Development
cabal build
cabal test
cabal bench
cabal haddock --open
cabal bench executes a Criterion suite that profiles cron-heavy workloads,
rate schedules, and timezone-aware helpers so you can gauge regression risk
when modifying the evaluator.
Contributing
Bug reports, suggestions, and pull requests are welcome. Please open an issue
before large-scale changes so we can keep the API coherent.
License
Released under the BSD-3-Clause license. See LICENSE for details.
References