Email automation
Email automation in funeral care: 980 weekly replies handled
Tuesday morning in Almere. 187 unread emails, 41 with a Dela policy number, and a fifteen-year-old dossier system nobody wants to migrate. Here is what we built.

It is Tuesday morning in Almere. The intake coordinator at a 31-person uitvaartzorg group opens the shared inbox and sees 187 unread emails. Forty-one of them mention a Dela policy number. Nineteen mention Monuta. Six are from a notary asking for the same death certificate that was attached to a thread last Friday. The rest are families asking when they can come in to collect personal belongings, whether the rouwkaart proofs are ready, and whether grandma's pension will keep paying out for another month.
That inbox is the one we were asked to fix.
The dossier system nobody wanted to touch
The group runs on a custom ASP.NET 4.0 dossier application that was written in 2011 by a developer who has since emigrated to Australia. It works. Bereavement coordinators use it every day. Every uitvaart gets a dossier number, every line item ties to that number, and every invoice (state, insurer, family) reconciles against the same code.
Replacing it would mean rebuilding fifteen years of bookkeeping logic on top of a stack the founder's brother-in-law could not approve. So we did not replace it. We did the cheaper thing. We built around it.
The constraint to acknowledge is the runtime. .NET Framework 4.0 itself stopped receiving updates in January 2016 per Microsoft's lifecycle policy. The application still runs on Windows Server 2012 R2 in a Hyper-V VM. It exposes nothing useful to the outside world. No REST endpoint, no webhook, no message bus. The only stable interfaces are the SQL Server 2014 database and an SMTP relay that already sends order confirmations from the system's own service account.
That was enough.
What 980 emails a week actually look like
Before we touched anything, we asked for ten weeks of inbox history exported to .eml and tagged by the two coordinators who handle intake. The headline number on the brief said "around a thousand a week". The real distribution looked like this:
- 41 percent were status questions ("when can we come in", "is the kaart approved", "did the notary file land")
- 24 percent were insurer claims with a Dela or Monuta policy number attached
- 18 percent were document requests (akte van overlijden, uittreksel, factuur, condoleanceregister)
- 11 percent were sympathy messages from family friends to the deceased, accidentally sent to the company inbox
- 6 percent were everything else: vendor invoices, NAW corrections, an occasional press inquiry
That breakdown set the design. You do not build one agent to answer all of those. You build a classifier that picks the right path, and you make sure each path is dull and predictable.
Two queues and one rule
We landed on two queues. A coordinator queue, where a human always replies before the family gets anything substantive. An autoresponder queue, where the agent acknowledges and timestamps without a coordinator in the loop.
The rule that decides between them is short enough to recite from memory. If the email mentions a death certificate, a policy number, a payout, a notary, a will, an executeur-testamentair, or anything that the AVG would call a special-category-related decision, it goes to the coordinator queue. Everything else can be acknowledged by the agent.
We wrote it that way on purpose. The Dutch supervisor for personal data has been explicit that automated decisions involving special-category data need human review. A condolence acknowledgment is not a decision. A claims handoff is, even when the agent is just "routing it". We did not want to argue that line later.
If an automated reply contains anything that could be read as a status update on a claim, a payout, or a legal matter, it is no longer just an acknowledgment. Treat that boundary as load-bearing.
The classifier
The classifier is a small prompt against an LLM, with a deterministic post-check. The model returns a JSON object with two fields: category and confidence. If confidence is below 0.85, or the predicted category is anything other than the sympathy acknowledgment path, the email goes to the coordinator queue with the predicted category as a hint, not a decision.
CATEGORIES = [
"status_question",
"insurer_claim_dela",
"insurer_claim_monuta",
"insurer_claim_other",
"document_request",
"sympathy_message",
"vendor_or_admin",
"unclear",
]
def route(email):
pred = classify(email) # returns {category, confidence}
has_policy = POLICY_RE.search(email.body) is not None
if has_policy or pred["category"].startswith("insurer_claim_"):
return Queue.COORDINATOR, "policy_number_detected"
if pred["confidence"] < 0.85:
return Queue.COORDINATOR, "low_confidence"
if pred["category"] == "sympathy_message":
return Queue.AUTORESPONDER, "sympathy_message"
return Queue.COORDINATOR, pred["category"]
The policy-number regex is the boring half and it does most of the work. Dela polisnummers follow a known prefix range, Monuta uses a different one, and both are long enough that a false positive against a random nine-digit string is rare. We log every match with the matched substring so a coordinator can verify a flagged email in under three seconds.
Writing the condolence acknowledgment
The acknowledgment is one paragraph. We rewrote it eleven times before it shipped. The first version sounded like a chatbot. The third sounded like a wedding planner. The version that went live reads like the owner of a small uitvaartonderneming wrote it on a quiet morning, because in the end the owner did. We gave him the draft and he edited it down with a red pen.
The acknowledgment never includes the deceased's name. It never references "your loss" by relationship. It does not promise a callback within a specific window. It says: your message has arrived, a coordinator has been notified, here is the dossier reference if you already have one, and here is the direct phone line if today is too long to wait.
It is sent by the existing SMTP relay from the dossier system, against the same service account that already sends invoice mails. That means SPF, DKIM and DMARC were already correct and the messages land in the inbox the family expects, not in spam. If a legacy system already has working email auth, run the agent's outbound through it. Setting up a parallel SMTP path is how acknowledgment messages end up in junk for half the recipients.
How we hooked into the dossier system without touching it
We did not write a single line of new code inside the ASP.NET application. The integration is a read-only SQL view, a write-only table, and a Windows service.
The view exposes the seven fields the agent needs: dossier number, status code, primary contact email, primary contact name, insurer code, policy number, last-event timestamp. The agent can look up an existing dossier by email address or policy number, and that is all it can read.
The write-only table is called inbox_event. The agent appends a row whenever it routes a message: dossier id (nullable), email message-id, category, confidence, queue, agent reply id (nullable), timestamp. The dossier application has a small extension page, written by us in plain Razor against the same framework, that joins inbox_event against the existing dossier records and shows the coordinator what arrived since they last opened the file.
The Windows service polls the IMAP inbox every 90 seconds, runs the classifier, decides the queue, writes the row, and (if applicable) hands off to the SMTP relay. It is two hundred lines of C# that have not been touched since week three.
What we got wrong on the first pass
Three things, all worth naming.
First, the agent originally tried to acknowledge sympathy messages from third parties (the 11 percent bucket). That broke on the second day, because some of those messages were from regional press asking for a quote, and a templated condolence reply to a journalist looks awful. We narrowed the autoresponder path to messages where the sender domain was a freemail provider and the body length was under 600 characters. The category exists, but the trigger is stricter.
Second, we underweighted forwarded emails. A coordinator forwarding an old thread to herself to "park it for later" was being classified as fresh intake. We added a check on References and In-Reply-To headers, and dropped any message that already had the dossier reference in the subject line.
Third, the first prompt asked the model to detect emotional tone. The classifier got worse, not better. Tone is not what decides the queue. The presence of a policy number, a notary, or a document request is what decides the queue. We pulled the tone field out and accuracy went up.
What the numbers say after eight weeks
The intake coordinators kept logging time-on-inbox before and after, in their own spreadsheet, because we asked them to. The week before the agent went live, the two of them together spent 38.5 hours on triage alone, before any actual work. Eight weeks in, that number is 11.2 hours, all of it on the coordinator queue, because everything in the autoresponder queue is already acknowledged by the time they open it.
The agent handles roughly 980 messages per week. About 62 percent land in the coordinator queue with a category hint, 31 percent are acknowledged and parked, and 7 percent are flagged as unclear and read by a human within an hour. No condolence message has been sent to the wrong family. No insurer claim has been auto-acknowledged. One press inquiry got an acknowledgment in week two and a coordinator caught it within twenty minutes, which is the kind of failure mode we wanted: visible, fast, fixable.
The dossier system has not been touched.
What this is not
This is not a story about replacing legacy software with AI. It is the opposite. The dossier system stays. The coordinators stay. The agent does the part of the work that does not need them, and it does it inside the rails the existing system already provides.
If you are sitting on a fifteen-year-old line-of-business application that everyone is afraid to migrate, the first question to ask is not how to replace it. It is where the inbox lives, and what would happen if the boring 30 percent of it answered itself by the time the team logs in.
When we built the email agent for the Almere uitvaartzorg group, the thing we ran into was the AVG line on automated decisions involving special-category data. We solved it by making the autoresponder small enough that it never makes one.
Five-minute audit, if you want one: open your shared inbox, export the last full week to .eml or CSV, and tag every message by what it is asking for. If a quarter of the volume is the same three questions answered the same three ways, you already know where to start.
Key takeaway
Build the agent around the legacy system, not over it. The dossier stays, the coordinators stay, and the agent does the boring 30 percent that never needed them.
FAQ
Why not replace the ASP.NET 4.0 dossier system outright?
Fifteen years of bookkeeping logic, custom invoicing, and coordinator habits sit on top of it. A rebuild is a multi-quarter project. The email agent ships in three weeks and frees the team to plan a migration on their own timeline.
What stops the agent from auto-replying to a claim?
Two checks. A regex for Dela and Monuta policy-number formats, and an LLM classifier whose confidence threshold for the autoresponder path is 0.85. Anything insurer-shaped goes to the coordinator queue with a category hint.
Where does the agent send outbound mail from?
Through the dossier system's existing SMTP relay, on the same service account that already sends invoices. SPF, DKIM and DMARC were already correct, so acknowledgments land in the inbox rather than the spam folder.
How is special-category data handled under the AVG?
The autoresponder never makes a decision about a claim, payout, or legal matter. It only confirms receipt. Anything that touches a policy number, notary, or document request is routed to a human before any reply goes out.