GitHub Agentic Workflows

Fuzzy Schedule Time Syntax Specification

Version: 1.2.0 Status: Draft Specification
Latest Version: fuzzy-schedule-specification
Editor: GitHub Agentic Workflows Team


This specification defines the Fuzzy Schedule Time Syntax, a human-friendly scheduling language for GitHub Agentic Workflows that automatically distributes workflow execution times to prevent server load spikes. The syntax supports daily, hourly, weekly, and interval-based schedules with optional time constraints and timezone conversions. The specification includes a deterministic scattering algorithm that uses hash functions to assign consistent execution times to workflows based on their identifiers, ensuring predictable behavior across multiple compilations while distributing load across an organization’s infrastructure.

This section describes the status of this document at the time of publication. This is a draft specification and may be updated, replaced, or made obsolete by other documents at any time.

This document is governed by the GitHub Agentic Workflows project specifications process.

  1. Introduction
  2. Conformance
  3. Core Syntax
  4. Time Specifications
  5. Timezone Support
  6. Scattering Algorithm
  7. Cron Expression Generation
  8. Safeguards
  9. Error Handling
  10. Compliance Testing
  11. Sync Notes
  12. Calendar Output Schema

The Fuzzy Schedule Time Syntax addresses the problem of server load spikes that occur when multiple workflows execute simultaneously using fixed-time schedules. Traditional cron expressions require explicit time specifications, leading developers to commonly use convenient times (e.g., midnight, on-the-hour) that create load concentration. This specification defines a natural language syntax that automatically distributes execution times while preserving schedule semantics.

This specification covers:

  • Natural language schedule expressions for daily, hourly, weekly, and interval-based schedules
  • Time constraint syntax using around and between modifiers
  • Timezone conversion syntax for local-to-UTC time translation
  • Deterministic scattering algorithm for execution time distribution
  • Cron expression generation from fuzzy syntax
  • Validation requirements and error handling

This specification does NOT cover:

  • Standard cron expression syntax (handled by GitHub Actions)
  • Monthly or yearly schedule patterns
  • Dynamic schedule adjustment based on load metrics
  • Schedule conflict resolution between workflows

This specification prioritizes:

  1. Human readability: Natural language expressions that clearly communicate intent
  2. Load distribution: Automatic scattering prevents simultaneous workflow execution
  3. Determinism: Same workflow identifier always produces same execution time
  4. Predictability: Execution times remain consistent across recompilations
  5. Timezone awareness: Support for local time specifications with UTC conversion

A conforming implementation is a parser that satisfies all MUST, MUST NOT, REQUIRED, SHALL, and SHALL NOT requirements in this specification.

A conforming fuzzy schedule expression is a schedule string that conforms to the syntax grammar defined in Section 3 and produces a valid fuzzy cron placeholder.

A conforming scattering implementation is an implementation that satisfies all scattering algorithm requirements in Section 6.

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Level 1 (Basic): Supports daily and weekly schedules without time constraints

Level 2 (Standard): Adds support for time constraints (around, between) and hourly schedules

Level 3 (Complete): Includes timezone conversion, interval schedules, and bi-weekly/tri-weekly patterns


A fuzzy schedule expression MUST conform to the following ABNF grammar:

fuzzy-schedule = daily-schedule / hourly-schedule / weekly-schedule / interval-schedule
daily-schedule = "daily" [time-constraint]
weekly-schedule = "weekly" ["on" weekday] [time-constraint]
hourly-schedule = "hourly" / ("every" hour-interval)
interval-schedule = "every" (minute-interval / hour-interval / day-interval / week-interval)
time-constraint = around-constraint / between-constraint
around-constraint = "around" time-spec
between-constraint = "between" time-spec "and" time-spec
time-spec = (hour-24 ":" minute) [utc-offset]
/ (hour-12 am-pm) [utc-offset]
/ time-keyword [utc-offset]
time-keyword = "midnight" / "noon"
am-pm = "am" / "pm"
utc-offset = "utc" ("+" / "-") (hours / hours ":" minutes)
weekday = "sunday" / "monday" / "tuesday" / "wednesday"
/ "thursday" / "friday" / "saturday"
hour-24 = 1*2DIGIT ; 0-23
hour-12 = 1*2DIGIT ; 1-12
minute = 2DIGIT ; 00-59
hours = 1*2DIGIT
minutes = 2DIGIT
minute-interval = 1*DIGIT ("m" / "minutes" / "minute")
hour-interval = 1*DIGIT ("h" / "hours" / "hour")
day-interval = 1*DIGIT ("d" / "days" / "day")
week-interval = 1*DIGIT ("w" / "weeks" / "week")

A basic daily schedule expression SHALL take the form:

daily

An implementation MUST generate a fuzzy cron placeholder: FUZZY:DAILY * * *

The execution time SHALL be deterministically scattered across all 24 hours and 60 minutes of the day.

A daily around schedule expression SHALL take the form:

daily around <time-spec>

An implementation MUST generate a fuzzy cron placeholder: FUZZY:DAILY_AROUND:HH:MM * * *

The execution time SHALL be scattered within a ±60 minute window around the specified time.

Example:

daily around 14:00
# Generates: FUZZY:DAILY_AROUND:14:0 * * *
# Scatters within window: 13:00 to 15:00

A daily between schedule expression SHALL take the form:

daily between <start-time> and <end-time>

An implementation MUST generate a fuzzy cron placeholder: FUZZY:DAILY_BETWEEN:START_H:START_M:END_H:END_M * * *

The execution time SHALL be scattered uniformly within the specified time range, including handling of midnight-crossing ranges.

Example:

daily between 9:00 and 17:00
# Generates: FUZZY:DAILY_BETWEEN:9:0:17:0 * * *
# Scatters within window: 09:00 to 17:00
daily between 22:00 and 02:00
# Generates: FUZZY:DAILY_BETWEEN:22:0:2:0 * * *
# Scatters within window: 22:00 to 02:00 (crossing midnight)

