CLI tool
bradymholt/cron-expression-descriptor avatar
bradymholt/cron-expression-descriptor

CronExpressionDescriptor: turning a cron string into a sentence a user can read

A .NET library that converts cron expressions into human readable descriptions.

1,116 stars195 forksC#MIT

At a glance

What is it?
This .NET library converts cron expressions into human readable descriptions in 29 languages, with options for casing, 24-hour time, locale and parse error handling. It describes schedules, it does not run them.
Who is it for?
CronExpressionDescriptor is the right choice for a .NET product that has to show a schedule to a person rather than to a parser: a booking screen, a report builder, a job editor, anything where a bare `0-10 11 * * *` would otherwise be the only explanation a user gets.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 44 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One static call turns a cron string into a sentence

The whole API surface can be shown in two lines. You call a static method on `ExpressionDescriptor` with a cron expression, and you get back a sentence:

csharp
CronExpressionDescriptor.ExpressionDescriptor.GetDescription("* * * * *");
> "Every minute"

There is no instance to create, no parser to configure and no result object to inspect. The library is a formatter for cron strings, and its output is aimed at a person reading a settings page.

The scope of the parser is wider than the five field Unix cron, and this is where the design shows. It supports all the cron expression special characters, which are `*`, `/`, `,`, `-`, `?`, `L`, `W` and `#`, and it accepts expressions of five parts, six parts where the extra part is seconds or a year, and seven parts where both are present. It also supports the cron expressions of the Quartz Enterprise Scheduler for .NET, which is a dialect with its own extensions.

Formatting is a separate concern from parsing, and the library treats it that way. It offers casing options, so a description can come back in sentence case, title case, lowercase and other styles, and the description text itself is localized into 29 languages.

The visual demo at `bradymholt.github.io/cron-expression-descriptor` exists for exactly this kind of library: paste an expression, see the sentence. It is a Blazor application, with `demo/App.razor`, `demo/Program.cs` and a `demo/wwwroot/` folder for static assets, sitting in the same repository as the library itself.

Install from NuGet, then call it once

The package is on NuGet as `CronExpressionDescriptor`, and there are two documented ways to bring it into a project.

In the Visual Studio package manager console the command is:

powershell
Install-Package CronExpressionDescriptor

From the .NET CLI it is:

bash
dotnet add package CronExpressionDescriptor

Both resolve to the same package, and the NuGet page is the place to look for the current version and for more information.

The first call in your code does not need any setup beyond the using directive for the `CronExpressionDescriptor` namespace, and the quick start is identical to the example at the top of the documentation:

csharp
CronExpressionDescriptor.ExpressionDescriptor.GetDescription("* * * * *");
> "Every minute"

From there the only question is what the expression means to you, and that is what the options are for. The signature that matters takes an optional `Options` object, and every behaviour that is not fixed by the expression itself lives there: how a day of week is numbered, whether the clock is 12 or 24 hour, which language the sentence is in, whether the sentence is verbose, and what happens when the expression cannot be parsed.

For a library this small, that is the correct design. The parsing rules are fixed, the presentation rules are configurable, and the two are not tangled together.

The Options object is where every cron ambiguity ends up

Five options carry all of the configuration, and each one resolves a genuine ambiguity in cron syntax.

`ThrowExceptionOnParseError` decides what a malformed expression does. If it is true, which is the default, an exception is thrown when the expression cannot be parsed or described. If it is false, the exception is caught and its message is returned as the description, which is the behaviour you want when the expression came from a user typing into a form.

`Verbose` switches between the short and the long description, and defaults to false.

`DayOfWeekStartIndexZero` decides how the day-of-week field is read, and it is the option most likely to change a result without anyone noticing. It controls whether cron expression DOW `1` means Sunday or Monday, and it defaults to true, so the default reading is the zero-indexed one. The library leaves the choice open precisely because the two conventions coexist in the wild, and a scheduler with a different default than the UI would otherwise produce a description that contradicts the behaviour of the job.

