Help me to build a error-free chatbot

Hi everyone,

I’d like some advice on the project I’m describing below.

For the past weeks, I’ve been building a WhatsApp chatbot demo in n8n to handle appointments and answer FAQs for a medical clinic. I’m using Google Sheets and Google Calendar.

I don’t have experience building workflows in n8n, and I’ve noticed that the AI ​​model (Gemini or others) makes several errors in its responses. It mixes up dates and times, and sometimes claims there are no free slots when there actually are. I know this is part of the troubleshooting and fine-tuning process, but I already have a fairly extensive System Message with many rules designed to prevent these errors, and it keeps making them.

So, my question is: for a WhatsApp chatbot, is it better to use fixed options (like a menu) to avoid these errors, rather than having the AI ​​respond with free-form text?

My goal is really to give customers the feeling that they’re talking to a person, not a robot. However, if the AI ​​is prone to so many errors, I’m wondering if it would be better to use buttons, fixed menus, or a hybrid approach?

I have a main workflow with 6 tools to check availability schedule, reschedule, cancel, follow-up appointments and answer FAQs, and each tool has a separated sub workflow.

What do you suggest me that would be better, use free text form, hybrid form with buttons for critical things or how?

And regarding the logic, is good practice to have the tools with separated sub workflows?

Thanks in advance.

1 Like

Hybrid is the right call, but I’d draw the line differently than “buttons vs free text”. The split that matters is: the AI decides what the person wants, and plain workflow logic decides the facts.

Right now the model is probably reading raw Calendar data and working out free slots itself. That’s exactly the part LLMs are bad at: comparing times, time zones, “next Tuesday”, overlapping events. No system message fixes that reliably, which is why the rules keep leaking.

What I’d change:

  1. Let the availability tool return the answer, not the raw data. Inside the sub-workflow, get the busy times with the Google Calendar node’s Calendar > Availability operation (Output Format set to Booked Slots), then use a Code node that subtracts those from the clinic’s opening hours for the requested day. Return a short, final list like “Tue 6 Oct: 09:30, 11:00, 15:00”. If the list is empty, return the words “NO_SLOTS”. The model only phrases what it gets back.

  2. Pin down the date before you call the tool. Let the AI turn “next Tuesday afternoon” into an exact date, but pass today’s date and the clinic time zone into every call, and have the tool echo back the date it actually searched (“searched 2026-10-06, Tuesday”). Most date mixups come from the model guessing what today is.

  3. Use buttons for the moment of commitment. Free text is fine for questions and for “what do you have on Thursday”. When it’s time to book, send the slots as buttons or a list, so the booking step gets an exact slot ID, not a sentence to interpret. One heads up: the WhatsApp Business Cloud node’s Send operation has no interactive message type, so send buttons with an HTTP Request node using your existing WhatsApp credential (type “interactive”). Reply buttons max out at 3, so for more slots use a list message (up to 10 rows), and the tap comes back to your trigger with the ID you set.

  4. Add one check at the end. Before the reply goes out, if the bot’s message says there are no slots but your availability tool returned some, don’t send it: resend the slot list, or hand over to a human. Turn on Return Intermediate Steps in the AI Agent node so you can see what the tool returned, and then it’s a tiny IF node that catches exactly the error you described.

And yes, separate sub-workflows per tool are good practice. It also makes the next step easy: keep a small list of test messages with the answer you expect (“Can I come Monday at 10?” on a day where you know 10:00 is free) and run them after every change, so you see a regression before a patient does.

2 Likes

Hybrid is the right instinct, but the split that actually kills your date/slot errors is a different one: the model should decide intent, plain code should decide facts.

Right now the model is almost certainly reading raw Calendar data and reasoning its way to “free slots” — that’s where mixed-up dates and phantom “no availability” come from. Restructure it so your availability tool returns a strict JSON list of concrete slot objects, and the model’s only job is to present those options back to the user, verbatim. It never computes availability, it quotes it.

Then add a validation layer after the model answers: a Code node that checks every date and time the model uttered against the actual slot list it was given. If it invented or mangled one, auto-correct it or re-ask. That’s cheap insurance and it eliminates the entire “claims there are no free slots when there are” class of error, because the truth was already in the payload.

On buttons vs free text: use WhatsApp interactive lists or reply buttons for the critical actions (confirm, reschedule, cancel) and free text for FAQs. The buttons guarantee the user can only pick a real slot — the model can’t hallucinate a confirmation the UI won’t allow.

