Library / SDK
bradymholt/cRonstrue avatar
bradymholt/cRonstrue

cRonstrue: turning a cron expression into a sentence your users can read

JavaScript library that translates Cron expressions into human readable descriptions

1,641 stars197 forksTypeScriptMIT

At a glance

What is it?
A zero dependency TypeScript library that parses cron strings, including Quartz syntax and nicknames, and returns a sentence, with locale bundles and a CLI. Useful anywhere a scheduler needs to be explained rather than executed.
Who is it for?
Adopt cRonstrue when the only job is rendering a schedule as text, especially if users outside your team type the expressions or the schedule is Quartz rather than plain five field cron. Do not adopt it as a scheduler: it describes, it does not run anything, and it will not tell you that an expression your scheduler accepts is semantically odd.
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 2 days ago.
What is it written in?
Mainly TypeScript, 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

The parsing problem is smaller than it looks, and the wording is not

A cron expression is a compressed string, and reading one requires holding field semantics in your head. Five fields, or six with seconds, or seven with a year field, plus special characters like * / , - ? L W and #, plus Quartz extensions, plus nicknames like @monthly. Any engineer who writes crontabs can read them. Most people who look at a scheduler UI cannot.

cRonstrue does exactly one thing: given an expression, return a sentence. The README's first example is the whole pitch. An expression of "*/5 * * * *" comes back as "Every 5 minutes".

The package is published as cronstrue, MIT licensed, with an empty dependencies object in package.json, which is the literal statement of the zero dependency claim. The main entry point is dist/cronstrue.js and typings ship as dist/cronstrue.d.ts, so it is a typed library rather than an untyped script.

It is a port of a C# project, cron-expression-descriptor, which has ports in a few other languages. That lineage explains the field handling options later, since a C# origin means Quartz's dialect came first. The homepage hosts a demo page with a live input box at the #cronstrue-demo anchor.

Install and first call, in whichever module system you already have

The package name on npm is cronstrue:

bash
npm install cronstrue

It is built as a UMD module, so it works in AMD, CommonJS and as a browser global. In Node with CommonJS:

js
const cronstrue = require('cronstrue');

With ESM, webpack or TypeScript:

js
import cronstrue from 'cronstrue';

In a browser, serve cronstrue.min.js from the dist folder and read it off the window:

html
<script src="cronstrue.min.js" type="text/javascript"></script>
<script>
  var cronstrue = window.cronstrue;
</script>

Because there are no dependencies, that script tag is the entire install for a static page.

There is also a command line entry, declared in package.json as bin/cli.js, so you can check an expression without writing a file:

sh
npx cronstrue "*/5 * * * *"

That prints Every 5 minutes. The CLI is the fastest way to answer a support question about a schedule.

For browsers served from a CDN, the README points at unpkg and recommends pinning a version rather than using @latest, because the latest tag triggers a 302 redirect to the current version tag. The example given is a pinned path under unpkg.com/[email protected]/dist/cronstrue.min.js.

Field conventions differ, so four options decide what a number means

The interesting failures in this library are not crashes. They are sentences that are confidently wrong, because the same digit means different things in different cron dialects. The options object, passed as the second argument to toString, is where you settle those questions.

dayOfWeekStartIndexZero decides whether a day-of-week 1 is Sunday or Monday, defaulting to true, so Sunday is 1. monthStartIndexZero decides whether January is 0 or 1, defaulting to false, so January is 1. Those two defaults determine how cRonstrue interprets Sunday and January. If your scheduler is Quartz, or if you are rendering expressions typed by users in a locale where the week starts on Monday, the defaults will describe the wrong day, and no error will be raised.

use24HourTimeFormat switches the output clock, defaulting to false, with the note that some translations default to true on their own. trimHoursLeadingZero removes the padding, turning "02:00 AM" into "2:00 AM". verbose changes phrasing rather than meaning, so "0 23 * * *" reads "At 11:00 PM, every day" instead of the terser default.

Two options cover the failure modes. throwExceptionOnParseError defaults to true, meaning a malformed expression throws; set it to false and the exception message becomes the description, which is usually what you want in a UI where users type. logicalAndDayFields defaults to false and changes how expressions specifying both day of month and day of week are worded, producing only on Friday rather than and on Friday. That choice exists because the two dialects disagree about whether specifying both fields narrows or widens the schedule.

Quartz syntax, nicknames and field counts are handled in one parser

The feature list claims support for all cron special characters including * /, - ? L W and #, for expressions with five fields, six fields with seconds or a year, or seven with both, and for Quartz Job Scheduler expressions generally. Time nicknames @yearly, @annually, @monthly, @weekly and @daily are recognised.

The README examples show how much of that is really used. A Quartz style expression with a question mark and a day range becomes a plain sentence:

js
cronstrue.toString("0 23 ? * MON-FRI");
> "At 11:00 PM, Monday through Friday"