A basic weekly schedule expression SHALL take the form:

weekly

An implementation MUST generate a fuzzy cron placeholder: FUZZY:WEEKLY * * *

The execution SHALL be scattered across all seven days of the week and all hours/minutes of each day.

A weekly day schedule expression SHALL take the form:

weekly on <weekday>

An implementation MUST generate a fuzzy cron placeholder: FUZZY:WEEKLY:DOW * * DOW

Example:

weekly on monday
# Generates: FUZZY:WEEKLY:1 * * 1
# Scatters across all hours on Monday

A weekly schedule MAY include time constraints using around or between:

weekly on <weekday> around <time-spec>
weekly on <weekday> between <start-time> and <end-time>

Example:

weekly on friday around 17:00
# Generates: FUZZY:WEEKLY:5:AROUND:17:0 * * 5
# Scatters Friday 16:00-18:00

A basic hourly schedule expression SHALL take the form:

hourly

An implementation MUST generate a fuzzy cron placeholder: FUZZY:HOURLY * * *

The minute offset SHALL be scattered across 0-59 minutes but remain consistent for each hour.

Example:

hourly
# Generates: FUZZY:HOURLY * * *
# Might scatter to: 43 * * * * (runs at minute 43 every hour)

An hour interval schedule expression SHALL take the form:

every <N>h
every <N> hours
every <N> hour

Where <N> MUST be a positive integer.

An implementation MUST generate a fuzzy cron placeholder: FUZZY:HOURLY:<N> * * *

Valid hour intervals SHOULD be: 1, 2, 3, 4, 6, 8, 12 (factors of 24 for even distribution).

Example:

every 2h
# Generates: FUZZY:HOURLY:2 * * *
# Might scatter to: 53 */2 * * * (runs at minute 53 every 2 hours)

A bi-weekly schedule expression SHALL take the form:

bi-weekly

An implementation MUST generate a fuzzy cron placeholder: FUZZY:BI-WEEKLY * * *

The schedule SHALL execute once every 14 days with scattered time.

A tri-weekly schedule expression SHALL take the form:

tri-weekly

An implementation MUST generate a fuzzy cron placeholder: FUZZY:TRI-WEEKLY * * *

The schedule SHALL execute once every 21 days with scattered time.

An interval schedule expression SHALL take the form:

every <N> <unit>

Where:

  • <N> MUST be a positive integer
  • <unit> MUST be one of: minutes, minute, m, hours, hour, h, days, day, d, weeks, week, w

An implementation MUST generate appropriate cron expressions based on the unit:

  • Minutes: */N * * * * (minimum N=5 per GitHub Actions constraint)
  • Hours: FUZZY:HOURLY:N * * * (scattered minute)
  • Days: 0 0 */N * * (fixed midnight)
  • Weeks: 0 0 */N*7 * * (fixed Sunday midnight)

Example:

every 5 minutes
# Generates: */5 * * * *
every 6h
# Generates: FUZZY:HOURLY:6 * * *
every 2 days
# Generates: 0 0 */2 * *

3.7 Error Norms for Invalid Schedule Expressions

Section titled “3.7 Error Norms for Invalid Schedule Expressions”

The following table specifies normative behavior (MUST/SHALL requirements) for malformed or unrecognizable fuzzy schedule expressions encountered during compilation. These norms apply at parse time (when the compiler processes the workflow frontmatter) and at test time (when the compliance test suite exercises the parser with invalid inputs).

#Error ConditionInput ExampleMUST/SHALL BehaviorError Code
E-01Unknown schedule keyword (not one of daily, weekly, hourly, bi-weekly, tri-weekly, every)monthlyImplementation MUST reject with a descriptive error naming the unrecognized keyword and listing valid keywordsUNKNOWN_KEYWORD
E-02Out-of-range hour in 24-hour formatdaily around 25:00Implementation MUST reject; the error message MUST state the valid hour range (0–23) and the offending valueHOUR_OUT_OF_RANGE
E-03Out-of-range minutedaily around 14:65Implementation MUST reject; the error message MUST state the valid minute range (0–59) and the offending valueMINUTE_OUT_OF_RANGE
E-04around keyword with no time specificationdaily aroundImplementation MUST reject; the error message MUST include an example of correct around usageMISSING_TIME_SPEC
E-05between keyword with only one time argumentdaily between 9:00Implementation MUST reject; the error message MUST state that between requires both a start and an end time connected by andINCOMPLETE_RANGE
E-06between range where start equals enddaily between 14:00 and 14:00Implementation MUST reject; a zero-duration window cannot scatter execution timesZERO_DURATION_RANGE
E-07Unknown weekday in weekly on <day>weekly on mondeyImplementation MUST reject with a did-you-mean suggestion when the input differs from a valid weekday by one characterUNKNOWN_WEEKDAY
E-08Invalid interval unitevery 5 fortnightsImplementation MUST reject; the error message MUST list valid units (minutes, hours, days, weeks and their abbreviations)UNKNOWN_UNIT
E-09Interval value below minimum allowed by GitHub Actionsevery 2 minutesImplementation MUST reject; the error message MUST state the minimum permitted interval (5 minutes for the minutes unit) and the GitHub Actions constraint sourceINTERVAL_TOO_SMALL
E-10Non-integer interval valueevery 1.5 hoursImplementation MUST reject; fractional interval values are not supportedNON_INTEGER_INTERVAL

Normative notes:

  • All error messages MUST be directed to the user’s console (stderr) and MUST be human-readable.
  • Implementations MUST NOT silently fall back to a default schedule when the input is invalid; all errors in rows E-01 through E-10 MUST cause compilation to fail with a non-zero exit code.
  • Implementations SHOULD NOT attempt automatic correction of the schedule expression. Actionable correction guidance in the error message is preferred over silent fixup.