Your sub-workflow-per-tool structure is good practice, keep it. One addition: pass a shared context item (user id, pending slot, last action) through every sub-workflow so conversational state never lives inside the chat text itself — that’s the other common source of “it booked Tuesday when the user meant Thursday”.

I run a WhatsApp appointment + FAQ assistant in production for a medical clinic on exactly this stack (Sheets + Calendar behind it), and this intent/facts split plus the validation layer is what took ours from “funny demo” to reliable. Happy to look at your workflow export if you want a second pair of eyes on the structure.

1 Like

I’d keep the separate tool sub-workflows and the hybrid interface. The boundary I’d tighten is how the availability result becomes the message the user receives.

First, trace one known-free slot that the bot incorrectly reports as unavailable:

requested date/timezone → Calendar response → calculated slots → tool output → sent message

That helps distinguish a date interpretation problem from a scheduling calculation error or an incorrect final response. More prompt rules won’t fix all three.

1. Make “no slots” and “couldn’t check” different outcomes.

Give the availability sub-workflow three explicit application-level states:

  • slots_found: the required lookups succeeded and concrete slots satisfy the clinic’s scheduling rules.
  • no_slots: those checks succeeded, but no slot fits the requested date, appointment duration, working hours, buffers, and required resources.
  • lookup_failed: availability could not be established. Offer a retry or reception handoff, never “fully booked.”

2. Validate the Calendar result before calculating slots.

Google’s FreeBusy response can contain per-calendar errors inside an otherwise successful HTTP response. Check those and verify that every required calendar has a usable result. Don’t turn an error or a missing calendar into busy: [].

Even a valid empty busy list only means no busy intervals were reported for that query; it does not supply appointment slots. Your scheduling rules still apply.

3. Render availability outside the LLM’s free-text response.

Route the validated result through a deterministic formatter and one controlled WhatsApp send path. Generate the slot labels, IDs, and fallback wording from that result. For this branch, don’t also send the agent’s free-text answer. A tool message saying “results sent” is not an execution guard.

Test a known-free slot, a full day, a simulated Calendar error, and a model response that contradicts valid slots. The user should receive one appropriate response, with lookup failure clearly distinguished from no availability.

Could you share a synthetic example of the availability tool’s JSON from one incorrect “no slots” reply, along with the requested date and timezone? No patient details are needed.

1 Like

Hi Raseck, I’ve built chatbot platforms for years, and this is a very common problem. The model shouldn’t do the date math at all.

  1. Hybrid is the right choice. Free text for FAQs and small talk, but buttons or a list for the critical steps: pick the service, pick the day, pick a slot, confirm.
  2. Never let the AI calculate dates. Before the model runs, a Code node computes “today”, the weekday and the next 14 days in the clinic’s timezone and passes them in. Tools take and return strict ISO dates (2026-10-02T10:00), not “next Tuesday”.
  3. The availability tool should return a ready list of free slots, and the AI can only offer slots from that list. Then it can’t invent “no free slots”.
  4. Always confirm with a button before writing to the calendar: “Tuesday 2 Oct, 10:00, confirm?”
  5. Yes, separate sub-workflows per tool are good practice. Just keep the input and output format identical for all of them.

If you’d like, I can review your workflow JSON and point to the exact spots that cause the date errors.

Max

1 Like

Hi @Maksim_Igoshev thanks for your reply. I’m attaching the main flow and the “tool check availability” flow. Claude AI actually provided the structure and JavaScript code, as I’d never done anything like this before, which is why it took me so long to get this demo ready.

Now I need to figure out how to adapt this to a hybrid model, but I don’t know how to use buttons or list like you suggest. Do you know an example using buttons or lists? I’m testing with chat trigger of kapso free version.

And I also have a question about what you mentioned. For example, how do to handle the case where someone says, “I want an appointment for next Tuesday”? Or, “I want to cancel my appointment scheduled for next Friday”, does the LLM have to interpret this and provide what input parameters to the “Tool: check_availability” and “Tool:cancel_appointment”?

PS: I’m currently using the free version of the BSP kapso for messaging, as I don’t have access to the Meta Business API. I understand it takes months to be approved by Meta, and I’m unfamiliar with the entire process with Meta.

Thank you very much in advance for any help.