The # selector for the nth weekday works too, where "23 12 * * SUN#2" is described as "At 12:23 PM, on the second Sunday of the month". Nicknames expand rather than being echoed: @monthly is described as "At 12:00 AM, on day 1 of the month".

The 24-hour option interacts with the last one, so "23 14 * * SUN#2" with use24HourTimeFormat set to true reads "At 14:23, on the second Sunday of the month". A step applied to the day-of-week field, written as 2-6/2, is described as every 2 days of the week, Monday through Friday once dayOfWeekStartIndexZero is turned off.

The boundary here is real: the parser accepts what the documented syntax allows and does not validate against your particular scheduler. It will describe an expression your cron daemon would reject, and it will describe one your daemon accepts under different semantics. If your product shows a schedule to users who cannot check the raw string, verify the expression against the real scheduler before shipping.

Locales are opt in, and the bundle sizes decide how you load them

Only English is included by default. Anything else is an explicit import, and the library gives two ways to do it.

The first pulls every locale at once through cronstrue/i18n:

js
import cronstrue from 'cronstrue/i18n';

cronstrue.toString("*/5 * * * *", { locale: "fr" }); // => Toutes les 5 minutes
cronstrue.toString("*/5 * * * *", { locale: "es" }); // => Cada 5 minutos

The cost is stated plainly: about 130k minified for the full bundle, which loads slowly in a browser.

The second loads the main module plus the locales you actually need, through cronstrue/locales/[locale]. The main module is about 42k minified and each locale about 4k, so a product supporting two languages ships roughly 50k instead of 130k. The i18n section supports 40+ languages.

Because each locale is a separate module, the packaging has to expose them individually. The files array in package.json confirms it ships dist/, locales/, i18n.js and i18n.d.ts, and the repository root carries a matching i18n.js and i18n.d.ts so the subpath resolves.

One detail worth knowing: use24HourTimeFormat defaults to false, but some translations default to true, so the same expression can render differently across locales unless you pass the option explicitly.

Maintenance pace, licence and what the library will not do for you

The repository is not archived and the last push was on 2026-10-12, the same day as v3.32.0. Two earlier releases, v3.31.0 and v3.30.0, landed on 2026-10-08 and 2026-10-06, so the release cadence at that moment was frequent. The MIT licence grants use, modification and redistribution with attribution and no warranty, and this is not legal advice.

The build setup explains the release shape. The test script runs mocha through ts-node over test/**/*.ts, and the unit tests in test/cronstrue.ts are pointed to by the README as the source of further usage examples including very complex expressions. That test file is arguably the most useful documentation the project has, because every option combination is exercised there in executable form. The build produces declaration files with tsc and a webpack bundle, with terser-webpack-plugin for minification, and the prepublish script clears dist and rebuilds it.

What the library does not do is execute anything, and it is not a validator for a specific scheduler. There is no output other than a description, no evaluation of whether a schedule is sensible, no timezone handling in the options list, and no documentation of how seconds or year fields interact with a daemon that does not support them. If your requirement is validation rather than explanation, this is the wrong dependency. If your requirement is a readable sentence next to an input box, a 42k module with no dependencies and a CLI is a hard package to beat.

Editorial conclusion

Adopt cRonstrue when the only job is rendering a schedule as text, especially if users outside your team type the expressions or the schedule is Quartz rather than plain five field cron. Do not adopt it as a scheduler: it describes, it does not run anything, and it will not tell you that an expression your scheduler accepts is semantically odd. Before shipping, set dayOfWeekStartIndexZero and monthStartIndexZero explicitly for your platform instead of relying on the defaults, and switch throwExceptionOnParseError to false only if a malformed expression should degrade to an error string in the UI rather than throw.

Frequently asked questions

How do I install and use cronstrue in Node?

Run npm install cronstrue, then require it with const cronstrue = require('cronstrue') in CommonJS or import cronstrue from 'cronstrue' in ESM and TypeScript. Calling toString with "*/5 * * * *" returns "Every 5 minutes".

Does cRonstrue support Quartz cron expressions?

Yes. Quartz Job Scheduler expressions are supported, including the ? and # characters, so "23 12 * * SUN#2" is described as "At 12:23 PM, on the second Sunday of the month". Five, six and seven field expressions are supported.

How do I get cronstrue output in a language other than English?

Only English loads by default. Import every locale with cronstrue/i18n, which is about 130k minified, or import single locales from cronstrue/locales/[locale], where the main module is about 42k and each locale about 4k, then pass the locale option to toString.

What happens if an invalid cron expression is passed to toString?

By default throwExceptionOnParseError is true, so the call throws. Setting that option to false catches the exception and uses its message as the description instead, which suits a user-facing input box.

Can I run cronstrue from the command line?

Yes. package.json declares a bin entry pointing at bin/cli.js, so npx cronstrue "*/5 * * * *" prints Every 5 minutes without installing anything locally.

Official sources

  1. bradymholt/cRonstrue 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-cronstrue.svg)](https://hysenlabs.com/projects/bradymholt-cronstrue)

Was this useful?