Assist has no reminder intent. Timers are relative-only, satellite-bound and
carry no message; list intents carry no time. So I built the missing piece:
custom sentences plus an intent_script, with a Local To-do list as the store.
"remind me at 2:30 pm to do laundry"
"remind me in twenty minutes to check the oven"
"remind me tomorrow morning to call the bank"
"remind me every Thursday at 6 pm to put out the trash"
"what are my reminders"
"cancel the reminder to do laundry"
"snooze for 20 minutes"
When one comes due it is spoken on every Assist satellite and pushed to your
phone with Snooze buttons.
Why none of the time parsing goes through the LLM
I started by doing it the obvious way — hand the sentence to the model, ask for
a timestamp. Two measurements changed my mind.
Asked for ISO 8601, ministral-3:8b came back with:
{"what": "water the plants", "when": "2023-08-12T18:19:00-05:00"}
The time of day is arithmetically perfect: I said “in a couple of hours” at
16:19. The year is three years wrong, so the reminder failed a
sanity check and nothing was scheduled.
Worse, over six identical requests, two produced no tool call at all — the
model simply replied “I’ll remind you at 7 AM tomorrow. I’ve set it.” and
created nothing. No amount of wording in the tool description fixed it. A
confident spoken confirmation for a reminder that does not exist is worse than
an error, because you stop thinking about it.
So every time is computed in Jinja from integers the grammar has already
validated. There is still an LLM fallback for phrasings the grammar misses, but
it supplies hour / minute / days_from_today — never a date — and the
confirmation is read back off the to-do list rather than from the model’s own
account of what it did.
If you are using the OpenAI/Ollama blueprint for this, its own documentation
notes the Ollama variant “struggles with relative time expressions like ‘in 3
hours’”. I think this is the same problem seen from the other side.
Prior art, because this is not an empty field
Three projects already do voice reminders, and I would not have got here as
quickly without reading them:
- Local voice reminders created with Assist — deterministic parsing, actionable notifications, weekday support. The most complete of the three.
- Assist: Create reminders by voice — a blueprint, one-click install, LLM time parsing.
- Reminders — Create and List Tasks — deterministic, creates and lists.
What is different here: reminders are spoken on the satellite rather than
only pushed; cancelling and snoozing work by voice; and there are recurring
series (“every Thursday at 6 pm”), which none of the three support.
Two of them are blueprints and install in one click. Mine is copy-two-files and
edit-two-lines. I looked at closing that gap and could not, and the reason is
worth knowing if you are building anything on Assist:
Blueprints exist only for automation, script and template. There is no
blueprint form for intent_script or for custom sentences. This project is 12
intent_scripts and one sentences file, plus three automations — so the only
blueprintable part is the plumbing, not the feature. A blueprint release would
mean copying the same files anyway, then also importing three blueprints and
configuring each, with the package’s automations and the imported ones both live
at once. More steps and more ways to get it wrong, so I dropped it.
If someone knows a way to ship an intent_script plus its sentences as a
one-click install, I would genuinely like to hear it. That is the one place the
blueprint-based projects are ahead, and it is a packaging limitation rather than
anything about the feature.
The phrasings that break, and why there are tests
A voice grammar fails in ways the config cannot show you. Six separate bugs were
found by someone saying a perfectly normal sentence that quietly matched
nothing:
| Said | Why it failed |
|---|---|
remind me at 2 p.m. … |
hassil does not strip punctuation, and only pm was listed |
set reminder for 9 pm … |
the article in “set a reminder” was mandatory |
remind me at 10.30 … |
only : was accepted as a minute separator |
remind me at 11 that it's move-in time |
only to joined the time to the task |
cancel reminders |
“cancel all reminders” required the word “all” |
There is also a trap worth knowing about even if you never use this: the
built-in HassStartTimer has wildcard sentences, so “remind me to do laundry
in two hours” matches the timer intent, with conversation_command = "remind me to do laundry". You get a timer — satellite-bound, no message, gone on
restart — instead of a reminder, and nothing tells you.
So the repo ships a test suite that runs offline in a venv, no Home Assistant
required:
pip install -r requirements-dev.txt
./tools/check_all.sh --offline # ~5 seconds
It generates about a thousand plausible phrasings and reports two things:
sentences that match nothing, and — more importantly — sentences that match but
where a time ended up inside the task wildcard. That second one schedules
the wrong time while sounding completely successful, and it is the only thing
the sweep treats as a hard failure.
There is also a one-liner for when something misbehaves in the Assist
transcript:
python tools/reminders_why.py "the exact text you said"
which prints the matched intent and every slot, or NO MATCH.
FINDINGS.md
While building it I ended up documenting 19 pieces of Assist behaviour that
either are not written down or contradict what the source’s own names suggest.
A few that cost me real time:
prefer_local_intentsfilters the opposite way to how it reads. With a
CONTROL-capable agent, the filter named “filter out intents that are not local
fallback” is a rejection list:HassGetStateandHassMediaSearchAndPlay
are the two that get handed to the LLM. Everything else stays local.- Every
intent_scriptis auto-published as an LLM tool, with parameters
derived from a generic name/area/floor schema that cannot express your custom
slots. The model then calls it with nonsense.platforms: [conversation]
suppresses the tool while keeping the voice path working. - The acknowledgement beep silently discards spoken responses. If every
entity a command targets is in the same area as the satellite, you get a beep
instead of your custom response, no matter what the response says. - Whisper’s
initial_promptmatters more than the model size. Swapping
small-int8forbase-int8looked like it lost accuracy — until I noticed
the prompt still described only light commands. With the prompt updated, base
matched small’s accuracy at a third of the latency.
Known limitations
- Monthly recurrence is unsupported. Month arithmetic in Jinja means deciding
what the 31st means in February, and a wrong date is worse than an
unsupported phrase. - “remind me at 6 to start dinner” is transcribed as “it’s 6” by every Whisper
configuration I tried. That is acoustic, not a grammar problem, and I have not
worked around it because accepting “it’s” for “at” collides with real tasks. - No re-send-until-acknowledged. The Companion app fires no event when a
notification is dismissed, so a nag loop would keep firing through the exact
gesture people use to mean “seen it”. - English only. The grammar is one file and the structure should port, but I
have not tried.
Happy to answer questions, and if you are one of the three authors above — thank
you, the reading saved me a lot of time.