Chat agents
Chat agents for dental labs: triaging 1,180 weekly orders
It's 14:47 in a dental lab outside Eindhoven. A zirconia order from a dentist in Tilburg just landed in Outlook. Thirteen minutes until the Roland DWX-52DCi locks the batch.

It is 14:47 in a dental lab just off the A2, north of Eindhoven. The CAD-technicus on shift is finishing a zirconia bridge in Exocad. Thirteen minutes from now, the Roland DWX-52DCi five-axis mill will lock the afternoon batch. Whatever is staged at 15:00 gets cut. Whatever isn't, waits four hours.
An order lands in the shared inbox. Tandarts in Tilburg. Implant crown on position 36. Emergence profile sketched freehand on a scanned form. The SQL Server 2016 traceability system needs a lot number tied to the patient's implant brand, and that brand is not in the email. The clock is now at 14:48.
This is the moment the chat agent earns its keep.
The lab, the numbers, and the 15:00 lock
The lab is a 19-person fabrikant making crowns, bridges and custom abutments for about 340 active dentists across the Benelux. They run 1,180 orders through the queue in a normal week — heavier on Mondays, lighter on Fridays, with the occasional Tuesday spike that nobody has ever fully explained.
Production is split into two mill cycles per day. The first batch starts at 09:30, the second at 15:00. Once a batch is scheduled, the Roland's job list is locked and the puck carousel is loaded by hand. A missed 15:00 batch isn't a disaster, but it cascades: the implant goes out a day late, the dentist's surgery slot is at risk, and someone has to phone a clinic in Limburg to apologise.
Before the chat agent, the 14:00–15:00 hour was a quiet panic. One person triaging email, one person on the phone, one CAD-technicus trying to catch outliers before they hit the queue. Anything fuzzy — and there is a lot of fuzzy — went into a Word document called te_bespreken.docx and got revisited the next morning.
The 11-year-old Exocad library nobody wants to retire
When the lab was set up in 2014, they built an Exocad library for their three most common implant systems. They calibrated it against test cases. They wrote internal SOPs around it. They sent the validation paperwork to their notified body. That library is now 11 years old.
It has been patched, but never replaced. Every replacement attempt collides with the same wall: the validated cases were validated against this library. Throwing it out means re-running validation on hundreds of case templates, which means a quarter of CAD-technicus time spent re-validating instead of milling.
So the library stays. The chat agent has to deal with it as-is. Specifically, the agent has to read the library's case-template names, which use a naming convention that predates the team's current head of CAD by two CAD heads:
SY-{system}-{position}-{abutment-rev}-{em-class}.dmeThe em-class is the emergence-profile class — A, B, C or X. X means "non-standard, requires a technician's eye". The agent's first job is to read the incoming order, infer the em-class, and decide whether the order is routable to the mill or needs to land in the CAD-technicus queue.
The chat agent's actual job
The chat agent is not a customer-service bot. Dentists do not chat with it. The agent reads orders out of three channels — email, the lab's web portal, and an old fax-to-PDF pipeline that still receives twelve orders a week — and decides what to do with them.
The decision tree is dull, which is the point:
def route(order):
if not order.implant_brand or order.implant_brand == "unknown":
return queue("disambiguate_brand", sla_minutes=10)
em_class = classify_emergence(order.scan, order.notes)
if em_class == "X":
return queue("cad_technicus", sla_minutes=45)
library_match = exocad_lookup(
system=order.implant_brand,
position=order.tooth_position,
em_class=em_class,
)
if not library_match:
return queue("cad_technicus", sla_minutes=45)
lot = mssql.fetch_lot(order.implant_brand, order.batch_hint)
if not lot:
return queue("traceability", sla_minutes=15)
return schedule_for_mill(library_match, lot, before="15:00")
About 80% of orders fall through to schedule_for_mill on the first pass. About 12% park in disambiguate_brand or traceability for a few minutes, usually long enough for a human to look at a scan and type a single answer. About 8% land in the cad_technicus queue, which is exactly where they belong — these are the orders where the lab needs a person to look at the geometry before the mill starts cutting.
The numbers matter because they shape the human side. The CAD-technicus shift now plans around an 8% inflow, not around the constant low-grade interruption of "is this one okay?"
Reading lot numbers out of SQL Server 2016
The implant traceability database is a homegrown application running on SQL Server 2016, which has now been out of mainstream support for several years. It is older than the company's current laptops. It does not have a REST API and it never will, because exposing it as a service would require re-validating the application against the lab's quality system.
The chat agent reads from it directly, in read-only mode, against a replica that lags the live database by about 30 seconds. The query is, deliberately, boring:
SELECT TOP 1
lot_number,
expiry_date,
sterilisation_cycle
FROM implant_lots
WHERE brand_code = @brand_code
AND status = 'AVAILABLE'
AND expiry_date > DATEADD(day, 30, GETDATE())
ORDER BY received_date ASC;
FIFO on receipt date, with a 30-day expiry buffer. The buffer used to be 7 days. It became 30 after a batch of abutments was milled on a lot that expired three weeks later in transit to a clinic in Luxembourg, which forced an emergency replacement on the lab's nickel.
The agent does not write to the database. Lot allocation — the actual reservation — happens through the same internal application the lab has used since 2017, triggered by a small service the agent calls. We did not want the agent committing inventory changes. If the agent is wrong, we want the wrong-ness to surface in the queue, not in the lot ledger.
If your traceability system sits inside a regulated quality system, do not let the agent write to it. Read-only access plus a thin commit service keeps your validation paperwork intact and your auditor calm.
The emergence-profile edge case
An emergence profile is the shape of the crown at the gum line. Get it right and the soft tissue heals around the restoration cleanly. Get it wrong and the patient is back in the chair within a year with inflamed gums and a CAD-technicus filling in a complaint form.
For most cases, the profile is implicit in the library — pick the right system, the right tooth position, and the right abutment revision, and the geometry comes out within tolerance. For about one in twelve cases, the dentist has either drawn a custom profile on the scan or written a note like "iets meer concaaf onder mesiaal" ("a bit more concave on the mesial side"). Those are the orders that have to land in front of a person.
The agent classifies emergence in two stages. First, it looks at the structured fields the portal collects — tooth position, soft-tissue depth, gingival biotype if provided. If those are within standard parameters, it provisionally assigns class A, B or C. Then it scans the freeform notes and any attached drawings for keywords and shapes that suggest a custom profile. Any signal of customisation pushes the class to X.
X is a one-way street. We never down-classify out of X. If the model is uncertain, the order goes to the human, full stop. Over a six-month window the X-rate has stabilised around 8.4%. The CAD-technicus team prefers a slightly noisy queue to a quiet queue that occasionally hands them a problem at 16:30.
14:47, one minute later
Back to the Tilburg order. At 14:48 the agent has:
- Inferred the implant brand from the scan's metadata header (the dentist's intraoral scanner stamps it).
- Cross-referenced the brand against the SQL Server lot table and found two viable lots, both with expiry well past the 30-day buffer.
- Read the freeform note, spotted "vrije emergentie naar buccaal" ("free emergence buccally"), and pushed the case to em-class X.
The order is in the cad_technicus queue with a stitched-together context card: scan, note, two candidate lots, the matching Exocad case template, and the dentist's history (two previous custom cases, both healed cleanly). The technician reads it in roughly 90 seconds, approves the geometry with a minor tweak, and the order is on the 15:00 mill at 14:54.
This is the boring success case. There are days when the agent flags six orders in the last hour and one of them has to wait for the 19:00 batch. That is fine. The point of the agent was never to push 100% through the 15:00 lock. The point was to spend the 14:00–15:00 hour deciding which orders actually deserve it.
What we learned building this
A few things shook out of this project that we would write on the wall of any chat-agent build.
The agent's value is not in the answers it gives. It is in the time it gives back to the humans who would otherwise be triaging. The lab's CAD-technicus team used to lose about 90 minutes a day to triage interruptions. They now lose about 20. That recovered time is the ROI; the agent itself is the delivery mechanism.
Legacy systems are not the enemy. The Exocad library and the SQL Server 2016 application are both older than the agent by a decade, and both are fine. We wrote thin adapters around them and treated them as the source of truth. Anyone telling you that you need to modernise your stack before you can put an agent on top of it is selling you a modernisation project, not an agent.
Confidence thresholds are a product decision, not a model decision. The 8.4% X-rate was chosen by the head of CAD, not by us. He looked at the queue at four different thresholds and picked the one where his team felt informed rather than overwhelmed. We tune the model around his choice, not the other way around.
When we built this chat agent for the Eindhoven lab, the thing we kept running into was that every "edge case" eventually had a fingerprint in either the Exocad library or the SQL Server schema. The win was in reading the legacy stack carefully, not in replacing it. If you're looking at a similar shape of problem — a small operations team, a regulated workflow, two pieces of software that are older than your last hire — that is the kind of AI agents work we like.
If you want to know whether an agent like this would survive contact with your operations, do this today: open the last 100 orders, emails or tickets that came through your inbox, and tag each one with the queue it should have landed in. If you can name fewer than five queues, you already have the routing table. The model is the easy part.
Key takeaway
In a regulated workflow, a chat agent's job is not to answer — it is to route the right order to the right human before the next batch locks.
FAQ
Why didn't you replace the 11-year-old Exocad library?
It carries the lab's validated case templates. Replacing it means re-running validation against a notified body, which costs more CAD-technician time than the library itself.
Why read SQL Server 2016 directly instead of putting an API in front of it?
Exposing the traceability application as a service would re-open its validation under the lab's quality system. A read-only replica plus a thin commit service keeps the paper trail intact.
What happens if the chat agent misclassifies an emergence profile?
It only ever misclassifies toward X, the human-review queue. Any ambiguous signal pushes the order to a CAD-technicus rather than to the mill. We never down-classify out of X.
How long did the build take?
Roughly nine weeks from kickoff to production cutover, with another six weeks of supervised running before the lab let the agent route orders without a human double-check on every case.
What does the agent do with orders that miss the 15:00 mill lock?
They roll to the 19:00 batch with a flag explaining why they didn't make 15:00. The lab planner sees the reason in the queue card, not buried in an email thread.