tinycron

Turns an English schedule into a Linux cron line.

Input Output
every weekday at 9am 0 9 * * 1-5
Every Monday At 5:30 PM 30 17 * * 1
every 15 minutes between 9 and 5 */15 9-17 * * *
twice a month 0 0 1,15 * *
nightly, or twice a day 0 */12 * * *
every other tuesday UNSUPPORTED
every few hours INCOMPLETE

If cron can't express a request, the answer is a class instead:

  • UNSUPPORTED: cron can't express it (seconds, "every other week", "the last day of the month").
  • INCOMPLETE: a time or count is missing.
  • INVALID: not a schedule.

Each answer has a confidence from 0 to 1. The model is 1.5 MB (1.7 MB with its tokenizer) and takes 2 to 4 ms on a laptop CPU.

Use

pip install -r requirements.txt
python to_cron.py "every weekday at 9am"
python to_cron.py --json < requests.txt
from to_cron import CronModel
model = CronModel()
model.to_cron("every weekday at 9am")   # "0 9 * * 1-5"
model.read("at 7am on weekdays and 9am on weekends")
# ("schedule", ["0 7 * * 1-5", "0 9 * * 0,6"], 0.96...)

The command marks an answer (unsure, check it) when its confidence is under the 95% threshold in config.json. Answers above that threshold were right 95% of the time on the dev set, counting UNSUPPORTED and INVALID as one answer.

Tested with Python 3.12 and onnxruntime 1.19, and with onnxruntime-web 1.20.1 (WebAssembly), which gives the same answers. Needs onnxruntime 1.19 or newer. Loading the model takes about 0.4 seconds, once.

Files

File What it is
model.onnx The model
tokenizer.json The tokenizer
config.json Version, file hashes, field layout, confidence thresholds
to_cron.py Python runner, as a command or a module
rules.py Exact parser for text that is already a cron line (optional)
requirements.txt Python packages for to_cron.py
examples.jsonl Test vectors for checking a port

Output

  • One to three standard 5-field cron lines (Vixie cron and cronie; Sunday is 0).
  • Or UNSUPPORTED, INCOMPLETE or INVALID.

Never seconds, years, L, W, # or @ shortcuts.

Defaults for vague requests:

  • No time means midnight: "every Monday" is 0 0 * * 1.
  • "Morning" is 9:00. "Every night at 7" is 19:00.
  • A bare hour from 1 to 6 is afternoon: "at 3" is 15:00.
  • "Weekly" is Sunday at midnight. "Monthly" is the 1st at midnight.
  • "Between 9 and 5" includes the last hour: 9-17.
  • Time zones are ignored.

Accuracy

Exact match on held-out sets.

Text Rows Accuracy Wrong per 1,000
Template requests 2,875 96.1% 39
Requests written by an AI model 1,762 88.5% 115
Real alarm and reminder requests (TOPv2) 1,510 96.6% 34
Real assistant requests from five more datasets 745 94.2% 58
Hand-written tricky requests 155 95.5% 45
Real text from code, forums and Q&A sites 2,978 86.7% 133
GitHub issue sentences, never trained on 2,699 74.7% 253

Other tools on the same held-out rows, counting only rows whose answer is a schedule, scored leniently:

System Template AI-written Real requests (TOPv2) Real text
english-to-cron 0.1.7 (Rust rules) 23.3% 29.3% 49.5% 55.8%
natural-cron-expression 1.0.2 (PHP rules) 16.3% 21.7% 56.8% 61.7%
cronformer 0.1.0 (6.3M parameters, 25 MB) 43.2% 41.6% 70.3% 64.8%
cronlm (16M parameters, 24 MB) 43.0% 45.0% 71.0% 45.9%
cron-mini (Qwen2.5-1.5B fine-tune, 3.1 GB)* 43.7% 44.6% 66.7% 61.0%
leom21/cron (SmolLM2-360M fine-tune, 724 MB)* 11.1% 9.1% 36.5% 24.4%
Claude Haiku (October 2026), prompted*† 80.2% 76.9% 46.9% 71.5%
Claude Sonnet (October 2026), prompted*† 78.6% 78.5% 66.7% 79.7%
Claude Opus (October 2026), prompted*† 81.7% 81.8% 66.7% 78.9%
this model (1.7M parameters, 1.5 MB) 96.1% 87.1% 97.0% 89.3%

* The language models are too slow for whole splits, so they ran on a seeded sample of 150 rows per column (96 to 126 of them schedules). On the same sample this model scores 95.2%, 85.1%, 96.9% and 87.8%. leom21/cron has no documented prompt; it got "Convert to cron: " before the text, the best of the prompts tried.

