# We Built an AI Booking Bot. Most of the Code Is Guardrails.

*Published 2026-04-29* | Author: titus-capilnean

<p>A few weeks ago, someone asked the McDonald's customer support chatbot to reverse a linked list. It did — with working code. Then it tried to upsell them on McNuggets.</p>

<p>This went viral for the obvious reason: it's funny. A fast food ordering bot moonlighting as a LeetCode tutor is peak absurdity. But underneath the joke is a real problem that most companies shipping AI chatbots haven't solved.</p>

<p>The McDonald's bot had no concept of what it shouldn't do. It was a capable language model pointed at a customer service interface with, apparently, zero constraints on scope. Ask it to order a Big Mac? Sure. Ask it to write a sorting algorithm? Also sure. Ask it about your competitor's menu? Probably that too.</p>

<p>This is the default outcome when you deploy an AI agent without guardrails. The model is helpful. Relentlessly, indiscriminately helpful. And that's fine for a general assistant — but it's a liability when the agent represents your business, touches your calendar, or sends emails on your behalf.</p>

<h2>We built one. Here's what we learned.</h2>

<p>We recently shipped an AI booking bot on civic.com. It does two things: helps visitors book a 45-minute intro call with our team, and lets them send us a note if they'd rather not get on a call. That's it.</p>

<p>The AI part — having a model understand "I'll take the 2pm Tuesday slot with Titus" and turn that into a calendar invite — was straightforward. A system prompt, a few tools, an afternoon of work.</p>

<p>The rest of the engineering was making sure the bot stays in that lane. Reliably. Under adversarial conditions. At scale. That took a week, and it's where all the interesting decisions live.</p>

<h2>The happy path is simple</h2>

<p>From a visitor's perspective, the bot is fast and uncomplicated. You click "Book a call," see a short greeting with five available time slots, each showing the team member's name and role. Tap a slot, answer four quick questions — name, email, company, what you're working on — and you get a calendar invite. The whole flow takes under a minute.</p>

<p>If you'd rather not book a call, you can send a note instead. Same quick collection of details, and the message goes straight to our team's inbox.</p>

<p>Simple on the surface. But every interaction passes through multiple layers of enforcement before anything reaches the model, the calendar, or anyone's inbox.</p>

<h2>The guardrails stack</h2>

<p>Here's what sits between a visitor's message and the AI, and why each layer exists.</p>

<h3>A scoped system prompt</h3>

<p>The bot's instructions define exactly two jobs: book a call, send a note. Every other request gets a polite redirect. This is the first line of defense, and it's the one the McDonald's bot was missing entirely.</p>

<p>But a system prompt alone isn't enough. Models are cooperative by nature — a sufficiently creative prompt can coax most models into going off-script. So we don't rely on the system prompt as the only boundary.</p>

<h3>A classifier that screens every message</h3>

<p>Before the main model sees a visitor's message, a lightweight classifier evaluates it. Is this on-topic for a booking flow? Is it an attempt to hijack the conversation? Is someone trying to extract the system prompt?</p>

<p>Off-topic and injection attempts never reach the main model. The classifier handles them directly with a short redirect. Three flags in a session and the conversation ends. This is cheap — a fast, small model making a simple three-way classification — and it catches the vast majority of misuse before it costs real tokens.</p>

<h3>Hard limits on everything</h3>

<p>Every session has a turn cap, a per-message input length limit, a per-response output token ceiling, and per-IP rate limits on the booking endpoint (plus site-wide caps on top). The exact numbers are tuned to be invisible during a normal booking — which typically wraps up in five or six turns — but tight enough to cut off anyone probing the edges.</p>

<p>We don't publish the thresholds. Every public API endpoint will eventually meet a bot, a fuzzer, or someone curious about what happens at the boundary. Keeping the limits opaque means attackers can't park right below them. Hard caps mean we don't have to guess about worst-case costs.</p>

<h3>Scoped tools with server-side enforcement</h3>

<p>The model has exactly three tools: check calendar availability, create a calendar event, and send an email. That's the full surface area.</p>

<p>The email tool's recipient is hardcoded server-side. The model can compose the message, but it can't choose who receives it. The calendar tools are restricted to team member calendars — the model provides a host identifier, but the server validates it against a known list before making any API call.</p>

<p>This is the principle that matters most: <strong>don't give the model capabilities it doesn't need, and don't trust the model to enforce constraints you can enforce in code.</strong></p>

<h2>What the AI never sees</h2>

<p>The most effective guardrail isn't what the model is told not to do — it's what the model never has access to in the first place.</p>

<p>Our team's calendar data is fetched and processed entirely server-side. The model receives a formatted list of available slots: "Tuesday, Apr 29 at 10:00 AM — with Titus (VP of GTM)." It never sees busy blocks, event titles, attendee lists, or any other calendar metadata. It can't leak what it doesn't know.</p>

<p>When a booking is created, the calendar event ID is captured through an internal channel for our logging system. The model gets back a simple success or failure signal — nothing more.</p>

<p>Host email addresses are used server-side for Google Calendar API calls, but the model works with first names only. Even if someone managed to jailbreak the conversation model, the tools themselves don't expose internal email addresses in their responses.</p>

<p>The less the model knows, the less it can leak. We treat the LLM as an untrusted component that happens to be good at conversation — and build the security model accordingly.</p>

<h2>Failing safely</h2>

<p>Things go wrong. APIs time out. Models produce unexpected output. Visitors do things you didn't anticipate.</p>

<p>When the classifier can't reach the API, it fails open — the message passes through to the main model, which has its own off-topic handling in the system prompt. When a calendar or email API call fails, the bot apologizes, gives the visitor a direct email address, and ends the conversation. It doesn't retry, hallucinate a confirmation, or keep chatting as if nothing happened.</p>

<p>Every guardrail trigger is logged. Turn limits, input caps, classifier rejections, tool failures — all of it feeds back into our analytics so we can see how the bot behaves under real traffic, not just in testing.</p>

<h2>The real product isn't the AI</h2>

<p>The language model is table stakes. Any modern LLM can understand "book me a call Tuesday at 2pm" and translate that into a structured tool call. That's a solved problem.</p>

<p>The unsolved problem — the one most companies skip — is everything around it. Scoping the agent's role. Screening inputs before they reach the model. Enforcing limits at every layer. Restricting tool access to the minimum needed. Keeping sensitive data out of the model's context entirely. Handling failures gracefully instead of silently.</p>

<p>The McDonald's bot could reverse a linked list because nobody told it not to. Ours can't — not because the model is less capable, but because we built the envelope that keeps it focused on the one job it's there to do.</p>

<p>That's what makes an AI agent production-ready. Not the model. The guardrails.</p>

<p><strong>If you want an AI chatbot on your site that stays on task, doesn't leak data, and handles abuse gracefully — book a call and see the guardrails in action.</strong></p>

Source: https://www.civic.com/articles/booking-bot-guardrails
