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,INCOMPLETEorINVALID.
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:
- Optional: if the text is already a cron line (
*/5 * * * *,@daily), answer with it asrules.pydoes and skip the model. - Cut the text to 256 characters. If only whitespace is left, answer
INVALID. - 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. - Run
model.onnxwith two int64 arrays of shape (1, length):chars(code points) andtokens(ids from step 2). - Read the outputs:
class_index(index intoclassesinconfig.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,)) andpairs(shape (1, 3, 24, 60): whether hour h fires at minute m). - For a schedule, read the first
line_countslots. In each slot, for each hour that is on, take the minutes that are on and thatpairsmarks for it; hours with the same minutes share a line. Ifpairsmarks 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 isUNSUPPORTED. Write a field as*when every value is on,*/nwhen the values are every n-th from the field's lowest value, otherwise the values, with runs of 3 or more as ranges (8,9but8-10). In the two day fields write a step asfirst-last/n, never*/n. If both day fields are restricted andday_oris false, answerUNSUPPORTED. - 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:
- Stack Exchange: android, apple, askubuntu.com, dba, devops, drupal, magento, pm, raspberrypi, salesforce, serverfault.com, sharepoint, sqa, superuser.com, unix, webapps, webmasters, wordpress and Stack Overflow (CC BY-SA 2.5, 3.0 or 4.0, by post date);
- TOPv2 (Chen et al. 2020, CC BY-SA 4.0);
- Amazon MASSIVE 1.1 (CC BY 4.0);
- SMCalFlow 2.0 (CC BY-SA 4.0) and PRESTO (CC BY 4.0);
- MTOP (CC BY-SA 4.0), Multilingual Task-Oriented Dialog (CC BY-SA), SLURP (CC BY 4.0) and HWU64 (CC BY 4.0);
- code comments next to cron lines in public repositories, found with Sourcegraph.
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
Dataset used to train BryceTRussell/tinycron
Evaluation results
- Exact match on Template requests (held out)self-reported0.961
- Exact match on Requests written by an AI model (held out)self-reported0.885
- Exact match on TOPv2 alarm and reminder requests (test)self-reported0.966
- Exact match on Real assistant requests from five datasets (held out)self-reported0.942
- Exact match on Hand-written tricky requestsself-reported0.955
- Exact match on Real text from code, forums and Q&A sites (held out)self-reported0.867
- Exact match on GitHub issue sentences (never trained on)self-reported0.747