← back to work.mountaintopvirtual.com
Case study · natural-language input, AI parsing, validated batch booking — two n8n workflows

One sentence in. Every date confirmed, then booked, one by one.

AKVS runs a lot of recurring appointments — the same slot, every week or every other week, for months at a time. Instead of making someone click through a calendar picker date by date, I built a system that takes a plain-English request like “every other Tuesday at 2pm starting next month,” has an AI model turn that into a structured recurrence rule, previews every resulting date, and then books the whole batch into GoHighLevel — without one rejected slot taking the rest of the batch down with it.

Client — AKVS · live at book.mountaintopvirtual.com Stack — n8n, Claude API (Anthropic), GoHighLevel / LeadConnector Calendar API Role — architecture, backend, integration

The problem

Three separate problems stacked on top of each other

Recurring bookings sound simple until you have to actually compute them. “Every other Tuesday starting next month” isn't one appointment, it's an open-ended set of them, and turning a sentence like that into exact calendar dates means parsing language a form field can't anticipate every phrasing of. Then there's the calendar math itself: a series that spans a daylight-saving transition needs each date computed with its own correct UTC offset, not one offset applied to the whole series. And once the dates are confirmed, booking isn't a single API call — it's a batch of them against a real calendar, where any individual date can be rejected (double-booked, invalid, whatever GoHighLevel's side decides), and the system has to handle that without either silently dropping a failed date or taking the rest of the batch down with it.

What I built

Two workflows, split where the job actually changes: parse & preview, then confirm & book

AI-parsed recurrence, not a regex

A webhook takes the plain-English request and sends it to Claude, which extracts a structured recurrence rule — the interval, the day, the start date, the end condition — out of however the client happened to phrase it. A code step then computes the literal dates from that rule, calculating each one's correct UTC offset individually so a date on the far side of a daylight-saving change doesn't come out an hour off. The result is returned as a preview. Nothing has touched GoHighLevel yet.

Dedupe the contact before anything touches a calendar

Once the dates are confirmed, a second workflow splits them into individual items and looks up the contact by email first. A new contact only gets created if none exists — the same client rebooking a second series doesn't end up with two contact records in GoHighLevel.

A rejected slot doesn't take the batch down with it

Each booking call to GoHighLevel's calendar-events API is explicitly configured not to throw on a non-2xx response, so a single rejected date comes back as normal data — with its real statusCode — instead of halting everything after it. A logging step immediately after classifies every item as ok or error, and the workflow hands that full per-item array back to whatever called it, so a partial failure is visible and specific instead of silent.

The judgment call: isolate what's recoverable, stop what isn't

Not every failure in this workflow gets the same treatment — that's deliberate

A rejected booking is treated as recoverable: that one item fails, the rest of the batch keeps running, and the caller gets back an explicit ok/error status for every date instead of a vague overall success or failure. Contact creation is not given the same leniency — if creating the contact fails, there's no valid contact to book an appointment against, so that branch is set to stop outright rather than continuing on and returning a result that looks complete but isn't. Deciding which failures get isolated and which get allowed to stop the item is the actual design decision here, not just wiring up the API calls.

Honest caveat: this books into a live production calendar, so I verified this behavior from the workflow's own configuration — the booking call's “never error” response setting, and the logging step's classification logic, both read directly off the live nodes — rather than by triggering a real execution against real appointment data to watch it happen.

The result

The live canvases

n8n canvas for the GHL Parse & Preview Dates workflow: a webhook feeding into an Anthropic Claude parsing step, a code node computing the exact recurrence dates, and a respond node returning the preview.

GHL Parse & Preview Dates — four nodes: webhook in, Claude extracts the recurrence rule, a code step computes the dates, respond with the preview.

n8n canvas for the GHL Recurring Appointment Booking workflow: webhook in, split appointments, contact lookup and conditional upsert branch, book appointment, log result, and respond with all items.

GHL Recurring Appointment Booking — splits the confirmed dates into items, resolves the contact, books each date, logs a per-item result, and responds with the full array.

2
workflows — parse & preview, then confirm & book
1
AI call to turn one sentence into a structured recurrence rule
0
dates silently dropped when a single booking call is rejected
Claude API structured parsing DST-safe date computation contact dedupe before booking per-item failure isolation GoHighLevel Calendar API

Where it stands

Both workflows are live in production for AKVS. The parse-and-preview step is the user-facing half, live at book.mountaintopvirtual.com; once a client confirms the dates it returns, the booking workflow runs server-side against GoHighLevel's calendar.