An implementation MUST support the following time formats:

The 24-hour format SHALL use the pattern HH:MM:

  • Hours MUST be in range 0-23
  • Minutes MUST be in range 0-59
  • Leading zeros MAY be omitted for hours
  • Minutes MUST use two digits with leading zero if necessary

Valid examples: 00:00, 9:30, 14:00, 23:59

The 12-hour format SHALL use the pattern H[H]am or H[H]pm:

  • Hours MUST be in range 1-12
  • AM/PM indicator MUST be lowercase am or pm
  • Minutes MAY be omitted (defaults to :00)
  • Colon and minutes MAY be included (e.g., 3:30pm)

Valid examples: 1am, 12pm, 11pm, 9am, 3:30pm

Conversion rules:

  • 12am converts to 00:00 (midnight)
  • 12pm converts to 12:00 (noon)
  • 1am-11am converts to 01:00-11:00
  • 1pm-11pm converts to 13:00-23:00

An implementation MUST support the following time keywords:

  • midnight: Represents 00:00 (start of day)
  • noon: Represents 12:00 (middle of day)

Keywords MUST be case-insensitive.

When using around <time>, the implementation MUST use a ±60 minute window centered on the specified time.

The window MUST handle day boundaries correctly:

  • around 00:30 creates window: 23:30 (previous day) to 01:30
  • around 23:30 creates window: 22:30 to 00:30 (next day)

When using between <start> and <end>, the implementation MUST:

  1. Accept ranges within a single day (e.g., 9:00 to 17:00)
  2. Accept ranges crossing midnight (e.g., 22:00 to 02:00)
  3. Calculate range size correctly for midnight-crossing ranges
  4. Distribute scattered times uniformly within the range

For midnight-crossing ranges where start > end:

  • Range size = (24*60 - start_minutes) + end_minutes

Example:

between 22:00 and 02:00
# Range: 22:00, 22:01, ..., 23:59, 00:00, ..., 02:00
# Duration: 4 hours (240 minutes)

An implementation MUST support UTC offset specifications using the format:

utc-offset = "utc" ("+" / "-") offset-value
offset-value = hours / hours ":" minutes

Where:

  • hours MAY be 1 or 2 digits
  • minutes MUST be 2 digits when specified
  • Offset MUST be in range UTC-12:00 to UTC+14:00

Valid examples: utc+9, utc-5, utc+05:30, utc-08:00

When a UTC offset is specified, the implementation MUST:

  1. Parse the local time value
  2. Parse the UTC offset value (in minutes)
  3. Subtract the offset from the local time to get UTC time
  4. Handle day wrapping correctly

Formula: UTC_time = local_time - offset

Example:

local_time = 14:00 (2 PM)
offset = +9 hours (JST)
UTC_time = 14:00 - 9:00 = 05:00 (5 AM UTC)

The implementation MUST handle day boundaries when converting times:

  • Negative results MUST wrap to previous day (add 24 hours)
  • Results ≥24:00 MUST wrap to next day (subtract 24 hours)
  • Wrap operations MUST preserve minute precision

Example:

local_time = 02:00 (2 AM)
offset = +9 hours
UTC_time = 02:00 - 9:00 = -7:00 → 17:00 (previous day)

An implementation SHOULD recognize common timezone abbreviations:

AbbreviationUTC OffsetNotes
PSTUTC-8Pacific Standard Time
PDTUTC-7Pacific Daylight Time
ESTUTC-5Eastern Standard Time
EDTUTC-4Eastern Daylight Time
JSTUTC+9Japan Standard Time
ISTUTC+5:30India Standard Time

Implementations MAY issue warnings for ambiguous abbreviations (e.g., “PT” could be PST or PDT).


The scattering algorithm MUST provide:

  1. Determinism: Same workflow identifier produces same scattered time
  2. Distribution: Scattered times distribute evenly across the allowed range
  3. Stability: Scattered times remain constant across recompilations
  4. Uniqueness: Different workflow identifiers produce different scattered times

The scattering algorithm uses the following formal input entities:

EntityTypeConstraintsDescription
workflow_identifierstringMUST be non-empty; SHOULD use owner/repo/path/to/workflow.md formatCanonical identifier hashed for deterministic scatter selection
schedule_stringstringMUST match a supported fuzzy placeholder form (FUZZY:*)Parsed schedule expression that determines algorithm branch
seedunsigned 32-bit integerMUST be derived deterministically from workflow_identifier using the configured hash functionHash-derived seed used for modulo operations
window_minutesintegerMUST be positive; MUST NOT exceed 1440Candidate-minute search window for around/between scattering

An implementation MUST use a hash function that satisfies the following requirements:

  1. Determinism: The hash function MUST produce the same output for the same input across all platforms and executions
  2. Distribution: The hash function SHOULD produce uniformly distributed outputs across the hash space
  3. Stability: The hash function MUST NOT change behavior across different versions of the implementation
  4. Integer output: The hash function MUST produce an integer output suitable for modulo operations

An implementation SHOULD use the FNV-1a (Fowler-Noll-Vo) 32-bit hash algorithm as a reference implementation:

hash = FNV_offset_basis
for each byte in input:
hash = hash XOR byte
hash = hash * FNV_prime
return hash
Where:
FNV_offset_basis = 2166136261 (0x811c9dc5)
FNV_prime = 16777619 (0x01000193)

Other suitable hash functions MAY be used, such as MurmurHash, xxHash, or CityHash, provided they meet the above requirements.

The workflow identifier used for hashing MUST be constructed as:

workflow_identifier = repository_slug + "/" + workflow_file_path

Where:

  • repository_slug is the format owner/repo
  • workflow_file_path is the relative path from repository root

Example: github/gh-aw/.github/workflows/daily-report.md

