The situation

The client is a residential real estate business generating inbound from five distinct places: their own website and landing-page forms, Meta ads, Google ads, WhatsApp, and the phone, including a meaningful volume of missed calls that nobody was systematically calling back. Every one of those leads was being moved into the CRM by a human, by hand, typically in a batch at some point during the working day. Sales followed up when they had a gap. There was no defined first-response time because there was no mechanism that would have produced one.

The commercial damage showed up in two specific places. The first was after-hours: a customer who filled in a form at nine on a Saturday evening, a high-intent moment when someone is actively looking, frequently heard nothing until Monday, by which point they had usually enquired with two or three competitors and spoken to whoever answered first. The second was inconsistency. Two leads of identical quality could receive completely different treatment depending on who picked them up and how busy that week was, and nobody could see this happening because the data to see it did not exist.

What made this a good engagement to scope is that the client had already diagnosed it correctly. They were not asking for "AI." They were asking for every lead to be captured, qualified, and followed up automatically, with a human brought in at the point where a human actually adds something, and they said so in those terms on the first call.

Why off-the-shelf didn't fit

Two categories had been tried. The CRM's own native automation (they were on GoHighLevel) handled the parts of this that live inside the CRM perfectly well: once a contact record exists with the right tags, firing a sequence off those tags is a solved problem and we did not rebuild any of it. What it could not do was the part before that. Native forms captured web leads; WhatsApp and missed calls did not become CRM records without someone making them into records, and no amount of workflow builder inside the CRM fixes an input that never arrives.

The other was a set of no-code assemblies the client had built themselves across Zapier and a trial of Make. These worked in the demo sense, in that a form submission did produce a CRM contact, and failed in every way that matters in production. There was no retry when the CRM API returned a 429 during an ad burst, so leads were silently dropped. There was no deduplication, so the same person enquiring by form and then by WhatsApp created two records and, later, two follow-up sequences. And there was no alerting, so all of this was discovered weeks after the fact by a salesperson noticing a duplicate.

That last point set the tone for the engagement. The client's own framing on the call was that they wanted engineering that survives load rather than brittle no-code shortcuts, and we quoted against that explicitly: error handling, retries, structured logging and failure alerts on every workflow that touches a lead were line items in the scope document, not implied.

Scoping the real workflow

The scope call was mostly spent on two questions that turned out to carry the whole build. The first: what does "qualified" actually mean here, in terms a system can evaluate? The client's initial answer was A/B/C tiers, which is a label, not a definition. Pushing on it produced the real criteria: what the customer needs, how urgent it is, roughly what they can spend, whether the property is in an area the business actually serves, and whether the language reads as someone transacting soon or someone browsing. That last distinction is the one with commercial value and the one a human was previously making implicitly.

The second question: what exactly happens when a human gets involved? This is where most lead-automation projects quietly fail, and the client had felt it. Their worst experience with their own no-code version was a customer receiving an automated nurture message an hour after a salesperson had already spoken to them on the phone. Getting that right meant defining ownership precisely: at what moment does a lead stop being the system's and become a person's, what does the system stop doing, and what happens to messages already queued at that moment.

We came out with five deliverables: unified multi-channel capture with source attribution, an LLM qualification step producing structured output against the criteria above, automatic CRM profiling and tagging with deduplication, tiered follow-up that halts cleanly on reply or human takeover, and a daily report the client could read without asking anyone. Plus the reliability layer, scoped as its own workstream rather than assumed.

<90sec
Median first response, any hour
100%
After-hours leads answered, from zero
5 wks
Scope call to production

What we built

Capture normalizes all five sources into a single lead event before anything else happens. Web and landing-page forms and the two ad platforms post directly; WhatsApp arrives through the Business API; missed calls come in as a telephony webhook carrying the number and the time, which is enough to treat a missed call as a lead rather than as nothing. Every event carries its source, and source is recorded permanently rather than inferred later, because attribution that is reconstructed after the fact is attribution nobody trusts.

Qualification runs before any human or any CRM write. The model reads the enquiry (form fields, message text, or in the missed-call case simply the fact and timing of the call) and returns structured output: need, urgency, budget band, service-area match, and an intent classification with a short stated reason. Structured output, not prose, because everything downstream branches on these fields and a paragraph of natural language is not a routing decision. Where the enquiry carries too little to judge, the system says so explicitly rather than guessing a tier.

