Skip to content
All work

Case study

Lettings Automation Platform

End-to-end automation turning inbound property enquiries — email and phone — into confirmed CRM bookings, with humans at two control points.

Role
Built the entire system end to end
Domain
Automation & Agents · Backend & APIs · Cloud & Production
Stack
Python · Flask · Playwright · n8n · Retell AI
Result
~25/day enquiries automated
Animation: inbound email and phone enquiries are parsed, matched to properties, and booked into a CRM by browser automation, with two human approval gates.

Overview

Inbound viewing enquiries — email and phone — become confirmed CRM bookings with staff involved at only two control points. Three layers: workflow orchestration, voice agents, and a browser-automation service driving a CRM with no public API.

The problem

Every enquiry was handled by hand: reply, ask availability, interpret it, check the calendar, book, confirm on the day. High volume, low judgement, dependent on someone being at a desk. The obvious fix — integrate with the CRM — was unavailable: it has no API.

One enquiry, end to end

Everything runs unattended except the two marked steps. Those were chosen as the moments that are irreversible, not the ones that are tedious.

  1. Enquiry arrives

    By email or phone. Both channels log to the same store and converge from here.

  2. Ask for availability

    Replies are classified as availability, a question, or silence — each routed differently.

  3. Answer what the system cannotHuman

    A question outside what the system can answer goes to staff rather than being guessed at.

  4. Parse and check

    Free-text availability becomes structured slots, checked against the calendar. Silence triggers a timed follow-up.

  5. Approve the bookingHuman

    A person signs off before anything is committed to a customer.

  6. Book in the CRM

    The service accepts the job, returns a run id at once, and drives the CRM in the background: find or create the applicant, book the slot, confirm, email.

  7. Confirm on the day

    An outbound call or message confirms attendance.

Three layers

Separated because they fail differently. Orchestration changes often, voice is vendor-configured, and browser automation needs real error handling — so a CRM change never touches conversation logic.

Orchestration
28 workflows: intake, reply classification, escalation timers, approval gates, outbound confirmation.
Voice agents
Five conversation-flow agents, inbound and outbound, with transfer, silence handling and post-call analysis.
Browser automation
A service that operates the CRM's web interface the way a person would, reporting each outcome back.
Property matcher
Resolves an enquiry to a real listing — and may only answer with a listing it was given.

My Role

Role

Built the entire system end to end

Contribution

  • Built 28 n8n workflows orchestrating email parsing, LLM-based availability extraction and human-approval gates
  • Configured five voice agents for inbound and outbound calls
  • Built a Flask + Playwright service that drives an API-less CRM headlessly to create applicants and book viewings
  • Designed an async webhook-to-callback contract (202 + run_id) decoupling slow browser work from the caller
  • Made the RPA survive a constantly drifting DOM with layered XPath fallbacks, per-row retries and JS-click recovery
  • Added a JSON-mode property matcher with a verbatim-only hallucination guard and deterministic fallback
  • Built a secret-scanning-gated, self-hosted CI/CD pipeline

Team Context

Sole builder across all three layers.

A CRM that changes under you

Most of the engineering went into one fact: the only integration available is the least stable one. The goal was never immunity — it was failing into a reported error instead of a wrong booking.

Failure modeThe interface drifts — rows shift, elements move — and automation breaks quietly weeks later.
ResponseLayered selector fallbacks, text-based rather than index-based pagination, per-row retry budgets, a scripted click when the normal one is intercepted.
Failure modeA booking takes far longer than any caller will wait, and a timeout mid-booking leaves the CRM in an unknown state.
ResponseAccept the job, return a run id immediately, post the outcome back. Failures arrive as a status, never as a timeout.
Failure modeTransient network and provider failures surfaced as outright booking failures.
ResponseRetries with backoff on retryable codes, and in-flight runs exposed on a health endpoint so a stuck job is visible rather than inferred.
Failure modeA language model can invent a property that does not exist — and book a real customer into it.
ResponseThe matcher accepts only verbatim candidates, downgrades an exact match that does not resolve to one listing, and falls back to deterministic matching when the model is unavailable.

Decisions

Drive the CRM through the browser rather than wait for an API.

No API existed and none was coming. Browser automation is the least elegant option and the only one that finished the job — the alternative automates six steps of seven and hands a person the last, which removes most of the value.

  • Instead of
  • Wait for a vendor API
  • Staff key in bookings
  • Replace the CRM

Two human control points — not zero, not five.

Full automation asks the agency to trust the system with customer commitments before it has earned that; reviewing everything reproduces the original workload. Gating the two irreversible moments keeps the throughput and leaves judgement with people.

  • Instead of
  • Fully automatic
  • Staff review every step

Results

~25/day

enquiries automated

28

orchestration workflows

5

voice agents

What I Learned

  • The unglamorous integration is sometimes the only one. Browser automation is fragile and was still right, because the elegant alternative did not exist.
  • Design for the failure you will actually get. The CRM was always going to change; what mattered was whether that produced a reported failure or a wrong booking.
  • Put people where decisions are irreversible, not where work is boring.

Limits

No accuracy, latency or success-rate metric was ever recorded, so none is claimed. Nothing measures how often availability parsing was right, how often a booking needed correction, or how often the automation failed. The published counts say how much ran, not how well. Behaviour was verified in a staged beta with staff reviewing every booking; a test suite over the booking flow existed in development and is no longer in the tree, so coverage is unverified.

What I'd Improve

  • Record success rates. Without them the system cannot be improved deliberately. First fix.
  • Make booking idempotent with a request key, so a retry after an ambiguous failure cannot double-book.
  • Run the booking path against a test record on a schedule, so interface drift is caught before a real enquiry hits it.
  • Restore the automated tests around booking orchestration — the part with irreversible consequences.