This format ensures workflows with the same filename in different repositories receive different execution times.

For FUZZY:DAILY * * * and FUZZY:DAILY_WEEKDAYS * * *, an implementation MUST use the weighted daily time slot pool to select execution time:

  1. Construct a weighted pool of (hour, minute) time slots using three preference tiers:
    • BEST (weight 3): hours 02–05 UTC, odd minutes {7, 13, 23, 37, 43, 53} → 72 slots
    • GOOD (weight 2): hours 10–12 UTC, minutes [5, 54] → 300 slots
    • OK (weight 1): hours 19–23 UTC, minutes [5, 54] → 250 slots
    • Total pool size: 622 slots
  2. Select slot: index = hash(workflow_identifier) % pool_size
  3. Extract (hour, minute) from the selected slot
  4. Generate cron: <minute> <hour> * * * (or * * 1-5 for weekday variant)

The pool is pre-computed once. Because each tier appears proportionally in the pool, a randomly selected slot is 3× more likely to land in the BEST window than in the OK window.

Example:

pool_size = 622
hash("github/gh-aw/workflow.md") % 622 = 84
slot[84] = (hour=2, minute=23) # BEST tier
cron = "23 2 * * *" (2:23 AM UTC)

For FUZZY:DAILY_AROUND:HH:MM * * *:

  1. Calculate target time in minutes: target_minutes = HH * 60 + MM
  2. Define window: [-60, +59] minutes from target
  3. Calculate hash modulo 120 (window size)
  4. Calculate offset: offset = hash_result - 60
  5. Calculate scattered time: scattered_minutes = target_minutes + offset
  6. Handle day wrapping (keep within 0-1439)
  7. Convert to hour and minute

Example:

target = 14:00 (840 minutes)
hash % 120 = 73
offset = 73 - 60 = 13
scattered = 840 + 13 = 853 minutes
hour = 853 / 60 = 14
minute = 853 % 60 = 13
cron = "13 14 * * *" (2:13 PM, within 13:00-15:00 window)

For FUZZY:DAILY_BETWEEN:START_H:START_M:END_H:END_M * * *:

  1. Calculate start and end times in minutes
  2. Calculate range size (handling midnight crossing)
  3. Calculate hash modulo range_size
  4. Add hash_result to start_minutes
  5. Handle day wrapping
  6. Convert to hour and minute

For midnight-crossing ranges (start > end):

range_size = (24 * 60 - start_minutes) + end_minutes

Example:

range = 9:00 to 17:00
start_minutes = 540, end_minutes = 1020
range_size = 1020 - 540 = 480 minutes (8 hours)
hash % 480 = 217
scattered = 540 + 217 = 757 minutes
hour = 757 / 60 = 12
minute = 757 % 60 = 37
cron = "37 12 * * *" (12:37 PM)

For FUZZY:HOURLY * * *:

  1. Calculate hash modulo 60
  2. Use result as minute offset
  3. Generate cron: <minute> * * * *

Example:

hash % 60 = 43
cron = "43 * * * *" (runs at minute 43 every hour)

For FUZZY:HOURLY:N * * *:

  1. Calculate hash modulo 60
  2. Use result as minute offset
  3. Generate cron: <minute> */N * * *

Example:

interval = 2 hours
hash % 60 = 53
cron = "53 */2 * * *" (runs at minute 53 every 2 hours)

For FUZZY:WEEKLY * * * and FUZZY:WEEKLY:DOW * * *:

  1. Select day-of-week: weekday = hash(workflow_identifier) % 7 (0=Sunday, 6=Saturday)
    For FUZZY:WEEKLY:DOW, the day is fixed from the expression instead.
  2. Select time from the weighted daily time slot pool (Section 6.3.1)
  3. Generate cron: <minute> <hour> * * <day>

Both patterns use the same weighted pool as the daily schedule, ensuring execution times prefer the BEST/GOOD/OK tiers rather than distributing flatly across the full day.

Example:

weekly on monday
day = 1 (Monday)
pool selection → (hour=2, minute=23) # BEST tier
cron = "23 2 * * 1" (Monday 2:23 AM UTC)

For FUZZY:BI-WEEKLY * * * and FUZZY:TRI-WEEKLY * * *:

  1. Select time from the weighted daily time slot pool (Section 6.3.1)
  2. Generate cron: <minute> <hour> */14 * * (bi-weekly) or <minute> <hour> */21 * * (tri-weekly)

Both patterns use the same weighted pool to ensure execution during preferred low-traffic windows.

To reduce scheduling collisions with other commonly-scheduled cron jobs, implementations MUST apply two minute-avoidance passes after computing the raw scattered minute value.

6.4.1 Hour Boundary Avoidance (avoidHourBoundary)

Section titled “6.4.1 Hour Boundary Avoidance (avoidHourBoundary)”

Minutes near the hour boundary (0–4 and 55–59) are subject to elevated load on GitHub Actions infrastructure, especially at 00:00 UTC.

An implementation MUST remap minute values as follows:

Input rangeOutput
[0, 4]minute + 5
[55, 59]minute − 5
[5, 54]unchanged

This ensures all generated minute values are in [5, 54].

Scope: Applied to ALL targeted-scatter patterns (DAILY_AROUND, DAILY_BETWEEN, WEEKLY_AROUND, and their weekday variants).

6.4.2 Peak Minutes Avoidance (avoidPeakMinutes)

Section titled “6.4.2 Peak Minutes Avoidance (avoidPeakMinutes)”

Known high-traffic periods require avoidance of minutes that fall within ±3 of the peak minute values.

An implementation MUST apply the following remapping after avoidHourBoundary:

ConditionAvoid rangeReplacement
hour ∈ [6, 9] AND minute ∈ [27, 33][27, 33]34
hour ∈ [14, 18] AND minute ∈ [12, 18][12, 18]19
hour ∈ [14, 18] AND minute ∈ [42, 48][42, 48]49

