Skip to main content

Building Expressions

There are four ways to construct a CronExpression: the constructor, direct property assignment, parsing an existing string, and the fluent helper methods. This page covers all of them.

Choosing an Approach

Each of the five fields can be set four different ways. Pick whichever expresses your intent most directly - they all write to the same underlying property, and they can be mixed freely on one expression.

FieldEvery NSpecific valuesRangeDirect
MinuteEveryMinute / EveryXMinutesOnMinutesRangeOfMinutes.Minute =
HourEveryHour / EveryXHoursOnHoursRangeOfHours.Hour =
DayEveryDay / EveryXDaysOnDaysRangeOfDays.Day =
MonthEveryMonth / EveryXMonthsOnMonthsRangeOfMonths.Month =
DayOfWeek(not supported)OnDaysOfWeekRangeOfWeek.DayOfWeek =
No day-of-week increment helpers

Step syntax is not valid in the DayOfWeek field, so assigning something like */2 to it throws NotSupportedException. Use OnDaysOfWeek for a list or RangeOfWeek for a range instead.

Note also that the day-of-week range method is named RangeOfWeek, not RangeOfDaysOfWeek, while its list counterpart is OnDaysOfWeek.

Every fluent method returns the same CronExpression instance, so calls can be chained, and each one touches only its own field - all others keep whatever value they already had.

Constructor and Properties

Every constructor parameter is optional and defaults to "*", so new CronExpression() is "* * * * *". Arguments are assigned through the properties, which means they are validated at construction.

// Incremental
var hourlyOnTheHalf = new CronExpression
{
Minute = "30",
Hour = "*",
};

// With the constructor
var weekdaysAt0630 = new CronExpression(minute: "30", hour: "6", dayOfWeek: "1-5");

// Render it
string text = weekdaysAt0630.ToCronExpression(); // "30 6 * * 1-5"

Direct assignment is the only way to express a field that the fluent helpers do not cover, such as a mixed list of ranges and steps:

var mixed = new CronExpression { Minute = "0,30,*/15" };

Month and DayOfWeek also accept three-letter names (JAN-DEC, SUN-SAT) anywhere a numeric value is accepted, through the constructor, Parse, or direct assignment alike:

var weekdaysFirstHalfOfYear = new CronExpression(month: "JAN-JUN", dayOfWeek: "MON-FRI");
weekdaysFirstHalfOfYear.Month; // "1-6" - names are translated to numeric immediately

See Month and Day-of-Week Names for the full rules.

Parsing

Parse also accepts the standard crontab macros (@daily, @hourly, and the like) case-insensitively, as the entire trimmed value in place of the five fields - a macro is expanded to its five-field equivalent before the rest of parsing runs. Otherwise, Parse throws a FormatException if the string does not contain exactly five parts. Runs of consecutive spaces are tolerated, since empty entries are discarded before the parts are counted.

Each part is then assigned through the corresponding property, so Parse also propagates any validation error those setters raise. A string with the right number of parts but a bad value fails with ArgumentOutOfRangeException or NotSupportedException rather than FormatException.

// Macro - expanded before parsing continues
CronExpression.Parse("@daily").ToCronExpression(); // "0 0 * * *"

// Strict parse - throws FormatException if not exactly 5 parts
var expr = CronExpression.Parse("0 0 * 1 *");

// Right shape, invalid value - throws ArgumentOutOfRangeException
CronExpression.Parse("0 0 * 13 *");

// Non-throwing parse - returns false for every failure above
if (!CronExpression.TryParse("invalid value", out var result))
{
// handle error path
}

TryParse returns false for all of these cases and sets expression to null; it never throws. @reboot is a recognized macro with no five-field schedule to expand into, so it throws NotSupportedException explaining why - see Macros.

Increment Helpers

Every* sets a field to *. EveryX* sets it to a step, either */increment or start/increment.

MethodEffect on CronExpressionValid values
EveryMinute()Minute = "*"
EveryXMinutes(int increment)Minute = "*/{increment}" (uses EveryMinute() when increment == 1)increment: 1-59
EveryXMinutes(int start, int increment)Minute = "{start}/{increment}" (uses EveryMinute() when start == 1 && increment == 1)start: 0-59, increment: 1-59
EveryHour()Hour = "*"
EveryXHours(int increment)Hour = "*/{increment}" (uses EveryHour() when increment == 1)increment: 1-23
EveryXHours(int start, int increment)Hour = "{start}/{increment}" (uses EveryHour() when start == 1 && increment == 1)start: 0-23, increment: 1-23
EveryDay()Day = "*"
EveryXDays(int increment)Day = "*/{increment}" (uses EveryDay() when increment == 1)increment: 1-31
EveryXDays(int start, int increment)Day = "{start}/{increment}" (uses EveryDay() when start == 1 && increment == 1)start: 1-31, increment: 1-31
EveryMonth()Month = "*"
EveryXMonths(int increment)Month = "*/{increment}" (uses EveryMonth() when increment == 1)increment: 1-12
EveryXMonths(int start, int increment)Month = "{start}/{increment}" (uses EveryMonth() when start == 1 && increment == 1)start: 1-12, increment: 1-12
// Every 5 minutes
new CronExpression().EveryXMinutes(5).ToCronExpression(); // "*/5 * * * *"

// Every 15 minutes starting at :00
new CronExpression().EveryXMinutes(start: 0, increment: 15).ToCronExpression(); // "0/15 * * * *"

