Voice agents
Voice agents in elderly care: 1,180 calls a week in Gouda
A 31-person elderly-care cooperative in Gouda was drowning in 1,180 weekly family callbacks. We put a Dutch voice agent in front of their Nedap Ons EPD. Here is what broke.

Tuesday, 09:14. The team coordinator at a 31-person elderly-care cooperative in Gouda is holding a printed call-back list. It has 38 names on it from Monday alone. Most are family members asking after their parent. Twelve are urgent. Three of those twelve have been waiting since Sunday.
This was the situation before we built them a Dutch voice agent. Six months later, the agent handles roughly 1,180 family callbacks a week, routes BOPZ-relevant signals to a senior nurse inside 60 seconds, and writes everything back into an eleven-year-old Nedap Ons installation. It also forgets things on a strict timetable, which turned out to be the hardest part of the project.
This post is the part you do not see in voice-agent demos. Real EPD, real Wkkgz constraints, real escalation paths. The numbers are anonymised at the client’s request.
The starting problem
The cooperative runs four small-scale zorgwoningen plus a day-care location. Thirty-one staff. Around 78 residents at any given moment, most living with some form of dementia. The team is good. The phone system, less so.
Three things were happening at once.
Family members were calling the central line at all hours, mostly during shift change, asking for an update on Mum or Dad. The day shift was answering during medication rounds. The night shift was getting calls at 23:00 from worried sons in Australia. Nobody was logging anything consistently.
The Nedap Ons EPD had eleven years of clinical history in it. New care notes were going in. Family conversations were not. So when a son called back two days later and said “I spoke to someone on Friday about my father’s swallowing”, nobody could find the note.
And the BOPZ flag, which is now really a Wzd flag since the law changed on 1 January 2020. Certain signals from family (“she said she wanted to leave”, “he hit her again”, “they refused medication this morning”) must be triaged by a qualified clinician and recorded with timestamps. Voicemail boxes do not do this.
The cooperative had two options. Hire two FTEs to staff a callback desk. Or build something. They asked us to scope option two.
What we built, in one paragraph
A Dutch-language voice agent answers the central callback number. It identifies the caller, pulls the resident record from Nedap Ons via a thin middleware, reads back the most recent care notes the family is authorised to hear, takes new information, writes it back as a structured note, and on any Wzd-relevant phrase escalates by SMS and push to the on-call senior nurse. Calls are recorded; transcripts are kept for seven years per Wkkgz; raw audio is deleted at 90 days.
That paragraph took five months to ship.
The eleven-year-old Nedap Ons
Nedap Ons is the dominant EPD for VVT (Verpleging, Verzorging en Thuiszorg) in the Netherlands. It has a documented API and several integration partners. The installation we inherited was on a release behind the current API contract by roughly three years. Two of the endpoints we needed had been deprecated and one had been replaced by a wholly different resource shape.
The cooperative was not going to do an EPD upgrade for our project. That would have been a six-figure programme on its own. So we built a thin adapter that speaks the old contract on the Nedap side and the new contract on our agent’s side. It runs as a small Node service inside the cooperative’s network.
A representative read path looks like this:
// adapter/getResident.ts
// Speaks legacy Nedap Ons on one side, our internal contract on the other.
import { onsClient } from "./onsClient";
import type { Resident } from "../types";
export async function getResident(bsn: string): Promise<Resident | null> {
const legacy = await onsClient.get(`/clienten/${bsn}`);
if (!legacy || legacy.status === "uitgeschreven") return null;
return {
id: legacy.clientId,
voornaam: legacy.voornaam,
achternaam: legacy.achternaam,
geboortedatum: legacy.geboorte,
locatie: legacy.locatie?.naam ?? "onbekend",
// Legacy returned a single array; newer releases split it.
contactpersonen: (legacy.contactpersonen ?? []).map((c) => ({
naam: `${c.voornaam} ${c.achternaam}`.trim(),
relatie: c.relatie,
telefoon: c.telefoon,
autorisatieniveau: c.machtiging ?? "basis",
})),
wzdStatus: legacy.bopz_status ?? "geen", // field still called bopz in storage
};
}
That last line is the kind of thing that catches you. The Wet zorg en dwang replaced BOPZ for elderly care on 1 January 2020. The Nedap installation predates that change. The field is still called bopz_status in storage. Our agent uses the current Wzd terminology in transcripts, but reads and writes the legacy field. We documented this in three places so nobody removes the alias in 2027 and breaks the escalation logic.
The 60-second escalation rule
The agent listens for a defined set of intents during family calls. Most are routine: how is she sleeping, did the pharmacy deliver, we are visiting Saturday at three. Those get a short factual answer drawn from the care notes the family member is authorised to hear, and a short summary is written back into Nedap Ons.
A second set of intents triggers a hard escalation. These are signals the Wzd requires a qualified clinician to assess:
- The resident is reported to be refusing care, food, or medication.
- The resident is reported to be physically restrained or trying to leave.
- A family member reports new aggression, falls, or a notable behavioural change.
- A family member raises a formal complaint as defined under Wkkgz.
When the agent classifies an utterance into any of these, three things happen inside 60 seconds:
- The agent says, in Dutch, that a nurse will call back shortly and gives a concrete window.
- The structured note is written to Nedap Ons with a Wzd tag.
- An SMS plus push notification fires to whichever senior nurse is on call, with the resident, the intent class, and a transcript link valid for 24 hours.
The 60-second budget is not a marketing number. It is the design ceiling we agreed with the cooperative’s manager zorg. We measure it. Median is 11 seconds. P95 is 34 seconds. The slowest path is SMS delivery via the carrier, which we cannot tune.
A voice agent in care is not a chatbot with a microphone. It is a triage system that happens to be conversational, and the triage rules are written in Dutch law.
Wkkgz, retention, and the only scalable delete
Audio retention is the part nobody warns you about. The cooperative is bound by the Wet kwaliteit, klachten en geschillen zorg (Wkkgz), plus the WGBO for clinical record retention. Different artefacts have different ceilings.
Our split:
- Raw audio: 90 days. Long enough to resolve a complaint, short enough to keep our risk surface small.
- Transcript with structured note: 7 years, written into Nedap Ons.
- Aggregated metrics (call volume, intent counts, escalation latency): retained indefinitely, no PII.
Ninety days is not a regulatory ceiling, it is our choice. Wkkgz allows longer. We argued, and the cooperative agreed, that audio adds little forensic value beyond the transcript once a few months have passed, and every extra month is risk.
Which brings us to the deletes. We are running on Postgres for the metadata, and the audio sits in object storage with a TTL. Postgres deletes are the painful kind. A piece doing the rounds on Hacker News this week made the argument that the only scalable delete in Postgres is DROP TABLE, which sounds glib until you have a calls table approaching nine figures of rows and a daily retention job that has to chew through it. We took the partition route from day one: one table per ISO week, dropped wholesale at day 90. The audio storage layer does its own thing on the bucket side.
If you are building anything with healthcare audio retention, model retention as a partitioning strategy from day one. Retrofitting it onto a single fat calls table is how you end up running DELETE jobs that never finish.
What broke
Six things were not in the original scope.
Dialect. The agent had to handle South-Holland Dutch, but a quarter of the family members are first-generation Surinamese, Moroccan, or Turkish. The first speech model we used dropped accuracy below 80% for non-standard accents. We swapped models and added a fallback path that asks the caller to repeat in shorter sentences if confidence is low. Not elegant. Effective.
The “hij is er niet meer” problem. The phrase “he is no longer here” in Dutch can mean the resident has passed away, has been moved to another house, or is out on a family visit. Our first intent classifier read it as bereavement and triggered a condolence response, which is the worst possible outcome when the resident is actually at the supermarket with his daughter. We now require a second turn of context before classifying death-related intents. The agent asks. This is fine.
Authorisation drift. Nedap Ons stores authorisation per contact (mag medische details horen, mag financiele details horen). The field changes over time as families argue. We were caching it for 24 hours, which is too long. We moved to a 5-minute cache with a write-through invalidation on the EPD webhook. Authorisation lookups now hit the EPD live for any sensitive disclosure.
Voicemail. Our agent answered every call. Some callers wanted voicemail. We added an explicit option in the opening prompt: press 9 to leave a message for the team without speaking to the assistant. About 8% of calls take it. They are mostly older spouses who do not want to talk to a machine, and that is a fair preference.
Night-shift handoff. The on-call rotation in Nedap Ons is a free-text field, not a structured one. We were parsing it. It broke twice in the first month when somebody typed the name with a comma. We now have a small admin UI that writes a structured on-call record, and the EPD field is kept in sync from there.
The complaints route. Wkkgz requires a documented complaint procedure. If a family member raises a complaint during a call, the agent must offer the formal route. We tried to handle this in-conversation. It did not work. People want to talk to a person about complaints. The agent now classifies the intent, opens a complaint case in the cooperative’s case system, and the manager zorg calls back the same working day. The agent’s job is to recognise the complaint and stop trying to resolve it.
What changed for the team
After six months in production, the cooperative’s metrics that we care about:
- Median family-call response time, measured from incoming call to substantive answer, dropped from 6h 41m (binder-driven) to 38 seconds.
- The senior nurses’ escalation queue holds 7 to 12 items per day, all of them genuinely clinical. Routine status calls do not reach them anymore.
- The Wzd register, which used to be reconstructed at the end of the week from sticky notes, now writes itself in real time.
The cooperative did not hire the two FTEs.
Staff response has been mixed and honest about it. The team coordinator said the binder was actually a useful artefact for shift handover and we had not realised. We added a daily printable summary as a result. The night shift likes the agent unambiguously. The day shift took about ten weeks to trust it.
What to do tomorrow if you run a care organisation
You do not start with the voice agent. You start with three things.
Map your call intents for two weeks. Print a tally sheet. Mark every incoming call with one of eight to twelve categories. You will know within ten working days which fraction is routine status, which is clinical, which is administrative. Without this, you will scope the wrong agent.
Get your EPD vendor on a call and ask them precisely which API version your installation is on and what the deprecation calendar looks like. If you are more than two minor versions behind, plan the adapter before you plan the agent.
Write down your retention policy by artefact class. Audio, transcripts, structured notes, metrics. The number is not the point. The fact that you decided is the point.
When we built the voice agent for the Gouda cooperative, the thing we ran into was that retention policy under Wkkgz does not arrive as a single number. It comes as four different numbers attached to four different artefacts, and the cleanest way to enforce them is at the table level, not the row level. The longer version of how we approach this kind of work lives on our AI agents page.
If the only thing you do today is the call-intent tally, you will already have made the decision easier. Print the sheet now.
Key takeaway
A voice agent in elderly care is a Dutch-law triage system that happens to be conversational. The hard parts are the EPD adapter and the retention policy, not the speech.
FAQ
Does the voice agent replace care staff?
No. It triages routine family calls and escalates clinical signals to a senior nurse within 60 seconds. The clinical decisions stay with people.
What if a family member refuses to speak to a machine?
The opening prompt offers a press-9 option to leave a voicemail for the team. Around 8% of callers take it, mostly older spouses.
How do you handle BOPZ versus Wzd terminology in an older EPD?
The Nedap installation still uses the legacy bopz_status field in storage. We alias it as wzdStatus in the adapter and document the alias in three places so nobody removes it.
Why delete audio at 90 days when Wkkgz allows longer?
Audio adds little forensic value beyond the transcript after a few months. Shorter retention shrinks the risk surface. Transcripts and structured notes stay for seven years.
Can you build this on top of any EPD, or only Nedap Ons?
The pattern fits any EPD with a callable API. The hard part is rarely the agent. It is the adapter between today's contract and whatever version the client is actually running.