Rationale:

  • EU morning peak (06:00–09:59 UTC): :30 is a commonly-used cron minute. Staying 3 minutes away (avoiding [27,33]) reduces collisions.
  • US business hours (14:00–18:59 UTC): :15 and :45 are quarter-hour marks widely used by monitoring and reporting cron jobs. Staying 3 minutes away (avoiding [12,18] and [42,48]) reduces collisions.

Application order: avoidHourBoundary MUST be applied before avoidPeakMinutes.

Scope: avoidPeakMinutes applies only to targeted-scatter patterns. Full-day scatter patterns that use the weighted pool (Section 6.3.1) already avoid peak windows by construction, since the pool does not include EU peak hours (06–09) or US peak hours (14–18).

Example:

FUZZY:DAILY_AROUND:14:00, workflow "my-scanner"
Raw scattered time: 14:28
Step 1 (avoidHourBoundary): 28 → 28 (no change; 28 ∈ [5,54])
Step 2 (avoidPeakMinutes): 28 → 34 (shifted; hour ∈ [14,18], minute 28 ∈ [27,33]
— wait, hour=14, so EU rule doesn't apply;
US :15 rule: 28 ∉ [12,18]; :45 rule: 28 ∉ [42,48])
→ no shift needed; result: 14:28
FUZZY:DAILY_AROUND:15:00, workflow "my-monitor"
Raw scattered time: 15:13
Step 1 (avoidHourBoundary): 13 → 13 (no change)
Step 2 (avoidPeakMinutes): 13 → 19 (shifted; hour ∈ [14,18], minute 13 ∈ [12,18])
→ result: 15:19

An implementation MUST ensure:

  1. Hash function produces same output for same input across platforms
  2. Modulo operations use consistent integer division
  3. Day wrapping uses consistent addition/subtraction rules
  4. Minute and hour extraction uses consistent division and modulo operations
  5. avoidHourBoundary is applied before avoidPeakMinutes for all targeted-scatter patterns
  6. Full-day scatter patterns use the weighted daily time slot pool (Section 6.3.1)

An implementation MUST generate fuzzy cron placeholders that can be resolved later by the scattering algorithm. Placeholders MUST use the format:

FUZZY:<TYPE>[:<PARAMS>] <cron-fields>

Where:

  • <TYPE> identifies the schedule type
  • <PARAMS> provides optional parameters (time, day, range)
  • <cron-fields> includes remaining cron fields (typically * * *)
Schedule TypePlaceholder Format
DailyFUZZY:DAILY * * *
Daily aroundFUZZY:DAILY_AROUND:HH:MM * * *
Daily betweenFUZZY:DAILY_BETWEEN:SH:SM:EH:EM * * *
HourlyFUZZY:HOURLY * * *
Hour intervalFUZZY:HOURLY:N * * *
WeeklyFUZZY:WEEKLY * * *
Weekly with dayFUZZY:WEEKLY:DOW * * DOW
Weekly day aroundFUZZY:WEEKLY:DOW:AROUND:HH:MM * * DOW
Weekly day betweenFUZZY:WEEKLY:DOW:BETWEEN:SH:SM:EH:EM * * DOW
Bi-weeklyFUZZY:BI-WEEKLY * * *
Tri-weeklyFUZZY:TRI-WEEKLY * * *

An implementation MUST provide a mechanism to resolve fuzzy placeholders to concrete cron expressions using the scattering algorithm and workflow identifier.

The resolution process MUST:

  1. Detect fuzzy placeholder format
  2. Extract schedule type and parameters
  3. Apply appropriate scattering algorithm
  4. Generate valid 5-field cron expression
  5. Validate resulting cron expression

Generated cron expressions MUST conform to GitHub Actions cron syntax:

  • 5 fields: minute hour day-of-month month day-of-week
  • Minutes: 0-59 or * or */N
  • Hours: 0-23 or * or */N
  • Day-of-month: 1-31 or * or */N
  • Month: 1-12 or * or */N
  • Day-of-week: 0-6 (Sunday=0) or *

The following safeguards are normative and apply to all scattering implementations.

R-SAFE-001: Implementations MUST enforce finite scatter windows. For around schedules, the effective jitter window MUST NOT exceed ±60 minutes from the requested anchor time. For between schedules, the scattered time MUST remain inside the declared closed interval.

R-SAFE-002: Implementations MUST apply collision-avoidance normalization before returning the final minute value. At minimum, the implementation MUST avoid hour-boundary hotspots and known quarter-hour peaks as defined by Section 6.4. This guarantee is deterministic for a given workflow identifier and schedule expression.

R-SAFE-003: If hash input material is empty (for example, missing workflow identifier), the implementation MUST fail with a descriptive error and MUST NOT fall back to random scattering.

R-SAFE-004: If non-unique hash input causes repeated collisions across workflows, the implementation MUST preserve deterministic behavior and SHOULD emit a warning indicating reduced distribution quality. Implementations MUST NOT silently switch to non-deterministic fallbacks to hide collisions.


An implementation MUST reject invalid expressions with clear error messages:

Error: Unknown schedule type 'monthly'
Valid types: daily, weekly, hourly, bi-weekly, tri-weekly, every
Error: Invalid time format '25:00' in 'daily around 25:00'
Time must be in 24-hour format (HH:MM, 0-23 hours) or 12-hour format with am/pm
Error: Unknown weekday 'mondey' in 'weekly on mondey'
Valid weekdays: sunday, monday, tuesday, wednesday, thursday, friday, saturday
Error: Invalid interval '5' in 'every 5h'
Valid hour intervals: 1h, 2h, 3h, 4h, 6h, 8h, 12h
Error: 'around' requires a time specification
Example: daily around 14:00
Error: 'daily at <time>' syntax is not supported
Use 'daily around <time>' for fuzzy scheduling within ±1 hour window