`Use24HourTimeFormat` is a nullable boolean, which is a small design detail with a visible effect. Its default is false, but some translations default to true, because a locale whose convention is a 24-hour clock should not have to say so. A nullable flag lets the library follow the locale when the application has not expressed a preference.

`Locale` selects the language and defaults to `en`.

They combine in a single call, which is how the documentation shows them used:

csharp
ExpressionDescriptor.GetDescription("0-10 11 * * *", new Options(){
    DayOfWeekStartIndexZero = false,
    Use24HourTimeFormat = true,
    Locale = "fr"
});

The point of the example is that these are not cosmetic. `DayOfWeekStartIndexZero = false` changes what the expression means, while `Use24HourTimeFormat` and `Locale` change how the result reads.

29 locales, three ways to choose one

Localization is the largest feature of the library, and there are three mechanisms for choosing a language.

The explicit way is the `Locale` option, which takes a language code. The documented call returns a German sentence for an English expression:

csharp
ExpressionDescriptor.GetDescription("0-10 11 * * *", new Options(){ Locale = "de" });
> "Jede Minute zwischen 11:00 und 11:10"

The global way is `ExpressionDescriptor.SetDefaultLocale("es")`, which sets the default for every later call in the process:

csharp
ExpressionDescriptor.SetDefaultLocale("es");

ExpressionDescriptor.GetDescription("*/45 * * * * *");
> "Cada 45 segundos"

ExpressionDescriptor.GetDescription("0-10 11 * * *");
> "Cada minuto entre las 11:00 AM y las 11:10 AM"

The implicit way is the thread culture. On a platform that supports .NET Standard 2.0 or later, `Thread.CurrentUICulture` is used to determine the default locale without passing the `Locale` option at all:

csharp
// .NET Standard >= 2.0 only
CultureInfo myCultureInfo = new CultureInfo("it-IT");
Thread.CurrentThread.CurrentUICulture = myCultureInfo;
ExpressionDescriptor.GetDescription("* * * * *");
> "Ogni minuto"

That last path is the one to know about, because it makes the library behave like the rest of a localized application instead of requiring a special call at every site. It also explains the nullable `Use24HourTimeFormat` flag: the library is willing to follow the ambient culture.

The translations themselves are community contributed and credited in the documentation, from English and the two Chinese variants through Czech, Danish, Dutch, Finnish, French, German, Hebrew, Hungarian, Italian, Japanese, Korean, Norwegian, Persian, Polish, Brazilian Portuguese, Romanian, Russian, Slovenian, two Spanish variants, Swedish, Vietnamese, Turkish, Ukrainian, Greek, Kazakh and Kyrgyz.

There is a cost to that breadth, and the documentation addresses it: if you do not want every supported language in your build output, set the `SatelliteResourceLanguages` property in your project. Each translation is a satellite assembly, so an application that ships all 29 pays for all 29.

Quartz syntax and the special characters the parser accepts

Two different cron dialects meet in this library, and a user of it needs to know which one they are on.

The classic Unix form is five fields, and that is what `* * * * *` means. Beyond that, the parser handles the special characters `*`, `/`, `,`, `-`, `?`, `L`, `W` and `#`, which together cover ranges, steps, lists, wildcards, the last day of a month, the nearest weekday and the nth weekday of a month. Field count is the second axis: five parts, six parts where the extra one carries seconds or a year, and seven parts where both are present.

On top of that, the library supports the cron expressions of the Quartz Enterprise Scheduler for .NET. Quartz expressions are not the same as Unix ones, and a string that is valid in one dialect can be rejected in the other. The library accepts the Quartz form, which is what makes it usable in a .NET application that schedules work through Quartz and wants to show the user the same schedule in words.

The `Options` do not include a dialect switch, and that is worth noticing. `DayOfWeekStartIndexZero` is the only knob that touches interpretation, and it applies to the day-of-week field alone. A deployment that must describe both Unix and Quartz schedules has to be confident about which parser behaviour it wants, because the library does not offer a per-call dialect selection.

