# Cron API

A [Recurring Schedule](/docs/core-concepts/scheduling#recurring-schedules) fires on a cron expression. BackWave models that expression as a `CronExpression` value rather than a bare string, and gives you two ways to make one: the fluent `Cron` builder for common shapes, and `CronExpression.Parse` for a raw cron string. Either way you end up with the same type, and the schedule stores a single canonical six-field form. This page is the reference for both entry points, the grammar `Parse` accepts, how the canonical form is derived, and how a schedule evaluates its cron in UTC or an IANA time zone.

Both `Cron` and `CronExpression` live in the `BackWave.Core` namespace.

## The Cron builder

`Cron` is a static factory whose methods each return a `CronExpression`. Every builder compiles down to a cron expression, so a builder can only express what cron itself can, and the canonical cron form is what gets stored either way. Reach for a builder when your schedule fits one of the common shapes below; drop to `CronExpression.Parse` for anything they do not cover.

| Method | Signature | Fires |
|---|---|---|
| Every minute | `Cron.EveryMinute()` | At the start of every minute. |
| Every N minutes | `Cron.EveryMinutes(int minutes)` | Every `minutes` minutes. `minutes` must be at least 1. |
| Hourly | `Cron.Hourly(int atMinute = 0)` | Once an hour, at `atMinute` (0–59, default 0). |
| Daily | `Cron.Daily(int hour, int minute = 0)` | Once a day, at `hour` (0–23) and `minute` (0–59, default 0). |
| Weekly | `Cron.Weekly(DayOfWeek day, int hour, int minute = 0)` | Once a week, on `day`, at `hour` (0–23) and `minute` (0–59, default 0). |
| Monthly | `Cron.Monthly(int dayOfMonth, int hour, int minute = 0)` | Once a month, on `dayOfMonth` (1–31), at `hour` (0–23) and `minute` (0–59, default 0). |

```csharp title="BuilderExamples.cs"
CronExpression everyFive = Cron.EveryMinutes(5);        // "*/5 * * * *"
CronExpression nightly   = Cron.Daily(hour: 2);         // "0 2 * * *"
CronExpression topOfHour = Cron.Hourly(atMinute: 15);   // "15 * * * *"
CronExpression mondays   = Cron.Weekly(DayOfWeek.Monday, hour: 9); // "0 9 * * 1"
CronExpression firstOfMonth = Cron.Monthly(dayOfMonth: 1, hour: 14, minute: 30); // "30 14 1 * *"
```

A few things to know about the builders:

- Each builder validates its arguments and throws `FormatException` when a value falls outside the range shown above. `Cron.EveryMinutes(0)`, for example, throws, which is why the argument must be at least 1.
- `Weekly` takes a `System.DayOfWeek`, where `DayOfWeek.Sunday` is 0 and `DayOfWeek.Saturday` is 6.
- `Monthly` does not clamp a day past the end of a short month. A schedule pinned to day 31 simply does not fire in a month that has no 31st; it is skipped that month, not moved to the last day.
- The builders cover minute-and-coarser schedules only. There is no builder for a seconds field, arbitrary ranges, or lists. Those require a raw string through `CronExpression.Parse`.

## Parsing a raw expression

`CronExpression.Parse` turns a cron string into a `CronExpression`. It is the only public constructor for the type; instances come from `Parse` or from a `Cron` builder, never from `new`.

```csharp title="ParseExample.cs"
CronExpression cron = CronExpression.Parse("0 30 9 * * *"); // 09:30 every day
```

`Parse` accepts standard cron in two field counts and rejects everything else:

| Fields | Interpretation |
|---|---|
| 5 | `minute hour day-of-month month day-of-week`. A `0` seconds field is prepended for you. |
| 6 | `second minute hour day-of-month month day-of-week`. |

Any other field count throws `FormatException`. Runs of extra spaces between fields are tolerated. Passing a null, empty, or whitespace string throws `ArgumentException`.

The six canonical fields, in order, and the value each accepts:

| Field | Position | Range |
|---|---|---|
| seconds | 1st (6-field form only) | 0–59 |
| minutes | 2nd | 0–59 |
| hours | 3rd | 0–23 |
| day-of-month | 4th | 1–31 |
| month | 5th | 1–12 |
| day-of-week | 6th | 0–7 |

### Field syntax

Within a field, comma-separated parts are combined, so a value matches if it matches any part. Each part supports:

| Form | Meaning |
|---|---|
| `*` | The whole range for that field. |
| `a` | A single value. |
| `a-b` | An inclusive range from `a` to `b`. |
| `*/s` | The whole range, stepped by `s`. |
| `a-b/s` | The range `a` to `b`, stepped by `s`. |
| `a/s` | From `a` to the field's maximum, stepped by `s`. |
| `a,b,c` | A list; each element may itself be any of the forms above, for example `1-5,10` or `0,15,30,45`. |

```csharp title="SyntaxExamples.cs"
CronExpression quarterHour = CronExpression.Parse("0,15,30,45 * * * *");
CronExpression businessHours = CronExpression.Parse("0 9-17 * * *");
CronExpression everyTenSeconds = CronExpression.Parse("*/10 * * * * *"); // 6-field
```

A part is out of range, and throws `FormatException`, when its low bound is below the field minimum, its high bound is above the field maximum, its low bound exceeds its high bound, or its step is less than 1. A reversed range such as `5-2` throws, and a step of 0 throws.

### What Parse does not accept

BackWave parses standard numeric cron only, with no Quartz extensions. The following all throw `FormatException`:

- **Names.** Month names (`JAN`, `FEB`) and day names (`SUN`, `MON`) are not recognized. Fields are numbers; a non-numeric token is rejected.
- **Quartz tokens.** `?`, `L`, `W`, `#`, and `LW` are not supported.
- **Named macros.** `@daily`, `@hourly`, `@reboot`, and similar shorthands are not supported.

Use the numeric equivalents: a weekday is `1` through `5`, and Sunday is `0` (or `7`).

## The canonical form

`CronExpression.Canonical` is the six-field string a schedule stores. It is the single stored representation regardless of which idiom defined the schedule, so a builder and an equivalent raw string persist identically.

```csharp title="Canonical.cs"
CronExpression.Parse("0 2 * * *").Canonical;   // "0 0 2 * * *"
CronExpression.Parse("30 0 2 * * *").Canonical; // "30 0 2 * * *"
Cron.Daily(2).Canonical;                        // "0 0 2 * * *"
```

Canonicalization is deliberately minimal. It guarantees two things and no more:

- The result always has six fields. A five-field input gets a `0` seconds field prepended; the other five fields are copied verbatim.
- Fields are separated by single spaces, so extra whitespace in the input does not survive.

It does not rewrite field text. Steps and ranges are not expanded (`*/15` stays `*/15`, `1-5` stays `1-5`), and a `7` for Sunday is not rewritten to `0`. Two expressions that mean the same thing but are written differently, such as `0 * * * *` and `0-0 * * * *`, produce different canonical strings. Because this string is both the value the schedule stores and the key BackWave parses and caches the expression under, prefer writing an expression one consistent way.

## Day-of-month and day-of-week

The day-of-month and day-of-week fields interact by the standard cron rule:

- When both fields are restricted, meaning neither is `*`, a day matches if it matches **either** field. `0 0 13 * 5` fires on the 13th of the month or on any Friday.
- When only one of the two is restricted, that field alone decides the day.

The restriction test is a literal check for `*`. A day-of-month field written as `1-31` counts as restricted even though it spans every day, so pairing it with a restricted day-of-week field switches on the "either" rule. To keep the plain "and" behavior, leave the field you do not want to constrain as `*`.

Sunday is both `0` and `7` in the day-of-week field, so `... * * 0` and `... * * 7` are equivalent.

## Next occurrence

`NextAfter` computes the first firing strictly after a given instant. It is the same "next due tick" calculation a schedule uses to decide what to mint.

```csharp title="NextAfter.cs"
CronExpression cron = CronExpression.Parse("*/15 * * * *");
DateTimeOffset? next = cron.NextAfter(DateTimeOffset.UtcNow);
```

| Property | Behavior |
|---|---|
| Signature | `DateTimeOffset? NextAfter(DateTimeOffset after)` |
| Evaluation | In UTC. The `after` argument is converted with `ToUniversalTime` before the search. |
| Result | The first occurrence strictly later than `after`; never equal to it. |
| Granularity | One second when the expression has a seconds field, otherwise one minute. |
| No occurrence | Returns `null` when nothing fires within four years of `after`. |

A `null` result is not an error. It means the expression is effectively unsatisfiable, such as one pinned to an impossible date like February 30 (`0 0 30 2 *`). Handle `null` where you call `NextAfter` directly.

## Time zones and daylight saving

A cron evaluates in UTC by default. Pass an IANA time-zone id to the schedule, such as `"America/New_York"`, and its occurrences are computed against that zone's local wall-clock time. A null time zone means UTC.

```csharp title="ZonedSchedule.cs"
await client.UpsertRecurringAsync(
    "nightly-report",
    Cron.Daily(hour: 2),
    new GenerateReport(),
    timeZone: "America/New_York");
```

Across daylight-saving boundaries the schedule follows fixed rules, and there is no knob to change them:

| Transition | Rule |
|---|---|
| Spring forward (a local time that is skipped) | The tick fires once, at the first valid instant after the gap. A `02:30` daily job in a zone that jumps `02:00` to `03:00` fires at `03:00` that day. |
| Fall back (a local time that occurs twice) | The tick fires on its first occurrence only, not the repeat. A nightly job in the ambiguous hour runs once, not twice. |

The effect is that a daily schedule fires exactly once per day through both transitions.

The time-zone id must resolve on the host running the schedule. An id that is present in one environment but missing in another, or a typo, is a common cause of a schedule that works in development and fails in production. `UpsertRecurringAsync` checks the zone when you define the schedule and throws `ArgumentException` at that call if it cannot be resolved, so the failure surfaces loudly at the door rather than silently later.

## Passing a cron to a schedule

`UpsertRecurringAsync` takes the `cron` argument as a `CronExpression`, not a string. Pass a `Cron` builder result directly, or wrap a raw string in `CronExpression.Parse`. There is no string-cron overload.

```csharp title="Upsert.cs"
// Builder.
await client.UpsertRecurringAsync("hourly-sync", Cron.Hourly(), new SyncCatalog());

// Raw expression.
await client.UpsertRecurringAsync(
    "seconds-heartbeat",
    CronExpression.Parse("*/10 * * * * *"),
    new Heartbeat());
```

When the schedule is stored, it keeps the cron's `Canonical` string and the time-zone id. That canonical six-field string is the single persisted representation of when the schedule fires. Because `cron` is already a parsed `CronExpression` by the time it reaches the client, the only thing that can fail validation at the call is an unresolvable time-zone id.

The [Scheduling](/docs/core-concepts/scheduling) page covers the rest of `UpsertRecurringAsync`, including the `scheduleId`, the `now` starting instant, and the Catch-Up and No-Overlap policies that govern missed and overlapping runs.

## Where to go next

- [Scheduling](/docs/core-concepts/scheduling): Due Time, `UpsertRecurringAsync`, and how minted jobs become runnable.
- [Schedule Recurring Jobs](/docs/guides/recurring-schedules): a worked walkthrough of defining a schedule, Catch-Up, and No-Overlap.
- [Client API](/docs/reference/client-api): the full signature of `UpsertRecurringAsync` and the rest of the client surface.
- [Limits & Defaults](/docs/reference/limits-and-defaults): the maintenance interval that bounds how quickly a due tick is minted, and other defaults in one place.