An implementation SHOULD issue warnings for valid but suboptimal patterns:

Warning: Consider using 'every 2h' instead of fixed interval
Fixed intervals create load spikes when many workflows run simultaneously

An implementation SHOULD NOT attempt to correct syntax errors automatically. All errors MUST be reported to the user with actionable correction guidance.

The following edge-case norms are mandatory in addition to §§9.1–9.4:

  1. Invalid scatter seed: If seed derivation produces an empty, negative, or non-integer value, the implementation MUST fail compilation with a descriptive error and MUST NOT fall back to a random or default seed.
  2. Out-of-range time values: Inputs containing hour values outside 0..23 (24-hour), minute values outside 0..59, or 12-hour values outside 1..12 MUST be rejected with an error that includes the offending token and valid range.
  3. Malformed grammar input: Expressions that violate the ABNF in §3.1 (e.g., missing and in between, dangling modifiers, extra tokens after a valid production) MUST fail parsing and MUST NOT be auto-corrected.
  4. Error code stability: For the same malformed input class, implementations MUST return a stable error code category across runs to support deterministic compliance tests.

A conforming implementation MUST pass all Level 1 tests. Implementations claiming Level 2 or Level 3 conformance MUST pass all tests for their claimed level and all lower levels.

  • T-SYNTAX-001: Parse daily to FUZZY:DAILY * * *
  • T-SYNTAX-002: Parse weekly to FUZZY:WEEKLY * * *
  • T-SYNTAX-003: Parse weekly on monday to FUZZY:WEEKLY:1 * * 1
  • T-SYNTAX-004: Parse all weekday names correctly
  • T-SYNTAX-005: Reject invalid schedule types
  • T-SYNTAX-006: Reject invalid weekday names
  • T-SYNTAX-007: Parse case-insensitive tokens
  • T-TIME-001: Parse 24-hour format 14:00
  • T-TIME-002: Parse 12-hour format 3pm
  • T-TIME-003: Parse 12-hour format 11am
  • T-TIME-004: Parse keyword midnight as 00:00
  • T-TIME-005: Parse keyword noon as 12:00
  • T-TIME-006: Convert 12am to 00:00 (midnight)
  • T-TIME-007: Convert 12pm to 12:00 (noon)
  • T-TIME-008: Reject invalid hours (>23 or <0)
  • T-TIME-009: Reject invalid minutes (>59 or <0)
  • T-TIME-010: Handle missing leading zeros (e.g., 9:30)
  • T-CONSTRAINT-001: Parse daily around 14:00
  • T-CONSTRAINT-002: Parse daily between 9:00 and 17:00
  • T-CONSTRAINT-003: Parse weekly on friday around 17:00
  • T-CONSTRAINT-004: Handle midnight-crossing ranges (22:00 and 02:00)
  • T-CONSTRAINT-005: Reject around without time specification
  • T-CONSTRAINT-006: Reject between with only one time
  • T-CONSTRAINT-007: Reject daily at <time> syntax
  • T-TZ-001: Parse utc+9 offset
  • T-TZ-002: Parse utc-5 offset
  • T-TZ-003: Parse utc+05:30 offset format
  • T-TZ-004: Convert 14:00 utc+9 to 05:00 UTC
  • T-TZ-005: Convert 3pm utc-5 to 20:00 UTC
  • T-TZ-006: Handle negative UTC conversion (wrap to previous day)
  • T-TZ-007: Handle >24:00 UTC conversion (wrap to next day)
  • T-TZ-008: Reject invalid offsets (e.g., utc+25)

10.2.5 Hourly and Interval Tests (Level 2/3)

Section titled “10.2.5 Hourly and Interval Tests (Level 2/3)”
  • T-HOURLY-001: Parse hourly to FUZZY:HOURLY * * *
  • T-HOURLY-002: Parse every 2h to FUZZY:HOURLY:2 * * *
  • T-HOURLY-003: Parse every 6 hours to FUZZY:HOURLY:6 * * *
  • T-INTERVAL-001: Parse every 5 minutes to */5 * * * *
  • T-INTERVAL-002: Parse every 2 days to 0 0 */2 * *
  • T-INTERVAL-003: Reject every 3 minutes (below 5-minute minimum)
  • T-INTERVAL-004: Parse bi-weekly to FUZZY:BI-WEEKLY * * *
  • T-INTERVAL-005: Parse tri-weekly to FUZZY:TRI-WEEKLY * * *

10.2.6 Scattering Algorithm Tests (Level 1-3)

Section titled “10.2.6 Scattering Algorithm Tests (Level 1-3)”
  • T-SCATTER-001: Hash produces same output for same input
  • T-SCATTER-002: Different inputs produce different outputs
  • T-SCATTER-003: Hash value is within modulo range (0 to modulo-1)
  • T-SCATTER-004: Daily schedule selects time from weighted pool (BEST/GOOD/OK tiers only)
  • T-SCATTER-005: Around schedule stays within ±60 minute window
  • T-SCATTER-006: Between schedule stays within specified range
  • T-SCATTER-007: Midnight-crossing range handles day wrap correctly
  • T-SCATTER-008: Hourly schedule produces minute in [5, 54]
  • T-SCATTER-009: Weekly schedule selects valid day 0-6
  • T-SCATTER-010: Same workflow gets same time across compilations
  • T-SCATTER-011: Daily schedule lands in BEST (02–05), GOOD (10–12), or OK (19–23) window
  • T-SCATTER-012: Minute values in [5, 54] for all patterns (hour-boundary avoidance)
  • T-SCATTER-013: DAILY_AROUND scatter landing in EU peak hours (06–09) avoids minutes [27, 33]
  • T-SCATTER-014: DAILY_AROUND scatter landing in US business hours (14–18) avoids minutes [12, 18] and [42, 48]
  • T-SCATTER-015: Weekly schedule uses weighted daily time pool (preferred windows)
  • T-SCATTER-016: Bi-weekly and tri-weekly schedules use weighted daily time pool
  • T-CRON-001: Generated cron has exactly 5 fields
  • T-CRON-002: Minute field is in range 0-59
  • T-CRON-003: Hour field is in range 0-23
  • T-CRON-004: Day-of-week field is in range 0-6 or *
  • T-CRON-005: Month and day-of-month are valid
  • T-CRON-006: Interval expressions use valid */N syntax