† The Claude models got the same one-paragraph prompt: Vixie cron, up to three lines, or UNSUPPORTED, INCOMPLETE or INVALID. Most of their TOPv2 misses are refusals of requests this model's labeling rules read as schedules. A second Haiku run with the same prompt scored 64.6% on TOPv2 and 61.8% on real text, so treat these scores as rough to 10 points or more. Claude also wrote the AI-written requests and helped label the data, which may favor them.

Limits

  • English only.

  • Accuracy depends on the kind of text:

    Text Wrong per 100
    Spoken requests to an assistant ("wake me at 7 on weekdays") 3 to 6
    Sentences from code, forums and Q&A sites 14
    Sentences from GitHub issues 25
    Short entries typed into an app ("daily except Fridays", "every 4 hrs from 0200") 35
  • The most common mistake is the class: a schedule answered with a class, or a class answered with a schedule. The next is one wrong field.

  • Weakest on different times on different days in one request, days of the month ("1st-10th"), days by exclusion ("daily except Fridays"), from-to events ("gym 6 to 7am"), windows with a step, steps with a start ("every 3 hours starting at 1am"), and cut-off entries ("every monday at").

  • Reads one request at a time, not a whole paragraph.

Porting to another language

to_cron.py is the whole recipe. The steps:

  1. Optional: if the text is already a cron line (*/5 * * * *, @daily), answer with it as rules.py does and skip the model.
  2. Cut the text to 256 characters. If only whitespace is left, answer INVALID.
  3. Tokenize with tokenizer.json (no special tokens). Give every character the id of the token that covers it, 0 for none. Offsets must count Unicode code points: convert Rust byte offsets and JavaScript UTF-16 offsets first.
  4. Run model.onnx with two int64 arrays of shape (1, length): chars (code points) and tokens (ids from step 2).
  5. Read the outputs: class_index (index into classes in config.json; 0 is a schedule), line_count (1 to 3), one boolean array per field (minutes, hours, doms, months, dows, shape (1, 3, field size); index i means the field's offset + i), day_or, confidence (shape (1,)) and pairs (shape (1, 3, 24, 60): whether hour h fires at minute m).
  6. For a schedule, read the first line_count slots. In each slot, for each hour that is on, take the minutes that are on and that pairs marks for it; hours with the same minutes share a line. If pairs marks nothing, the slot is one line of all its hours and minutes. Drop repeated lines, then sort by first hour, then first minute. More than 3 lines is UNSUPPORTED. Write a field as * when every value is on, */n when the values are every n-th from the field's lowest value, otherwise the values, with runs of 3 or more as ranges (8,9 but 8-10). In the two day fields write a step as first-last/n, never */n. If both day fields are restricted and day_or is false, answer UNSUPPORTED.
  7. Check the port against examples.jsonl.

Run one text at a time, or batch texts of the same length.

Model

Architecture Character-level bidirectional GRU with pretrained word piece vectors from cross-encoder/ms-marco-MiniLM-L-6-v2
Parameters 1.7 million, 0.5 million of them the word piece table
Size 1.5 MB, and 0.3 MB of tokenizer
Weights Word piece table 3-bit, GRU 5-bit, output layers 6-bit, character table 8-bit; unpacked when the model loads
Format ONNX, opset 20
Input English text, up to 256 characters
Output One to three cron lines or a class, with a confidence
Runs on CPU, offline

Training data

About 169,000 labeled rows, plus about 56,000 texts labeled by earlier models of this design. Real text comes from:

The rest is generated: template requests and AI-written requests, checked by blind readings. microsoft/deberta-v3-xsmall (MIT) and cross-encoder/ms-marco-MiniLM-L-6-v2 (Apache-2.0) served as training teachers and do not ship with the model. No non-commercially licensed text was used.

Changes

  • 1.0.0: first release.
  • 1.0.1: the runtime uses one thread, which is faster on a laptop CPU. Same model.
  • 1.1.0: the weights are packed to 3, 5 and 6 bits, and the tokenizer drops 11,366 word pieces the model never trained on: 3.2 MB to 1.5 MB, the tokenizer 0.5 MB to 0.3 MB. Same answers on the test vectors; scores move by at most 0.7 points (table above).

License

Apache-2.0 for the model and the code in this folder. Training data credits are above.

Downloads last month
44
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Dataset used to train BryceTRussell/tinycron

Evaluation results

  • Exact match on Template requests (held out)
    self-reported
    0.961
  • Exact match on Requests written by an AI model (held out)
    self-reported
    0.885
  • Exact match on TOPv2 alarm and reminder requests (test)
    self-reported
    0.966
  • Exact match on Real assistant requests from five datasets (held out)
    self-reported
    0.942
  • Exact match on Hand-written tricky requests
    self-reported
    0.955
  • Exact match on Real text from code, forums and Q&A sites (held out)
    self-reported
    0.867
  • Exact match on GitHub issue sentences (never trained on)
    self-reported
    0.747