Build a booking agent that checks the real calendar
I build Settl solo with Claude, and the same rule keeps showing up whenever AI touches a real workflow: language can be flexible, but state cannot. A booking agent may chat naturally. The calendar still has to be the authority.
Why replacing the form is not enough
A form asks every question in one fixed order. A booking agent can ask only what matters next. That is better for the customer, but it creates a new risk: the model may offer a time that sounds reasonable without checking whether it is free.
The agent needs to separate two jobs:
- Conversation: understand the service, location, preferred day, and constraints.
- Calendar action: read busy periods, offer valid slots, then create one confirmed event.
Google Calendar exposes a freeBusy query for checking occupied time and events.insert for
creating an event. Those are two separate calls for a reason. Read availability first. Write only
after the customer confirms. The official references are freeBusy
and events.insert.
Model the booking as states
Do not ask the model to remember where it is. Store one explicit status and advance it in code.
{
"status": "collecting | checking | offering | awaiting_confirmation | booked | handed_off",
"service": "intro call",
"duration_minutes": 30,
"timezone": "Asia/Singapore",
"preferred_window": "Tuesday afternoon",
"offered_slots": [],
"confirmed_slot": null
}
If the customer changes the date after you offer times, clear the old offers and check again. Never carry stale availability into the next turn.
Save the exact slot ids or timestamps that were offered. A reply like "the second one" only makes sense against that saved list. If the conversation resumes after the offer expires, say the times need to be checked again. Do not silently map an old answer onto a new list.
The booking prompt
Use the model to choose the next question or action. Keep the calendar calculations in code.
You are a booking assistant.
Collect only the missing details required for this service.
Ask one short question at a time.
Never invent availability or claim a booking is complete.
Allowed decisions:
- ask_for_detail
- check_availability
- offer_slots
- request_confirmation
- create_booking
- handoff
You may offer only slots returned by CALENDAR_RESULTS.
You may request create_booking only when the customer clearly confirms one offered slot.
Return JSON. Do not send prose outside the JSON object.
Calculate valid slots in code
The model should not do date arithmetic. Give your code business hours, duration, buffer time, minimum notice, maximum booking horizon, and the customer's timezone.
BUSINESS RULES
- Open Monday to Friday, 09:00 to 17:00 Asia/Singapore
- Appointment length: 30 minutes
- Buffer after each appointment: 15 minutes
- Minimum notice: 4 hours
- Maximum booking horizon: 30 days
- Return at most 2 options
Your slot function subtracts busy periods from working hours, applies the rules, then returns ISO timestamps. The agent turns those timestamps into a friendly question. This keeps daylight-saving and timezone errors out of the prompt.
Confirm before writing
The safest flow has two visible steps:
Agent: I can do Tuesday at 2:00 PM or 3:30 PM Singapore time. Which works?
Customer: 3:30 works.
Agent decision: create_booking for the exact previously offered 3:30 PM slot.
Before creating the event, check availability one final time. Another person may have taken the slot while the customer was deciding. If it is gone, apologize plainly and return two new options.
After creation, send the exact date, time, timezone, location or meeting link, and reschedule link. Do not use "confirmed" until the calendar API returns success.
Treat cancellation and rescheduling as separate actions with the same confirmation discipline. The agent should show the booking it is about to change, ask for a clear yes, and record the result. Never identify a booking from a person's name alone when several people may share it.
Test cases worth keeping
Most booking bugs live outside the happy path. Test these before a public demo:
1. Customer asks for a busy time.
2. Customer replies "the second one" to two offered slots.
3. Customer changes timezone halfway through.
4. Offered slot is taken before confirmation.
5. Calendar read succeeds but event creation fails.
6. The same confirmation message arrives twice.
7. Customer asks for a person.
Expected results should include the final stored state and the number of calendar writes. For the duplicate message test, that write count must remain one.
Run a small concurrency test too. Start two conversations, offer both the final open slot, then confirm them seconds apart. One should book. The other should receive fresh options. This test proves your second availability check is real instead of decorative.
When a normal form wins
Use a form when every booking requires the same regulated or lengthy information, when the answers need strict validation before any conversation, or when customers want to compare a full schedule at once. Use an agent when the next question genuinely depends on the last answer.
The point is not to delete forms. It is to remove questions that do not belong in this customer's path.
A hybrid often works best. Let the conversation narrow the service and time, then open a short form for information that needs exact formatting, consent, or document upload. The customer still avoids irrelevant questions, while your business keeps a reliable record for the details that must not be interpreted loosely.
Run this on your codebase
Inspect this project and design a conversational booking agent backed by the real calendar.
1. Find the current form, calendar code, database models, timezone utilities, and auth flow.
2. Do not edit anything yet. Report the exact files and APIs already available.
3. Define a booking state machine from collecting details through booked or handed_off.
4. Keep date arithmetic, business hours, buffers, and slot generation in deterministic code.
5. Use the model only to extract answers and choose the next allowed decision.
6. Query real availability before offering slots and again before creating the event.
7. Require explicit confirmation of one previously offered slot.
8. Make duplicate customer messages unable to create duplicate events.
9. Write tests for timezone changes, stale slots, failed writes, and human handoff.
10. Return the data shape, tool contracts, prompt, file plan, and tests before coding.
End with the smallest vertical slice that proves one conversation can create one real event.
Download the runnable pack
This is a complete starter for the workflow in this guide, not a screenshot. The safe default validates fictional input and returns a deterministic, reviewable draft. It does not publish content, message a customer, change pricing, edit production data, or execute another external action.
- Download the complete pack
- Import the n8n workflow
- Open the sample input
- See the expected dry-run result
- Run with Docker Compose
- GitHub validation workflow
- GitHub manual run workflow
Run the downloaded folder locally:
npm test
npm run validate
npm run sample
cp .env.example .env
docker compose up --build
The ZIP includes an importable n8n webhook, a dependency-free Node 22 service, Docker Compose, GitHub Actions validation and manual-run workflows, fictional sample input, expected dry-run output, and tests. If you later enable Claude, keep the API key in the service environment. The n8n export contains no credentials. Live mode still returns a draft for approval.