RequirementTest IDLevelStatus
Parse basic dailyT-SYNTAX-0011Required
Parse basic weeklyT-SYNTAX-0021Required
Parse weekday specificationT-SYNTAX-0031Required
Parse all weekday namesT-SYNTAX-0041Required
Reject invalid typesT-SYNTAX-0051Required
Case-insensitive parsingT-SYNTAX-0071Required
Parse 24-hour formatT-TIME-0012Required
Parse 12-hour formatT-TIME-002, 0032Required
Parse time keywordsT-TIME-004, 0052Required
Handle 12am/12pm correctlyT-TIME-006, 0072Required
Validate time rangesT-TIME-008, 0092Required
Parse around constraintsT-CONSTRAINT-0012Required
Parse between constraintsT-CONSTRAINT-0022Required
Handle midnight crossingT-CONSTRAINT-0042Required
Parse UTC offsetsT-TZ-001, 002, 0033Required
Convert timezones correctlyT-TZ-004, 0053Required
Handle timezone day wrapT-TZ-006, 0073Required
Parse hourly schedulesT-HOURLY-001, 002, 0032Required
Parse interval schedulesT-INTERVAL-001, 0023Required
Hash determinismT-SCATTER-001, 0021Required
Scattering distributionT-SCATTER-004-0091-3Required
Weighted daily poolT-SCATTER-011, 015, 0161-3Required
Peak avoidance (hour boundary)T-SCATTER-0121-3Required
Peak avoidance (EU morning peak)T-SCATTER-0132-3Required
Peak avoidance (US business hours)T-SCATTER-0142-3Required
Generate valid cronT-CRON-001-0061-3Required

Implementations SHOULD provide:

  1. Automated test suite covering all compliance tests
  2. Test report indicating pass/fail status for each test
  3. Conformance level declaration (Level 1, 2, or 3)

# Basic daily (scattered across full day)
schedule: daily
# Fuzzy: FUZZY:DAILY * * *
# Might generate: 43 5 * * * (5:43 AM)
# Daily around specific time
schedule: daily around 14:00
# Fuzzy: FUZZY:DAILY_AROUND:14:0 * * *
# Might generate: 13 14 * * * (2:13 PM, within 1-3 PM window)
# Daily during business hours
schedule: daily between 9:00 and 17:00
# Fuzzy: FUZZY:DAILY_BETWEEN:9:0:17:0 * * *
# Might generate: 37 12 * * * (12:37 PM, within 9 AM-5 PM)
# Daily with timezone conversion (JST to UTC)
schedule: daily around 14:00 utc+9
# Fuzzy: FUZZY:DAILY_AROUND:5:0 * * *
# Converts to 5:00 AM UTC, scatters in window 4-6 AM UTC
# Basic weekly (any day, any time)
schedule: weekly
# Fuzzy: FUZZY:WEEKLY * * *
# Might generate: 43 5 * * 1 (Monday 5:43 AM)
# Weekly on specific day
schedule: weekly on monday
# Fuzzy: FUZZY:WEEKLY:1 * * 1
# Might generate: 18 14 * * 1 (Monday 2:18 PM)
# Weekly with time constraint
schedule: weekly on friday around 17:00
# Fuzzy: FUZZY:WEEKLY:5:AROUND:17:0 * * 5
# Might generate: 42 16 * * 5 (Friday 4:42 PM, within 4-6 PM)
# Every hour with scattered minute
schedule: hourly
# Fuzzy: FUZZY:HOURLY * * *
# Might generate: 43 * * * * (every hour at minute 43)
# Every 2 hours
schedule: every 2h
# Fuzzy: FUZZY:HOURLY:2 * * *
# Might generate: 53 */2 * * * (every 2 hours at minute 53)
# Every 5 minutes (fixed, not fuzzy)
schedule: every 5 minutes
# Generates: */5 * * * * (fixed interval)
# Bi-weekly
schedule: bi-weekly
# Fuzzy: FUZZY:BI-WEEKLY * * *
# Might generate: 43 5 */14 * * (every 14 days at 5:43 AM)
# JST (UTC+9) business hours to UTC
schedule: daily between 9am utc+9 and 5pm utc+9
# Converts to: daily between 0:00 and 8:00 (UTC)
# Fuzzy: FUZZY:DAILY_BETWEEN:0:0:8:0 * * *
# EST (UTC-5) afternoon meeting
schedule: weekly on monday around 3pm utc-5
# Converts to: weekly on monday around 20:00 (UTC)
# Fuzzy: FUZZY:WEEKLY:1:AROUND:20:0 * * 1
# IST (UTC+5:30) morning standup
schedule: daily around 9:30am utc+05:30
# Converts to: daily around 4:00 (UTC)
# Fuzzy: FUZZY:DAILY_AROUND:4:0 * * *
Error CodeDescriptionExample
ERR-SYNTAX-001Unknown schedule typemonthly (not supported)
ERR-SYNTAX-002Invalid time format25:00 (hour out of range)
ERR-SYNTAX-003Invalid weekdaymondey (typo)
ERR-SYNTAX-004Missing required componentdaily around (no time)
ERR-SYNTAX-005Unsupported syntax patterndaily at 14:00 (use around)
ERR-TIME-001Hour out of range25 (>23)
ERR-TIME-002Minute out of range60 (>59)
ERR-TIME-003Invalid 12-hour format13pm (hour >12)
ERR-TZ-001Invalid UTC offsetutc+25 (>14)
ERR-TZ-002Malformed offset syntaxutc9 (missing +/-)
ERR-INTERVAL-001Invalid interval valueevery 0h (must be >0)
ERR-INTERVAL-002Unsupported intervalevery 5h (not factor of 24)