Hi Raseck, I went through both files and the base is good. First your question, then a few things worth fixing.

On dates: right now the model works out the date itself, which is why the prompt needs long rules about the year and times that have passed. In the hybrid, the model only detects the intent and returns the phrase as written, for example intent=book and when=“next Tuesday” or “mañana”. A Code node with Luxon in the clinic’s time zone then turns the phrase into a real date, that goes into check_availability, and the patient picks a free slot with a button. If the phrase is ambiguous, like “at the weekend”, the bot does not guess, it shows buttons with the nearest days. Past slots for today are removed in code with one line, not by a prompt rule.

Cancelling and rescheduling barely need the model. You already have the WhatsApp number in the message, so look up the patient’s upcoming appointments and show them as buttons; the patient taps the right one, and cancel_appointment or reschedule_appointment gets the calendar event id. That way the wrong appointment can never be cancelled. Overall the hybrid looks like this: the main menu as buttons (book, reschedule, cancel, question), the model only for free text and knowledge base questions, and anything that changes an appointment goes through buttons and a confirmation.

What I can see in the files:

  1. In “Kapso - Send Text” the “to” field is hard-coded to one number, so replies go to that number instead of the person who wrote. It should be the wa_id from “Config + Data”.
  2. In check_availability the node is named “Valculate available hours” but the connection points to “Calculate available hours”. It looks like the slots are never calculated, please check the node name.
  3. The prompt asks the agent to call list_my_appointments, but there is no such tool connected. And reschedule and cancel only receive wa_id and change the “most recent active” appointment, while the prompt says to pass google_event_id.
  4. The wa_id is filled in by the model through $fromAI. It is safer to take it straight from the message data, so the model cannot change it.
  5. The JSON you posted shows your Kapso webhookSecret, so I would rotate it.

On Meta: a free BSP is fine for the demo. For production you can connect the WhatsApp Cloud API directly through Meta; in my experience it is usually not months, but the timing depends on their business verification.

If you like, I can do this rework on your workflows: the hybrid button menu, date parsing in code, and fixed cancel and reschedule. I would start by fixing the issues above so you see the result right away.

Max

чт, 1 окт. 2026 г. в 17:09, Raseck Malkic <notifications@n8n.discoursemail.com>:

1 Like

@Maksim_Igoshev Thanks a lot Maksim, for your reply, suggestions and your time. When I exported the JSON, I was worried that some private data might be exposed. I didn’t realize the webhook secret and number were included, but I’ve changed them now.

I’ve tried making a few of the improvements you suggested, but I have no idea how to get a button menu to appear or how to make the bot respond with a list of options. If you could share an example or show me how it’s done when you have the time, I’d really appreciate it.

One more question: would you recommend using a BSP like Twilio, Kapso, or another one? What would be the pros and cons compared to the Meta API?

Regards

I built a credential-free Kapso/n8n example for the menu you asked for. It covers three-action buttons, a four-action main-menu list, slots lists and confirmation buttons. The payload/selection logic passes 29 synthetic checks; I have not sent a live WhatsApp message or run your n8n instance.

Book + Reschedule + Cancel + Question is four options, so use a list. Reply buttons allow at most three.

Set a Code node to Run Once for All Items. For one item containing the documented Kapso v2 trigger output, this builds that four-option list (the snippet was also tested separately):

const item = $input.first().json;
const event = item.body ?? item;
if (event.message?.kapso?.direction !== 'inbound' || event.message.kapso.passive === true) return [];
const to = event.message?.from?.replace(/^\+/, '');
if (!/^\d{7,15}$/.test(to ?? '')) throw new Error('Missing sender');
if (!/^\d+$/.test(event.phone_number_id ?? '')) throw new Error('Missing phone_number_id');
return [{ json: {
  url: `https://api.kapso.ai/meta/whatsapp/v24.0/${event.phone_number_id}/messages`,
  body: {
    messaging_product: 'whatsapp', to, type: 'interactive',
    interactive: {
      type: 'list', body: { text: 'Choose an appointment action.' },
      action: { button: 'View options', sections: [{ title: 'Appointments', rows: [
        { id: 'menu_book', title: 'Book appointment' },
        { id: 'menu_reschedule', title: 'Reschedule' },
        { id: 'menu_cancel', title: 'Cancel appointment' },
        { id: 'menu_faq', title: 'Ask a question' }
      ] }] }
    }
  }
} }];

