Key takeaways
- Almost every assistant that claims to book is really collecting a request that somebody confirms by hand afterwards.
- Booking for real needs two tools, not one: checking slots and creating the appointment. And time passes between them.
- What you verified when offering a slot is not valid when confirming it: you have to re-read the calendar right before writing.
- Never just say no. A clash has to come back with alternatives or the user walks away.
- The model cannot be trusted with input: it sends stale dates, strings containing the word "undefined", and phone numbers that say "no".
Table of contents
Booking for real, or looking like it
When a vendor tells you their assistant "books appointments", it is worth asking one specific question: does it write to the calendar, or does it leave a request for somebody to confirm?
The second option is perfectly reasonable and it is what most do. But it is not the same thing. If somebody picks a slot at eleven at night and the next morning the front desk tells them that slot was not free, you have turned a booking into an errand, and added a disappointment on top.
This article is about what the first option takes. It is the system this site’s assistant uses to schedule intro sessions, and you can try it from the chat.
Two tools, not one
The agent does not talk to the calendar directly. It has two separate tools, and that separation is what makes the system reliable.
The first checks availability. It takes a date, or none, and returns free slots. It has three modes, because people ask in three different ways: about one specific day, about a whole week, or simply "whatever you have next". That last mode is the most used, and the one that avoids the tedious conversation of proposing days one at a time.
The second creates the appointment. It takes the person’s details and the chosen slot, and it is the only one that writes.
Between the first offering a slot and the second confirming it, real time passes: the user reads, thinks about it, types their name and email. It might be two minutes or it might be ten.
The slot that no longer exists
There is the problem almost nobody plans for. If the system trusts the availability it checked at the start of the conversation, sooner or later it creates an appointment on top of another. All it takes is two people talking to the assistant at once, or someone on the team putting something in the calendar by hand meanwhile.
The fix is neither elegant nor clever, it is simply the correct one: the tool that creates the appointment re-reads the calendar right before writing. It does not trust what was checked five minutes ago, nor even its own earlier query.
The full flow looks like this: receive the details, look at the events already on that day, validate that the requested slot is still free, and only then create the event and send the emails. If at that final moment the slot is taken, nothing is written.
Overlapping is not matching
A small detail that ruins the system if you get it wrong: checking whether two appointments clash is not checking whether they start at the same time.
An appointment from 10:00 to 10:20 and another from 10:10 to 10:30 do not start alike and overlap all the same. The correct check is the classic interval one:
const isOccupied = busy.some(b => slotStart < b.end && slotEnd > b.start)
Two intervals overlap if one starts before the other ends and ends after the other starts. Four comparisons, no edge cases. Compare start times instead and the system works in testing, where everyone picks times on the hour, and fails in production.
Do not just say no
When the slot is taken, the easy response is an error. It is also the surest way to lose that person: they have just spent three minutes giving you their details and what they get back is a refusal.
What the system does is work out, at that very moment, which slots are still free that day and return up to five of them, along with the message that this time is no longer available. The agent does not have to ask anything again: the alternatives arrive with the answer and it offers them in the same sentence where it delivers the bad news.
It is a product decision more than a technical one. The cost is half a dozen lines. The difference between "not available" and "that one has gone, but I have 11:00, 11:30 or 12:00" is the difference between a conversation that dies and one that continues.
The model sends junk and you plan for it
This is the part that appears in no demo and takes up half the code.
A language model calling a tool is not a form. It sends what it feels like, and the shape changes depending on how the request arrives. Sometimes the data is in the body, sometimes inside a field holding a string with JSON in it, sometimes in a different field again. The first thing the system does, before looking at anything, is unwrap all those layers down to a flat object.
Then there are the values. The model literally sends the word undefined as if it were data. It sends null as text. And when somebody does not give their phone number, instead of omitting the field, it sends things like "no", "no phone" or a dash. All of that has to be normalised, or you end up with a client record stating their phone number is "undefined".
And finally validation: when required fields are missing, the error returned lists which ones. That is not cosmetic. The message goes back to the agent, and an agent reading "missing: email, date" can ask for them and retry. One reading "error" just stalls.
The date the agent reuses
The oddest bug, and the hardest to reproduce: the agent proposes dates in the past.
It happens because the model has the whole conversation in its context, and in long conversations it ends up reusing a date mentioned at the start, or one that appeared in an example. It is not broken: it is doing what a model does, which is continue a pattern.
The defence is an explicit check against today’s date in the right timezone, and an error message that does more than refuse:
if (date < todayISO) {
throw new Error('La fecha de reserva (' + date + ') está en el pasado. ' +
'Fecha actual en Madrid: ' + todayISO + '. El agente probablemente ' +
'reutilizó una fecha antigua. Pide al usuario una nueva fecha.')
}
That message is written to be read by the model, not by a person. It tells it what happened, what the real date is, and what to do next. Writing errors with their reader in mind is, in agent systems, the difference between one that recovers on its own and one that needs you to step in.
Daylight saving is not written by hand
Creating an event in the calendar needs the time along with its UTC offset. The temptation is to write +01:00 and move on, and it works until the last Sunday in March, when Spain shifts to +02:00 and every appointment moves by an hour with nobody understanding why.
The way to avoid that without a date library is to ask the system itself: take noon UTC on that day, have it formatted in the Madrid timezone, and look at what hour comes out. If it says one, the offset is one hour. If it says two, it is two.
It is an ugly trick and it is correct by construction: there is no table of when the clocks change to maintain, because the operating system already has that table.
What it costs to maintain
So as not to leave the impression that this is ever finished, an honest warning about the real cost.
By separating the availability lookup from the appointment creation, you end up with two places that know your opening hours: the one generating the slots on offer, and the one working out alternatives when there is a clash. Change the opening hours in one and forget the other, and the system starts offering slots on one side that the other does not recognise. It throws no error, breaks nothing, and only shows when someone tries to book at an odd time.
It is exactly the same kind of silent bug as the Markdown conversion we described in another article: the system responds, it looks right, and it is wrong. If you build this, the first thing to do once it works is pull the opening hours into a single place that both read from.
Frequently asked questions
Can this be done with any calendar?
With any that lets you read and create events from outside through an API. Google Calendar and Microsoft 365 do. Sector-specific management software varies: some allow it, some only on certain licences, and some not at all. It is the first thing to check, before promising anything.
What if the software does not allow integration?
Then the assistant collects the request with all the details and leaves it ready to confirm in one click. That is the first option from the start of the article. It works, it is useful, and it should be called by its name instead of calling it booking.
Do you need n8n to build it?
No. We have it in n8n because the rest of the system lives there and you can take it in at a glance, but the logic is the same in code: unwrap the input, validate, re-read the calendar, check for overlap, write or return alternatives. The tool is the least of it.
How do I know this actually works?
By opening the chat on this site and booking. It is the same system described here, and the appointment lands in a real calendar with its confirmation email.