The FNV-1a 32-bit hash provides adequate collision resistance for workflow scattering purposes. The birthday paradox suggests approximately 77,000 workflows are needed for a 50% collision probability. For organizations with fewer workflows, collisions are unlikely.

If collision occurs (two workflows receive identical execution times), this does not create a security vulnerability but reduces the effectiveness of load distribution.

The deterministic nature of the scattering algorithm means execution times are predictable given the workflow identifier. This is intentional for consistency but means:

  • Attackers cannot cause DOS by triggering simultaneous execution
  • Execution times cannot be used as secrets
  • Load distribution is transparent and auditable

Implementations MUST handle timezone offsets with integer arithmetic to prevent floating-point rounding errors that could cause inconsistent execution times.

Implementations SHOULD validate UTC offsets are within reasonable bounds (UTC-12 to UTC+14) to prevent overflow in time calculations.

Implementations MUST validate all user inputs before processing:

  • Schedule type MUST be from allowed set
  • Time values MUST be within valid ranges
  • Interval values MUST be positive integers
  • All string inputs MUST be sanitized to prevent injection attacks

This section maps the fuzzy schedule specification to implementation files.

Normative AreaImplementation File(s)
Frontmatter schedule parsing and grammar handlingpkg/parser/schedule_parser.go
Deterministic fuzzy scattering and peak-minute avoidancepkg/parser/schedule_fuzzy_scatter.go
Parser/scatter conformance testspkg/parser/schedule_parser_test.go, pkg/parser/schedule_fuzzy_scatter_test.go
Calendar/cron visualization support for compile tooling (see §12)pkg/cli/compile_schedule_calendar.go

After changing fuzzy schedule semantics:

  1. Update this specification section and any affected normative clauses.
  2. Update parser/scatter implementation in the mapped files.
  3. Re-run parser/scatter tests to verify behavior remains deterministic.

The compile-time schedule calendar emitted by pkg/cli/compile_schedule_calendar.go documents the aggregate UTC trigger density of scheduled workflows. A conforming implementation MUST treat the calendar as a human-readable console artifact rather than a machine-readable file format.

ElementRequirement
Output streamMUST be written to stderr only, and MUST NOT be emitted in JSON output mode.
Emission conditionMUST be omitted when no scheduled workflows are present.
Title lineMUST render the heading Schedule Heatmap (UTC).
Hour headerMUST contain 24 UTC hour labels from 00 through 23, in ascending order.
Day rowsMUST render exactly seven rows in Mon, Tue, Wed, Thu, Fri, Sat, Sun order.
CellsMUST render one glyph per hour slot using the implementation’s intensity mapping (·, , , , ).
LegendMUST explain the trigger-count buckets for each glyph after the grid.
File outputMUST NOT create a separate file; the calendar is an inline stderr rendering only.

Implementations SHOULD preserve a fixed-width grid so adjacent cells remain visually aligned in plain-text terminals. ANSI styling MAY be applied when stderr is a terminal, but the unstyled text content MUST preserve the same row/column structure.

  • Changed: Daily, weekly, bi-weekly, and tri-weekly scattering now share the weighted 622-slot pool introduced in Sections 6.3.1 and 6.3.5–6.3.6.
  • Added: Peak-minute avoidance rules in Section 6.4 to steer schedules away from :00, :15, :30, and :45 hotspot minutes during documented peak windows.
  • Added: Calendar output schema requirements (Section 12) for the compile-time heatmap rendered by compile_schedule_calendar.go.


  • Changed: Section 6.3.1 — Replaced flat hash-modulo-1440 daily scatter with a 622-entry weighted daily time slot pool (BEST 02–05 UTC ×3, GOOD 10–12 UTC ×2, OK 19–23 UTC ×1)
  • Changed: Sections 6.3.5–6.3.6 — Weekly, bi-weekly, and tri-weekly scatter now uses the same weighted pool as the daily schedule
  • Added: Section 6.4 — Peak Minutes Avoidance documenting:
    • avoidHourBoundary: shifts minutes [0,4]→[5,9] and [55,59]→[50,54]
    • avoidPeakMinutes: EU peak (hours 06–09) avoids ±3 min of :30 (shifts [27,33]→34); US business hours (14–18) avoids ±3 min of :15 (shifts [12,18]→19) and ±3 min of :45 (shifts [42,48]→49)
  • Renumbered: Section 6.4 (Algorithm Requirements) → Section 6.5
  • Added: Compliance tests T-SCATTER-011 through T-SCATTER-016 covering weighted pool behavior and peak avoidance
  • Updated: Compliance checklist (Section 9.3) with new required rows for weighted pool and peak avoidance
  • Changed: Hash function requirement relaxed from MUST to SHOULD for FNV-1a
  • Added: General hash function requirements (determinism, distribution, stability, integer output)
  • Added: Support for alternative hash functions (MurmurHash, xxHash, CityHash)
  • Changed: Moved FNV reference from normative to informative references
  • Initial specification release
  • Defined core fuzzy schedule syntax grammar
  • Specified scattering algorithm using FNV-1a hash
  • Added timezone conversion support
  • Defined three conformance levels (Basic, Standard, Complete)
  • Included comprehensive test suite with 50+ test cases
  • Added examples for all schedule types
  • Defined error codes and handling requirements

Copyright 2024 GitHub. All rights reserved.