After it, use HTTP Request: POST, URL ={{ $json.url }}, JSON body ={{ $json.body }}, and Header Auth with X-API-Key in your existing Kapso credential. This uses Kapso’s endpoint; the example does not need a direct Meta connection.

A selected row returns as message.interactive.list_reply.id; a reply button uses message.interactive.button_reply.id. Route those IDs with a Switch node. For booking/cancellation, bind the choice to that sender’s pending menu, show a confirmation, and recheck the exact slot/event before a Calendar write.

If your Kapso trigger produces a different shape, send one synthetic output item from a list tap and your n8n/Kapso-node version. Replace numbers and IDs with placeholders; omit tokens, webhook secrets and patient details. That will let me adapt the mappings to your trigger.

References: Kapso lists, send-message API.

1 Like

For this demo, I’d stay with Kapso if its sandbox supports the buttons and lists in your example. You already have it connected, so that would let you test the menu without switching providers.

For a real clinic, I’d compare cost at the expected message volume, support, who will maintain it, and how the clinic keeps control of its WhatsApp account and number.

To make a later switch easier, keep provider-specific send requests in a separate n8n sub-workflow, and translate incoming payloads into the fields your booking workflow needs. That won’t make migration automatic, but it limits how much booking logic you’d have to change.

One correction to the earlier date advice: Luxon handles timezone and date calculations, but it doesn’t interpret “next Tuesday” by itself. You still need to resolve that phrase and confirm the actual date with the user.

Is this for one clinic or separate bots for several clinics? That affects the account, credential and calendar setup.

For the “error-free” part specifically, the biggest wins I’ve seen come from what happens around the AI, not inside the system message. A pattern that works well:

  1. Validate before you write. After the agent extracts a date/time, add an IF node that sanity-checks it: is the date actually in the future, is it within clinic opening hours, is every required field present? If not, loop back and ask the patient again instead of writing bad data into Google Sheets or Google Calendar. A wrong entry in the calendar is 10x harder to fix than one extra question.

  2. Give the agent structured output. In the AI Agent node, define the output schema (date, time, service, patient name as separate fields). The agent then returns clean JSON instead of a sentence you have to parse with expressions afterward. This alone kills most of the “mixed up dates” errors.

  3. Set up an error workflow. In the workflow settings (the three dots, top right), attach an Error Workflow that sends you a Telegram or email message when anything fails. You’ll hear about a broken booking before the patient complains.

  4. Watch the 24-hour window. WhatsApp only lets you send free-form messages within 24 hours of the patient’s last message. After that you need an approved template message. Build your follow-up reminders around this or they’ll silently fail.

One gotcha: test the whole thing with the manual trigger first (pin a few real example messages as test data), and only switch on the live WhatsApp trigger once the happy path and the weird inputs work. The webhook won’t wait for you to debug.

@Marchik Thanks for your advice. I’ll definitely take it into consideration, as I didn’t know or hadn’t thought of much of what you’ve told me. Everything I’ve done so far has been by asking Claude for the free version, haha, and after so many weeks without finishing, I decided to ask in this community. This is really my first attempt at offering a WhatsApp chatbot solution or any kind of N8N automation for a business. In a casual conversation, a doctor showed interest because they have a backlog of appointments and aren’t responding to WhatsApp messages in a timely manner, so I’m trying to build a working demo to increase my chances that they’ll buy me the solution. So, it’s only for one clinic for now, and I decided to try Kapso because it offers free messages for testing, and I don’t know how long it might take with Meta just for testing purposes.

For a first demo with one clinic, I’d focus on one appointment request reaching reception. Kapso gives you a way to test that while you work out the clinic’s booking process.

Ask the doctor or receptionist to walk you through how they handle one request today. Then use a made-up enquiry to show the bot collecting the details reception needs and passing them over. Tell the patient the request is awaiting confirmation, not booked.

Have reception check whether they can take the next step without asking for those same details again. Anything missing becomes a specific thing to fix. That gives you something useful to show the doctor before expanding the bot.

Where does reception currently record appointment requests and confirm bookings?

Hi Raseck,

Checking in on the chatbot. Did the fixes I listed work on your side? If anything still breaks, send me the error or the execution log and I’ll point to the exact node.

Max

пт, 2 окт. 2026 г. в 20:26, Raseck Malkic <notifications@n8n.discoursemail.com>:

Good discussion — one gap I haven’t seen mentioned: the slots you quote can go stale between the availability check and the booking. Your availability tool computes free slots at turn N; the user taps a button; by the time the booking subworkflow runs, that slot may be gone (another patient, calendar sync). If the booking step writes the event from whatever time string is still in the conversation, you get double bookings or a silently wrong time.

Handle it in the booking subworkflow, not in the prompt:

  1. Take the slot ID from the button payload and re-run the same availability check for that exact range right before creating the event.
  2. If it’s still free, book it and build the confirmation from the created event data — not from the agent’s memory.
  3. If it’s taken, re-run availability for nearby slots and send those. Never let the agent invent an alternative.

This gives you a deterministic conflict branch instead of trusting conversation state, and it pairs well with Marchik’s three-status split above.

Rechecking before the write is useful. There’s still one gap, though: two booking runs can both see the same slot as free before either creates the event. Both can then send a confirmation.

If you later add automatic booking, I’d test two requests for the same clinician and overlapping appointment times arriving together. At most one should be confirmed; the other should get a conflict response or go to reception. Testing a slot that was already taken won’t catch this race.

Rechecking and then creating a Calendar event are still separate steps. Preventing conflicts reliably needs a booking process that coordinates all writers, including reception. An n8n-only lock won’t cover appointments entered directly in Calendar.

For Raseck’s first demo, I’d keep the reception handoff we discussed until that booking path is clear. The bot can acknowledge an appointment request without telling the patient the appointment is confirmed. That still addresses the doctor’s slow WhatsApp responses without promising a booking the system hasn’t secured.

1 Like

The model is mixing the dates because it is doing the maths. Stop it from doing that.

Let the AI only read the message and return two things: what the patient wants, and the date words they used, like “next Tuesday”. A Code node turns those words into a real date in the clinic timezone. Google Calendar then says which slots are free. The model should never pick the date or the slot.

When they book, send a WhatsApp list and a Confirm button. Read the slot back before you write it. Check Calendar again right before the booking, so two patients cannot take the same time. If Calendar or Sheets fails, tell the patient the clinic will confirm, and alert your staff.

Medical questions should go straight to staff. The bot should not answer those.

Your six sub-workflows are fine, keep them.

1 Like

Thanks to everyone for the advice. I’m trying to put together a plan based on the suggestions from everyone who kindly replied. @Abdulrasaq Regarding letting the AI ​​handle cases like “next Tuesday,” “the day after tomorrow,” or “the first Monday in November” how do I ensure the AI ​​correctly interprets expressions like those? I still haven’t figured that part out yet.

Thanks again.

@Raseck honestly you don’t want the AI interpreting those, you want it picking.

What’s worked for me: before the agent runs, a Code node builds a list of the next ~60 days with Luxon in the clinic’s timezone, like “Tue 13 Oct 2026 = 2026-10-13”. Put that in the system message with today’s date at the top. Now “first Monday in November” is just a lookup.

Have the agent return only an ISO date through a Structured Output Parser. Then a Code node checks it’s actually in the list and not in the past. If it’s not, ask again.

Then always read it back with buttons: “Tuesday 13 Oct, right?” [Yes] [Other day]. “Next Tuesday” confuses humans too, so the confirm catches whatever’s left.

1 Like

@Raseck Do not let the AI turn those words into a real date. Let it only copy what the patient said.

Ask the model for simple JSON like this:

{

“intent”: “book”,

“date_phrase”: “next Tuesday”

}

So “the day after tomorrow” stays as those words, and “the first Monday in November” stays as those words. The model does not calculate anything.

A Code node then turns the phrase into a real date in the clinic timezone. Use Luxon, which is already in n8n. Give it today’s date so it does not rely on the server clock.

“tomorrow” or “the day after tomorrow” - add 1 or 2 days

“next Tuesday” → the next Tuesday after today

“the first Monday in November” → find 1 November, then move to the first Monday

The Code node gives back a real date. Google Calendar checks free slots on that date. The model only shows the slots Calendar returns. If the phrase is unclear, ask the patient to pick from a short list instead of guessing.

Your six sub-workflows can stay. Only move the date step into the Code node.

Full disclosure: I am with Pond, we build custom AI agents. If you want this mapped against your Kapso setup before you change it, we can do the architecture for free. You keep the map even if you hire someone else, and there’s a 7-day refund if you get it built with us.