One VTU logoOne VTU
All posts
EngineeringAIProduct

Every rule that makes a timetable a timetable is English text in a prompt

We went looking for the scheduler in our AI study timetable and there is not one. No overlap detection, no chronological sort, no feasibility check — the plan is whatever the model returns, and the rules exist only as sentences asking it to behave.

12 September 20266 min read

The AI Study Timetable asks for a lot: your daily routine, your weekly commitments, your exams, how many hours a day you want to study, and how long a session should be. It hands back a day-by-day plan.

Going in to write about it, we assumed there was a scheduler in there. There is not.

There is no planner

We searched for one properly: no scheduler class, no plan-building engine, and no date arithmetic in the planning path at all. The only inDays call in the whole AI layer is a display helper that works out how many days remain for a countdown label.

The only deterministic code in the feature is minor housekeeping: normalising time strings, building fallback sections when a part of the plan is missing, and blanking sections the student has no data for.

The schedule is generated, not computed. Which means every property that makes a schedule a schedule is not enforced anywhere — it is requested.

So the rules are prose

These are real lines from the prompt, asking politely for things a scheduler would simply guarantee:

Every start and end MUST be a 24-hour time in strict "HH:mm"
format. Never use AM/PM.

Blocks must be in chronological order, must NOT overlap, and
each block's "end" must be later than its "start".

Study blocks should add up to about {maxDailyStudyHours} hours,
split into sessions of roughly {preferredStudySessionMinutes}
minutes.

Chronological order. No overlaps. Ends after starts. Inside the hours the student asked for. Every one of those is an invariant, and every one of them is a sentence in a prompt that a language model may or may not honour on any given run.

We know how that sounds. It is also, deliberately, the design — more on why below.

The failure we were actually afraid of

It was not an overlapping block. It was the model inventing things.

Ask a model to fill in a backlog section and it will fill it in, whether or not the student has backlogs. Ask for exam countdowns and it will produce plausible subjects with plausible dates. The plan then renders those as fact, and a student revises for an exam that does not exist.

So there are two defences, and they are stacked on purpose.

First, it is asked not to. When a student has no backlogs or no exams, the prompt gains a section headed === SECTIONS TO LEAVE EMPTY ===, instructing it to return an empty list for that part of the schema.

Then it is not trusted. After the response is parsed, those two sections are replaced with empty values in code whenever the student’s input list was empty. The comment on that code says it plainly: models otherwise fill both with invented subjects and dates, which the UI then presents as fact.

That is the shape of the whole feature: ask the model for the good version, then delete anything it made up. The code says the model is asked, not trusted — and the ordering of those two words is the design.

The one repair we do make

Times come back in whatever shape the model felt like: 9, 9:00, 9.00, 9 AM, 9:00 pm, and occasionally with a full-width colon from a model that has spent too long in East Asian text.

All of those are normalised to HH:mm. The important part is what happens when a time is not recognised: it is passed through unchanged.

That is a decision, not laziness. Guessing at a malformed time means silently changing when a student is told to study. Passing it through means a strange time appears on screen where they can see it and correct their inputs — a visible oddity instead of an invisible error.

What we deliberately do not validate

There is no overlap detector. No chronological-order check. No check that blocks sit inside waking hours. No check that the plan totals the hours requested. No feasibility check at all — if your exams start in three days and you asked for six hours a day, nothing in the app will tell you that is impossible.

That is uncomfortable to write down, so here is the reasoning.

Once you have detected a bad schedule, you have two options, and both are worse than shipping it.

Reject and retry. The student already sat through a rewarded ad for this plan. A retry costs them another credit and up to a minute of waiting, and the second attempt can fail the same way.

Repair it. Re-flow overlapping blocks, re-sort them, trim to the hour budget. But then the app has authored the schedule and is still presenting it as the model’s plan. That is the same category of act as inventing exam dates, which we had just gone to some trouble to refuse.

So the constraints stay in the prompt and out of the parser. It is a real limitation and it is the one we chose.

Two bugs worth admitting

Gemini 2.5 spent the entire budget thinking. Its reasoning tokens are consumed before any output, and a full timetable response is large — so the request burned the whole attempt window on internal reasoning and came back as a timeout. The fix is to send a thinking budget of zero, but only for models whose name contains 2.5, because the 2.0 models reject that field with a 400. A version check by substring is not elegant; it is what the API requires.

A rate limit told us nothing. When a provider returned 429, we threw the response body away and showed “Rate limit reached”. Which is the same message whether the fix is trying another key or waiting. We now extract the provider’s own explanation — truncated to 300 characters, because an error body can echo your request content back and we are not putting a student’s exam dates into a log line.

The countdown goes stale, and we know

Each exam countdown carries a number of days remaining, and that number is computed once, when the plan is generated, and then written into the stored plan.

Reading a plan back does not recompute it. So a plan generated a month ago still cheerfully says forty days remaining. If an exam date cannot be parsed at all, the same code path produces a countdown reading “0 days remaining” for an exam that genuinely exists.

And a plan never refreshes itself: regenerating overwrites the old plan in place, so there is no history and nothing to compare against.

Why exams come from the exam timetable

Exam dates used to be typed in by hand during setup. They are now read from the official exam timetable, and the manual step is gone entirely.

The reason is the same class of problem as the invented countdowns, but with the student as the source of the error. Dates typed in by hand drift — a schedule is set, the university moves a paper, nobody updates the app, and the plan quietly counts down to the wrong day. Reading them from one place means there is one answer, and it is the university’s.

What it is, honestly

This feature is a good prompt and a careful set of deletions. It is not a scheduler, and calling it one would be the kind of thing we would criticise in someone else’s app.

What it does have is a hard rule about fabricated data: the model is never allowed to introduce a subject, a date or a backlog that the student did not provide. That rule is enforced in code, twice. The rest of the plan is a suggestion the model makes from a description of your week — which is useful, and worth reading before you follow it.

Keep reading

Related reading

EngineeringAndroidProduct

The card you mark as known never comes back

Flashcards has three states and no spaced-repetition scheduler. Marking a card known removes it from revision permanently, and four different screens disagree about which cards are due.

16 September 20264 min read