// Every 6 hours
new CronExpression().EveryXHours(6).ToCronExpression(); // "* */6 * * *"

// Quarterly
new CronExpression().EveryXMonths(3).ToCronExpression(); // "* * * */3 *"

Remember that a */n step counts from the field's own minimum, so EveryXDays(2) selects odd-numbered days rather than even ones. See Cron Expression Format for the full rule.

List Helpers

Each On* method takes any number of int values, sorts them ascending, removes duplicates, and writes them as a comma-separated list. OnMonths and OnDaysOfWeek also have a string overload accepting names, sorted and deduplicated the same way once translated to numbers.

MethodField modifiedExample inputResulting cron field
OnMinutes(params int[] minutes)Minute0, 15, 30, 450,15,30,45
OnHours(params int[] hours)Hour6, 12, 186,12,18
OnDays(params int[] days)Day of month1, 15, 311,15,31
OnMonths(params int[] months)Month1, 6, 121,6,12
OnMonths(params string[] months)Month"JAN", "JUN"1,6
OnDaysOfWeek(params int[] daysOfWeek)Day of week1, 3, 51,3,5
OnDaysOfWeek(params string[] daysOfWeek)Day of week"MON", "FRI"1,5
// Top and bottom of every hour
new CronExpression().OnMinutes(0, 30).ToCronExpression(); // "0,30 * * * *"

// Quarterly, by naming the months
new CronExpression().OnMonths(1, 4, 7, 10).ToCronExpression(); // "* * * 1,4,7,10 *"

// The same, using names instead - input order doesn't matter, the result is always sorted numerically
new CronExpression().OnMonths("JAN", "APR", "JUL", "OCT").ToCronExpression(); // "* * * 1,4,7,10 *"

// Noon on Monday, Wednesday, and Friday
new CronExpression().OnHours(12).OnDaysOfWeek(1, 3, 5).ToCronExpression(); // "* 12 * * 1,3,5"

// Sorting and de-duplication are automatic
new CronExpression().OnHours(12, 6, 6).Hour; // "6,12"

OnDaysOfWeek treats 0 and 7 as distinct values, so OnDaysOfWeek(0, 7) keeps both and sorts to "0,7" rather than collapsing to a single 0. Both still match Sunday.

The string overloads of OnMonths and OnDaysOfWeek don't take a required leading argument the way you might expect for disambiguation, they're a second params overload alongside the int one. That's deliberate: a required first argument would break the common OnMonths([.. someArray]) call style used above, since the compiler can no longer tell whether the spread should fill the leading parameter or the trailing array. See List Helpers in the API reference for the full reasoning. Every name here is a standalone list value, never the end of a range, so SUN always means 0 in OnDaysOfWeek, unlike in RangeOfWeek below.

Range Helpers

Each RangeOf* method writes an inclusive start-end range. The start value must be strictly less than the end value. RangeOfMonths and RangeOfWeek also accept names, on either side, in any combination with a number.

MethodField modifiedExample inputResulting cron field
RangeOfMinutes(int start, int end)Minute0, 300-30
RangeOfHours(int start, int end)Hour8, 178-17
RangeOfDays(int start, int end)Day of month1, 151-15
RangeOfMonths(int start, int end)Month1, 61-6
RangeOfMonths(string start, string end)Month"JAN", "JUN"1-6
RangeOfWeek(int start, int end)Day of week1, 51-5
RangeOfWeek(string start, string end)Day of week"MON", "FRI"1-5
// Business hours
new CronExpression().RangeOfHours(8, 17).ToCronExpression(); // "* 8-17 * * *"

// Weekdays only
new CronExpression().RangeOfWeek(1, 5).ToCronExpression(); // "* * * * 1-5"

// The same, using names instead
new CronExpression().RangeOfWeek("MON", "FRI").ToCronExpression(); // "* * * * 1-5"

// First half of the year
new CronExpression().RangeOfMonths(1, 6).ToCronExpression(); // "* * * 1-6 *"

// A number on one side and a name on the other both work, in either position
new CronExpression().RangeOfMonths(1, "JUN").ToCronExpression(); // "* * * 1-6 *"
new CronExpression().RangeOfMonths("JAN", 6).ToCronExpression(); // "* * * 1-6 *"

RangeOfWeek accepts 7 as the end of a range to reach Sunday, so RangeOfWeek(5, 7) produces "5-7" - Friday through Sunday. The name SUN behaves the same way as the end of a range: RangeOfWeek("MON", "SUN") produces "1-7", not "1-0", since SUN becomes 7 rather than 0 specifically at the end of a range - see Month and Day-of-Week Names for the full rule.

Chaining

Because every fluent method returns the same instance, the three families combine freely, and direct property assignment can fill in anything they do not cover.

// Every 15 minutes during business hours, weekdays only
new CronExpression()
.EveryXMinutes(15)
.RangeOfHours(8, 17)
.RangeOfWeek(1, 5)
.ToCronExpression(); // "*/15 8-17 * * 1-5"
// 08:00 every 3 days, setting the time directly
var every3DaysAt8 = new CronExpression().EveryXDays(3);
every3DaysAt8.Minute = "0";
every3DaysAt8.Hour = "8";

every3DaysAt8.ToCronExpression(); // "0 8 */3 * *"

Rendering

ToCronExpression() returns the five-field string and is the only method that performs the day/month check described in Cron Expression Format. That check runs only when Day is a single numeric value, so an expression whose day field is a list, range, or step is rendered without it.

See Validation and Errors for everything that can throw while building an expression.