Voice reminders for Assist, with the time worked out in Jinja rather than by an LLM

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.

GitHub - peerloomllc/ha-voice-reminders: Deterministic spoken reminders for Home Assistant Assist — no LLM deciding what time you meant · GitHub — MIT.

"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:

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_intents filters 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: HassGetState and HassMediaSearchAndPlay
    are the two that get handed to the LLM. Everything else stays local.
  • Every intent_script is 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_prompt matters more than the model size. Swapping
    small-int8 for base-int8 looked 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.

Thanks for the tag, interesting write up.
It is one thing that always confuses/surprises/interests me when people try what you tried at the start.

I haven’t really dived into parsing times/durations/dates beyond my reminder project you tagged. I still use this on a daily basis for every one of my time-critical to-do items, usually keeping < 5 active and if they’re not time critical or getting too far down the list I move them to larger kanban-ish list on vikunja. Anyway…

I see LLM as a good intermediate tool for developing, but always seems like a terrible tool for repeated processing, especially for a pretty deterministic and known set of inputs. Just from asking it random questions it’s clearly never giving the same deterministic answer, so it seems to me like it would be pretty unpredictable in exactly the ways you found. Instead it seems better for single instances of calculating and programming something in a way that can be implemented and verified exactly as you did.

Some other fun examples around for time and reminder processing, one that comes to mind is the “natural language processing” that to-do list apps like todoist implement. They take a wide variety of possible inputs but they’re all still in a list of possibilities that can be written out and define exactly what they should translate into every time.

The slight downfall is less flexibility, there are certainly a few phrasings that my “Remind me” script fail, but just using it for a little while it was easy for a few people in my household to get used to it. I’ve wanted to implement similar for setting repeating reminders but haven’t gotten around to seeing how they can be tracked in HA tasks/calendars yet.

And yes, you saw at least at the time I made my reminders script there wasn’t any easy way to package it. HA has even come a long way since then, I was playing with one of the pretty early “year of the voice” versions that have been improved since…

Yea, I tried pretty early on but then got distracted. Now with Claude and other tools it was just a few hours of back and forth testing to get it where I wanted it. Deterministic todo lists, repeating tasks, etc is now fully functional for me. Finally can replace Google Nests/Assistant for easily setting reminders for myself

I have HAOS running in a proxmox vm. How do I get the two files INTO it? I have file editor and could copy/paste them into created files in a created matching folder named correctly. Would that do the trick? Assuming yes I created the two files at the correct place and pasted the contents from the raw output on your github. I setup packages in configuration.yaml and created a “Reminders” list. Restarted home assistant and told it “Remind me in 2 minutes to test reminders.” to which it replied “Unexpected error during intent recognition” which is a new one on me. I did find that my phone notification entry is “notify.n3a” and not the format yours had but would think that would only crash when trying to SEND the timer expired?

I had to smack Claude around a bit to get a response that wasn’t 10 pages long, lol, but try this:

Copy/paste through File editor is fine — just make sure the paths are exactly
/config/custom_sentences/en/reminders.yaml (the en/ is not optional) and
/config/packages/reminders.yaml. If you have Terminal & SSH it rules out a
mangled paste:

  mkdir -p /config/custom_sentences/en /config/packages
  wget -O /config/custom_sentences/en/reminders.yaml \
    https://raw.githubusercontent.com/peerloomllc/ha-voice-reminders/master/custom_sentences/en/reminders.yaml
  wget -O /config/packages/reminders.yaml \
    https://raw.githubusercontent.com/peerloomllc/ha-voice-reminders/master/packages/reminders.yaml

You’re right that notify.n3a isn’t it. That message is assist_pipeline’s
catch-all for anything escaping the conversation agent — and errors from inside
an intent_script action never reach it, they get wrapped and spoken as a normal
“sorry” response. So this failed before the intent ran: either the package didn’t
load (sentence matched, no handler registered) or the sentences file didn’t parse.

Quickest check: after a restart, Settings → Automations & Scenes should list three
automations starting “Reminders —”, and Developer Tools → Actions should offer
Remind me. If they’re missing, the package isn’t being read — usually a second
homeassistant: block in configuration.yaml; the packages: include has to go
under your existing one. Developer Tools → YAML → Check configuration will
name the file and line.

If they are there, it’s the grammar file: Settings → System → Logs → Load full
logs, and the traceback next to that message will name it.

Two things to fix regardless. The notify anchor near the top of
packages/reminders.yaml (in the CONFIGURATION block) — but check Developer Tools
→ Actions for the exact name first. The Companion app makes both a notify.n3a
entity and a notify.mobile_app_n3a action; you want the action, because the
Snooze buttons ride in a data: payload the entity-style call can’t carry. And
confirm in Developer Tools → States that your list is exactly todo.reminders
a second Local To-do list would give you todo.reminders_2.

While iterating: intent_script.reload after a package edit, conversation.reload
after a sentences edit. No restart needed.

I’ll try things using the ssh addon you mentioned..just need to decide which one to install. It looks like the community version enhances the original if I read right. Appreciate the detailed response!

@peerloomllc I have 2 google home devices named “Bedroom Display” and “Kitchen Display”. Is there a way to have reminders announced there? Currently my only “assist satellites” are two phones in the house running home companion. What happens if music is playing via music assistant when the reminder is due?

update The basic reminders are working great. I did have opencode zen add the notifications to my google displays at the same time. I actually intended it to create a NEW automation to handle that part but it decided to mod remindlers.yaml directly instead. It worked so I left it for now but assume when you put out an update I’m gonna run into issues.

Um, cannot this be installed via HACS??