Deduplication sits between qualification and the CRM. Phone numbers are normalized to E.164 before comparison and emails are lowercased and stripped of plus-addressing, which sounds trivial and was responsible for a large share of the duplicates in the client's previous setup. A match updates the existing record and appends the new enquiry to its history; it does not create a second contact and does not start a second sequence.

From there the CRM record is created or updated with contact details, source, qualification score and tags, and GoHighLevel's own sequences take over per tier. A-tier gets an immediate response with a booking link on the channel the customer used, B-tier enters a 24-hour and 72-hour nurture, C-tier and anything gone quiet past the threshold drop to a monthly re-marketing list. High-priority leads simultaneously fire a Slack notification with the qualification summary, so the sales team learns about an A-tier lead by being told, not by refreshing a dashboard.

Handoff is a claim. When a salesperson opens a lead or the customer replies, the lead is marked owned, and every queued outbound message checks that ownership at send time. The automation freezes with full context handed over, meaning conversation history across channels and the qualification summary, and the appointment setter books against the shared calendar.

Underneath all of it: retries with backoff on every external call, a dead-letter queue for anything that exhausts them, structured logs keyed by lead, and alerts when a workflow fails or when capture volume on any channel drops to zero for longer than that channel's normal quiet period. Silence on a lead channel is indistinguishable from a broken integration unless something is watching for it.

Where it got hard

The freeze had a race condition, and it reached staging before we found it. The original implementation checked lead ownership when a follow-up was scheduled rather than when it was sent. That is fine until a salesperson picks up a lead in the gap between those two moments, which in a 24-hour nurture step is a gap of 24 hours. During testing a lead was claimed by a human eleven minutes before a queued message went out, and the message went out anyway, precisely the failure the client had described as their worst experience with their previous setup. We moved the ownership check to send time and made it a hard gate on the outbound path rather than a condition on the branch, so the last thing that happens before a message leaves is a fresh read of whether it still should.

The A-tier classifier over-fired on ad-sourced leads for the first week of shadow running. Ad copy trains people to write in urgent language, so paid-social leads arrived saying "need urgently" and "ready to move now" at a rate that had nothing to do with actual intent, while organic form leads describing a genuine timeline in flat language scored lower. We calibrated per source using the client's own historical outcomes, so urgency language is weighted differently depending on where the lead came from. This is only detectable if you shadow-run against real traffic before going live, which is why we do.

The quietest problem came from the client rather than from us. Our first version treated after-hours response as an unqualified good, the whole point of the project, and sent on every channel at any hour. The client's view, correctly, was that a 2am WhatsApp message is normal and a 2am SMS is intrusive enough to lose the deal you were trying to save. Quiet hours are now channel-specific, and A-tier leads arriving overnight get an immediate reply on messaging channels and a queued one on SMS at a civilised hour.

Rollout & results

The pipeline shadow-ran for six business days before go-live, capturing and qualifying every lead while humans continued to work them normally. That surfaced both the tier calibration problem and the freeze race. It went live in week five.

Median first response time is now under ninety seconds regardless of when a lead arrives, against a previous median that was only measurable during business hours and was several hours even then. After-hours leads went from receiving no response until the next working day to receiving one every time, the change the client cared most about. Manual re-keying is gone entirely; the duplicate-contact rate, which had been running at roughly one in nine, is effectively zero.

The daily report is what the client mentions most. New leads by source, A/B/C distribution, follow-up status, replies, appointments booked and lost leads, arriving every morning without anyone assembling it, changed how they buy ads. They can see which source produces A-tier rather than which source produces volume.

"The number I keep coming back to isn't the response time. It's that I can finally see which of my channels produce real buyers, because before this nobody was writing down where anything came from." Managing Director, client engagement

What we'd do differently

We would build the ownership claim as the first primitive in the system rather than as a condition inside the follow-up logic. Every lead-automation build has the same fundamental hazard, the system and a human acting on the same customer at the same time, and treating "who owns this lead right now" as a first-class piece of state, with every outbound path gated on it, is the design that prevents it. We arrived at that shape by fixing a bug rather than by starting there, and it is now how we start.

We would also have pushed harder on getting historical outcome data in week one. The per-source tier calibration needed it, and we did not ask until we could see the problem, which cost a couple of days we did not need to spend.

Losing leads to response time?

If your honest answer to "what happens to a lead that arrives at 11pm" is "someone gets to it," that's the whole problem. Tell us what your inbound actually looks like.

Request Your Agent