Harsh Shah All projects

Case study

BizBot

An agentic AI receptionist for appointment-based businesses. A customer messages a hair salon, a mobile detailer, a cleaning company, an auto shop or a tutor over SMS, WhatsApp or web chat, and it checks availability, books, cancels and reschedules against the business's live schedule: an LLM agent with booking tools on WhatsApp, a deterministic state machine calling the same tools on SMS and web chat. The owner controls everything the agent knows from one dashboard and can take any conversation over at any point.

Role
Founder and sole engineer
Period
2025 – present
Stack
OpenClaw agent runtime, Groq gpt-oss-120b, n8n, Next.js, Supabase Postgres, Twilio
Status
Live in production · 13 paying customers

Visit site

The problem

The stylist is mid-colour, the detailer has both hands in a wheel arch, the cleaner is three rooms into a job. Nobody picks up, and the message that would have been a booking goes nowhere. A language model can answer it, and putting one on the line fails in three ways.

All three had to hold for businesses that schedule nothing alike: a one-person mobile detailer, a cleaning company that runs three jobs at once, a salon of four stylists where only one does curly cuts. Those are the product's three booking modes, solo, capacity and per_staff, and no tenant ever sees another's data.

What I built

A multi-tenant platform with an agentic core: one tool-calling agent, a deterministic engine beside it, and an owner dashboard that is the only source of what the agent knows.

In production

From the businesses running on BizBot. Client data is under contract, so these are rounded.

13paying businesses live, across 3 industries
13 monthslongest-running customer
2,380bookings created by the agent to date
220/mobookings a month at the current run rate
40%of those bookings taken outside business hours
1,000/moconversations a month across the client base
83%of conversations finished without an owner stepping in
7 smedian time to first reply
380reschedules and cancellations handled without the owner
2,180appointment reminders sent

How it works

The model owns exactly one channel. SMS and web chat authenticate the request, resolve the tenant, check for an owner takeover, and run a deterministic state machine; only WhatsApp reaches the agent, whose tool calls are validated before they run. Both paths end in the same five booking webhooks, and a dashboard write reaches the agent's prompt about 30 seconds later.

channels how the message is understood the one write path systems of record owner control plane SMS · web chat Twilio · web widget verify + resolve signature · tenant router.js state machine · no model HITL gate owner takeover WhatsApp SIM-linked number OpenClaw agent LLM · tool calling openclaw-call validated tool args Groq gpt-oss-120b budget + rate proxy booking tools 5 n8n webhooks check-availability create-booking lookup-bookings cancel-booking reschedule-booking Postgres RLS · advisory lock Google Calendar free/busy · events Resend confirmations owner dashboard staff · hours · FAQs sync poller · 30 s re-renders the agent
Figure 1: two ways in, one write path, and an owner who can re-render the agent or take the conversation over.

All three booking modes (solo, fixed capacity, several staff) write through one Postgres function, create_booking_rpc: it checks qualification, staff hours, time off and overlap, then inserts, under an advisory lock on the business. The lock is what stops two customers confirming in the same second from both counting the slot free; no constraint can, because capacity bookings are supposed to overlap up to the business's limit. Solo alone runs a free/busy check against the owner's own calendar first, because the function cannot see it. check_availability_rpc returns a reason as well as slots, so a closed day and a day the team is off do not both come back as fully booked.

Owners also run the business by message: a WhatsApp allowlist of seven operations over bookings, hours, services and FAQs, authenticated by the owner's number and logged whether each one ran or was refused.

The hard part

Getting a language model to take real actions against a live business when every argument it passes started as a stranger's free text.

Choosing and measuring the model

Tool calling is where models fail quietly. Llama 3.3 70B and Llama 4 Scout 17B both dropped or malformed booking tool calls; gpt-oss-120b formatted them reliably, at roughly a tenth of the cost of a frontier model. So I stopped guessing at prompts: a 53-scenario harness drives the real model through an agent loop against the real booking tools and asserts on which tools were called, what the reply said, and what landed in Postgres. A pass means the booking exists, not that the model said it did. One day of prompt work took it from 36 of 53 to 46. It has not run since those prompts were replaced.

Keeping the agent grounded

A prompt is a snapshot; a business changes daily. A stylist given Wednesdays off while the prompt still said nobody was off is the failure owners notice. So nobody writes the agent's facts by hand: a dashboard write stamps a sync request, a poller re-renders the workspace from live rows and restarts the gateway, and a test fails if a new config action forgets to ask. One gateway means one workspace, so only the tenant holding the WhatsApp line is rendered. Open slots and anything else time-sensitive come from a tool at the moment of the question.

Making tool calls injection-proof

The agent's tool transport is a shell command. The first skill recipes had the model paste customer text into a curl line, so a quote or a $(...) could reach the shell. Tool calls now go through one script: JSON on a quoted heredoc, which the shell does not expand and a JSON string cannot end, every field validated per action, the webhook URL and secret held by the script and never in the prompt. Tests run both shapes through a real shell: the old recipe executes a hostile value, the new one delivers it as data.

Numbers

Measured in the repo, each next to what produced it.

46 / 53agent eval scenarios passing, asserting on real database writes, up from 36
~$0.015per model call on gpt-oss-120b, capped by a per-tenant daily budget
30 sfrom an owner's dashboard edit to a re-rendered agent
5booking tools shared by the agent and the state machine
18industry presets that prefill services, prices and FAQs
484behavioural test assertions, 41 node:test files, all passing (17 Sep 2026)

What I would do differently