What the library does not do is schedule. It parses an expression and returns a description of it. It does not compute the next occurrence, it does not validate an expression against the semantics of the scheduler that will run it, and it has no notion of a job. That is a deliberate limit of the scope, and it is what keeps the library small enough to be a dependency rather than a framework.

MIT licence, two .NET Standard targets, and a cut-off below .NET Framework 4.5

The licence is MIT, with the text in the `LICENSE` file at the root of the repository, and the author is Brady Holt.

Platform support is narrow in a specific and dated way. Beginning with version 2.0.0 the NuGet package contains a library targeting.NET Standard 1.1 and 2.0, which allows the library to be consumed by applications running on.NET Core 1.0 or later,.NET Framework 4.5 or later, and Mono 4.6 or later. The documentation then notes that an application targeting an earlier version of.NET Framework, with 4.0 given as the example, is out of range.

For most modern work that is a non-issue, but it is the kind of boundary that matters in a long-lived enterprise application still on an old framework, and it is the first thing to check.

The build is a Visual Studio solution, `CronExpressionDescriptor.sln`, with `lib/`, `test/`, `demo/`, `docs/` and `scripts/` directories, a `nuget.config`, an `.editorconfig` and a `RELEASING.md` that describes the release process. The release history is unusual in a small way: versions 2.49.0, 2.50.0 and 2.51.0 were all published on 2026-09-09 within a few seconds of each other, which is the signature of an automated publish script rather than three hand-made releases, and the last push to the repository was on the same day, 2026-09-09. The project is not archived.

The version numbers are the other half of the upgrade story. A package that has been published this many times carries behaviour changes in the description text, and the `Options` defaults are the part of that behaviour most likely to shift what a user sees. The release notes are where a text change of that kind should be checked before an upgrade is rolled out.

Editorial conclusion

CronExpressionDescriptor is the right choice for a .NET product that has to show a schedule to a person rather than to a parser: a booking screen, a report builder, a job editor, anything where a bare `0-10 11 * * *` would otherwise be the only explanation a user gets. It is the wrong choice for code that needs to compute the next fire time or to schedule anything, because it describes expressions and does not run them, and it is a poor fit for a project still targeting .NET Framework 4.0, which the library no longer supports. Before adopting it, read the `Options` defaults against your own cron dialect, in particular `DayOfWeekStartIndexZero`, which decides whether day 1 is Sunday or Monday, and check the `ThrowExceptionOnParseError` setting, since the default of true turns a malformed user-entered expression into an exception rather than a message.

Frequently asked questions

How do I install CronExpressionDescriptor in a .NET project?

Use `Install-Package CronExpressionDescriptor` in the Visual Studio package manager console, or `dotnet add package CronExpressionDescriptor` from the .NET CLI. The package is published on NuGet as CronExpressionDescriptor.

What happens when CronExpressionDescriptor cannot parse an expression?

By default the `ThrowExceptionOnParseError` option is true, so an exception is thrown when the expression cannot be parsed or described. Set it to false and the exception message is returned as the description instead.

Does CronExpressionDescriptor support Quartz cron expressions?

Yes, it supports the cron expressions of the Quartz Enterprise Scheduler for .NET, along with the special characters `*`, `/`, `,`, `-`, `?`, `L`, `W` and `#`, and expressions of five, six or seven parts where the extra parts carry seconds and year.

How do I keep all 29 languages out of my build output?

Each translation is a satellite assembly, so you can set the `SatelliteResourceLanguages` property in your project to exclude the languages you do not need from the build output.

How does CronExpressionDescriptor decide whether day 1 is Sunday or Monday?

The `DayOfWeekStartIndexZero` option decides whether the day-of-week field `1` is read as Sunday or Monday, and it defaults to true. Set it to false to use the other convention, as the documentation example does together with the 24-hour clock and the French locale.

Official sources

  1. bradymholt/cron-expression-descriptor on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/bradymholt-cron-expression-descriptor.svg)](https://hysenlabs.com/projects/bradymholt-cron-expression-descriptor)

Was this useful?