Case study
BizBot
An agentic AI receptionist for appointment-based businesses. A customer messages a salon, a dental clinic, a yoga studio, a vet, an auto shop or a tutor over SMS, WhatsApp or web chat, and an LLM agent with booking tools checks availability, books, cancels and reschedules against the business's live schedule. The owner controls everything the agent knows from one dashboard and can take any conversation over from the AI 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
The problem
Every appointment business has the same bottleneck: the front desk. The stylist is mid-cut, the hygienist is mid-cleaning, the instructor is mid-class, and the message that would have been a booking goes unanswered. Putting a language model on the line is the obvious fix, and it fails in three predictable ways.
- Grounding. An agent is only as right as its context. If the owner gives a stylist Wednesdays off and the agent still offers Wednesday, the AI is confidently wrong on the business's behalf.
- Safe actions. A booking agent does not just chat, it writes to a calendar and a database, with arguments taken from free text a stranger typed.
- Cost and control. Every message costs tokens, some customers try to talk the model into things, and the owner needs to be able to step in.
BizBot had to solve all three for many businesses at once, from a one-chair barber to a multi-staff clinic, without one tenant ever seeing another's data.
What I built
A multi-tenant platform with an agentic core: a tool-calling LLM agent, a deterministic conversation engine beside it, a shared set of booking tools, and an owner dashboard that is the single source of truth for everything the AI says.
- A hybrid agent architecture. WhatsApp conversations run through an OpenClaw agent on Groq's gpt-oss-120b, which reasons over a per-tenant system prompt and invokes skills for availability, booking, lookup, cancellation and rescheduling. SMS and web chat run through a deterministic state machine with natural-language intent, service and date parsing. The model decides what to do; neither path owns the business rules.
- Tools, not prompts, enforce the rules. Both paths call the same five booking tools, n8n workflows backed by Postgres functions. Minimum notice, maximum advance, cancellation windows, staff qualifications and double-booking protection live there, so an LLM cannot be talked past them and the two paths cannot drift into two sets of rules.
- Owner-controlled knowledge, live-synced to the agent. From the dashboard the owner manages services, prices and durations, staff and who can perform what, staff hours and time off, business hours, booking policies and FAQs. Every one of those writes triggers a re-render of the agent's workspace (system prompt, tool recipe and skill files) from templates plus live Postgres data within about 30 seconds, and the dashboard shows whether the agent is in sync. Static knowledge rides in the prompt; live availability always comes from a tool call, so the agent never quotes a slot from memory.
- Guardrails around every action. Tool arguments go through a validator before they reach a webhook, secrets and URLs never appear in the prompt, a response contract forbids the model from exposing IDs or raw JSON, a proxy enforces per-tenant rate limits and a daily spend budget, and the owner can pause the AI mid-conversation and hand it back.
- Tenant isolation at the database. Row-level security on every tenant table for the dashboard; the workflows route across tenants on purpose, so resolving the tenant from the number a customer messaged is treated as the security boundary it is.
In production
From the businesses running on BizBot. Client data is under contract, so these are rounded.
How it works
Three planes. The deterministic path verifies the message signature, resolves the tenant, checks whether the owner has taken over, and lets the state machine choose a tool. The agentic path hands the message to the OpenClaw agent, whose calls to the model are metered per tenant and whose tool calls are validated before they run. The owner control plane keeps the agent's context current. Every real action lands in the same five tools.
Bookings split by how a business schedules. A solo practitioner's booking checks Google Calendar free/busy and writes the event. A business with capacity or several staff books through one Postgres function that checks qualification, staff hours, time off and overlap, then upserts the customer and inserts the booking in a single transaction, with an exclusion constraint that turns two customers racing for the same slot into a clean "slot unavailable" rather than a double booking. Availability answers carry a reason as well as slots, so the agent can say the business is closed or the only qualified stylist is off, instead of calling a closed shop fully booked.
Owners can also run the business by message: a WhatsApp owner mode accepts a strict allowlist of seven operations (confirm, complete, reschedule or cancel a booking, add an FAQ, toggle hours, toggle a service), authenticated by the owner's number and audited whether allowed or refused.
The hard part
Getting a language model to take real actions against a live business, reliably and safely, 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 and about $0.001 a call. To iterate on prompts without guessing, I built a 53-scenario eval harness that drives the real model through an agent loop against the real booking tools and asserts on three things: which tools were called, what the reply said, and what actually landed in Postgres. A pass means the booking exists, not that the model said it did. Scenarios cover happy paths, ambiguous dates, time formats, returning customers, partial information, out-of-scope requests and eleven adversarial cases. Prompt iteration took the suite from 36 of 53 on the first full run to 46 of 53 in a day.
Keeping the agent grounded
A prompt is a snapshot, and a business changes daily. A staff member given Wednesdays off while the prompt still said nobody was off is exactly the failure owners notice. The fix was architectural: every dashboard write stamps a sync request, a poller re-renders the workspace from live data and restarts the agent, and a test fails if a new config write forgets to ask. Anything time-sensitive, like open slots, is fetched by a tool at the moment of the question.
Making tool calls injection-proof
The agent's tool transport is a shell command, and the first skill recipes had the
model build a request line with the customer's words inside it, so a quote or a
$(...) in a message could reach the shell. Tool calls now go through a
single script that receives the JSON on a quoted heredoc, where the shell expands
nothing and a JSON string cannot end the block, validates every field per action, and
holds the webhook URL and secret itself. Tests push hostile values through a real
shell: the old recipe executes them, the new one delivers them as data.
Numbers
Measured in the repo, each next to what produced it.
What I would do differently
- Give the agent context through tools, not a re-rendered prompt.
Rendering tenant knowledge into the workspace and restarting the agent keeps it
fresh within 30 seconds, but it ties freshness to a restart and one workspace to one
tenant. A
get_business_contexttool, or context injected per request, would make the agent stateless across tenants and every edit instant. - Make evals a merge gate from day one. The harness proved its value in a single day of prompt work, then stopped running when the prompts moved to per-tenant templates. An eval that asserts on database state belongs in CI next to the unit tests, run on every prompt, skill or model change, with the score tracked over time.
- Use native, schema-typed function calling end to end. A shell command as the tool transport needed a validator script to be safe. Typed tool schemas validated before execution remove that class of problem instead of defending against it.
- One conversation log for every channel. Agent turns live in the runtime's session memory, so the owner's inbox, takeover and analytics cover SMS and web chat but not the agent's WhatsApp conversations. The agent should write to the same log as everything else, which is also the dataset the evals should be built from.