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.6 MB (1.9 MB with its tokenizer) and takes 2 to 4 ms on a desktop 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.99...)
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.5 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. None of these rows are in the training data.
| Text | Rows | Accuracy | Wrong per 1,000 |
|---|---|---|---|
| Template requests | 2,875 | 98.4% | 16 |
| Real alarm and reminder requests (TOPv2) | 1,510 | 96.4% | 36 |
| Real assistant requests from five more datasets | 745 | 94.2% | 58 |
| Fresh requests written by an AI model | 949 | 93.9% | 61 |
| Real text from code, forums and Q&A sites | 2,978 | 87.9% | 121 |
Errors on these sets guided the training data, so they flatter this model:
| Text | Rows | Accuracy | Wrong per 1,000 |
|---|---|---|---|
| Requests written by an AI model | 1,762 | 95.2% | 48 |
| Hand-written tricky requests | 155 | 97.4% | 26 |
| GitHub issue sentences, never trained on | 2,699 | 77.6% | 224 |
Other tools on the same 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.9M parameters, 1.6 MB) | 98.6% | 94.8% | 96.7% | 90.9% |
* 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 98.4%, 93.4%, 95.8% and 91.9%. 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.
This model's errors on the AI-written column guided its training data, so its lead there is flattered.
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") 4 to 6 Sentences from code, forums and Q&A sites 12 Short entries typed into an app ("daily except Fridays", "every 4 hrs from 0200") 14 Sentences from GitHub issues 22 The
95%threshold is fitted on the dev set, and how far to trust it depends on the text. Above it, answers were right 97% to 98% of the time on spoken requests, 93% on sentences from code, forums and Q&A sites, 92% on short app entries and 85% on sentences from GitHub issues, countingUNSUPPORTEDandINVALIDas one answer.In sentences, 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. In short requests, one wrong field comes first, then the class.
Weakest on different times on different days in one request, requests with a detail missing (
INCOMPLETE), several minutes past the hour, hour windows ("9 to 5"), lists of hours, days by exclusion and days of the month.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,)),pairs(shape (1, 3, 24, 60): whether hour h fires at minute m) andhour_days(shape (1, 3, 24, 38): for hour h, the first 7 values are weekdays from Sunday and the last 31 are days of the month from the 1st). - 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; an hour with none is dropped. Give the hour the weekdayshour_daysmarks for it, or the slot'sdowsif it marks none, and the days of the month it marks, or the slot'sdomsif it marks none. Hours with the same minutes and days share a line. If every hour is dropped, the slot is one line of all its hours and minutes. Drop repeated lines, then sort by first hour, then first minute, then the line's text. 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 and n is at most half the field's size, 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, as the scores above were. In a batch the texts share the model's quantization scale, which changed 1 answer in 1,019 when tested.
Model
| Architecture | Character-level bidirectional GRU with pretrained word piece vectors from cross-encoder/ms-marco-MiniLM-L-6-v2 |
| Parameters | 1.9 million, 0.5 million of them the word piece table |
| Size | 1.6 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 200,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 requests and developer prose written by an AI model, checked by blind readings. microsoft/deberta-v3-xsmall (MIT) served as a training teacher and does not ship with the model. cross-encoder/ms-marco-MiniLM-L-6-v2 (Apache-2.0) also served as a teacher, and its word piece vectors and a cut-down copy of its tokenizer ship in model.onnx and tokenizer.json. No dataset under a non-commercial license was used for this model's training rows.
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: weights packed to 3, 5 and 6 bits and unused tokenizer pieces dropped: 3.2 MB to 1.5 MB. Same answers on the test vectors; scores move by at most 0.7 points.
- 1.2.0: retrained on app-style requests and prose that names times without asking for a schedule. Same size, speed and test vector answers. Held-out app-style requests 66.9% to 73.9% (counting
UNSUPPORTEDandINVALIDas one answer); other scores move by at most 1.6 points. - 1.3.0: new weights, trained on more short requests, with new heads for minute steps and for giving each hour its own days. A port must read the new
hour_daysoutput (steps 4 and 5). 1.6 MB, up from 1.5 MB, and 2.3 ms, up from 2.0 ms.rules.pynow only reads text that is already a cron line: it no longer fills gaps in the model's hours. Scores against 1.2.0: template 96.0% to 98.4%, AI-written 90.1% to 95.2%, tricky 96.1% to 97.4%, real text 86.5% to 87.9%, GitHub 74.8% to 77.6% (1.2 points of it from corrected labels), TOPv2 level at 96.4%, assistant 94.5% to 94.2% (two rows). Held-out app-style requests 73.9% to 85.9%, counted as in 1.2.0. Answers above the95%threshold are a little less reliable on long prose and spoken requests, and more reliable on short app entries. The test vector "remind me sometime" is nowUNSUPPORTED, wasINVALID.
License
Apache-2.0 for the model and the code in this folder. Training data credits are above.
- Downloads last month
- 93
Dataset used to train BryceTRussell/tinycron
Evaluation results
- Exact match on Template requests (held out)self-reported0.984
- Exact match on Requests written by an AI model (not trained on; errors guided training data)self-reported0.952
- Exact match on TOPv2 alarm and reminder requests (test)self-reported0.964
- Exact match on Real assistant requests from five datasets (held out)self-reported0.942
- Exact match on Fresh requests written by an AI model (held out)self-reported0.939
- Exact match on Hand-written tricky requests (not trained on; errors guided training data)self-reported0.974
- Exact match on Real text from code, forums and Q&A sites (held out)self-reported0.879
- Exact match on GitHub issue sentences (never trained on; errors guided training data)self-reported0.776