The problem
Inbound leads text in and expect an immediate, human-feeling reply, but sales reps can't answer every message within seconds or consistently capture the details (use case, timeline, budget, objections) that decide whether a lead is worth a call.
Leads that aren't triaged and enriched fast go cold, and the qualifying context never makes it into the CRM in a structured, reportable form.
The approach
A two-way SMS assistant runs each conversation through a deterministic finite-state machine, so replies are on-script and auditable while an LLM handles the messy natural-language understanding underneath.
Every inbound message is parsed by an LLM tool-calling step that classifies intent, detects objections, and extracts contact and qualification fields; the assistant advances state, sends the right templated response, and mirrors lead data and conversation state into the CRM on every change.
A second build productizes the single-tenant bot into a vertical-agnostic, multi-tenant SaaS: the same engine driven entirely by per-tenant configuration, with database-level tenant isolation so multiple clients run in one process with no cross-tenant leakage.
What I built
- 12-state conversation engine (greeting, qualifying, value-prop, scheduling, handoff, plus objection-handling, unresponsive, paused, and opt-out branches) with deterministic, testable transitions
- LLM extraction service (tool calling) that classifies intent, extracts name/company/industry/use-case/timeline/budget, and detects objection type and schedule-readiness
- RingCentral integration for two-way SMS with OAuth and signed-webhook verification, and a HubSpot sync that writes conversation state and qualification fields to custom contact properties automatically
- Concurrency-safe Postgres data layer (row-level locking for state transitions, idempotent event storage keyed on message ID) behind a BullMQ/Redis job queue with rate limiting and follow-up cadence
- Database-stored template system with cascading variable resolution that never sends a message containing an unresolved variable
- Productized multi-tenant successor: control-plane/data-plane split, Postgres row-level-security tenant isolation, a Zod-validated per-tenant config schema, and regression simulations proving behavior parity
- TypeScript
- pnpm/Turborepo
- Fastify
- OpenAI GPT-4o-mini (tool calling)
- RingCentral SMS API
- HubSpot CRM API
- PostgreSQL
- Prisma
- BullMQ
- Redis
- Zod
- Vitest
- Railway
Result
The original bot ships with a full automated safety net — 103 tests (89 unit, 14 integration covering the end-to-end pipeline, concurrency, idempotency, rate limiting, and follow-up cadence) run in CI on every push — and the follow-on build proves the same engine can be re-hosted as configuration-only, multi-tenant SaaS.
- 103 automated tests (89 unit + 14 integration) enforced in CI
- 12-state finite-state conversation machine with explicit objection/opt-out branches
- Multi-tenant productization gated on a two-tenant zero-cross-leakage proof