Automated NPS & Review-Routing System
A reputation-management system for a healthcare clinic that surveys patients at a defined visit milestone, parses free-text replies into a satisfaction score, and routes by sentiment — inviting promoters to leave a public review while directing anything less positive to a private follow-up with a human. Built across a polling and dispatch engine and a CRM messaging layer, with cursor-based incremental sync, retry-safe queuing, and authenticated webhooks. A live mis-routing incident was traced to two compounding faults across both systems and corrected in each.
I built and hardened the polling, dispatch and parsing engine, and set up the CRM-side review messages from the agency's template. When they misrouted a reply, I diagnosed the cause and specified the fixes.
The problem
Public reviews are the primary trust signal for a local clinic, but asking indiscriminately risks surfacing unhappy patients publicly. The clinic needed to ask at the right moment and route only promoters to the public link.
What was built
Two independent scheduled pipelines in the automation layer — a 15-minute intake that polls attendees, counts sessions, validates, and queues; and a 1-minute dispatch that sends anything due — feeding a CRM messaging layer that sends the survey, receives the reply, posts it to a parser, and branches on the resulting score.
The hard part
A patient replied "11" — meaning eleven out of ten, an enthusiastic promoter — and received the detractor apology. The worst possible outcome.
Two compounding faults, in two different systems:
- The parser. Its hyperbole guard handled three-or-more-digit numbers and explicit "N out of 10" forms, but a bare two-digit number matched none of the number patterns, so the score stayed null and nothing was written.
- The CRM. With the score field unset, the detractor condition was
<= 5— and an empty field still satisfies<= 5. So any reply the parser couldn't understand routed to the apology.
The parser bug made it silent. The CRM condition made it harmful. Neither was obviously wrong on its own.
The fixes had to land in both places: in the parser, match the first integer and clamp anything above ten down to ten; in the CRM, change the detractor condition to >= 1 AND <= 5 so an empty field falls through to a re-ask branch instead.
Reviewing around the incident surfaced more — a score field that was never cleared between cycles, so a returning patient could branch on a months-old score; a fixed ten-second wait between writing the score and reading it back, which is a race; and a completely unauthenticated parser webhook that would have let anyone POST an arbitrary score against any contact ID. That was closed with a shared-secret header that fails closed.
What can be verified
- The system runs in production and routes replies by sentiment
- a live mis-routing incident was diagnosed to root cause across two systems and fixed in the parser
- a security hole was found and closed.
The workflows
(select to enlarge)
(select to enlarge)
(select to enlarge)
(select to enlarge)
On numbers: every figure above is an artefact count or a measured technical value. No business-outcome metric, whether time saved, revenue or conversion, was captured on these engagements, so none is claimed.
On status: reflects repository evidence and platform backups, not a live systems check.