A reservation landing page and no-code CRM automation built for a
fictional BBQ restaurant — a self-directed learning exercise on
GoHighLevel's first day of use. The page was the easy part. The real work was
deliberately breaking the automation, then fixing what broke.
Type: Landing Page + No-Code CRM AutomationPlatform: GoHighLevel (GHL)Tools: Tally Forms · Workflow Builder · Reputation Management · Sheets · GmailRole: Solo builderTimeframe: Four build days · Day 1–4 on GHL
⚠ This is a practice project, not a live client engagement. Chief Sizzling
Grill is a fictional BBQ restaurant built to learn how no-code CRM automation actually
behaves end-to-end — not just how to drag boxes together, but why each piece does what
it does. No real customers, no real data. Everything below — the bugs, the fixes, the
destroy-testing — is real work against a simulated business.
// tl;dr
A full reservation-to-retention pipeline built solo on GoHighLevel in four days:
intake routing, ownership assignment, a 2-round escalation ladder, real calendar
booking, appointment-ID capture, cancellation teardown, review-response routing, and
a post-visit review request & retention sequence — then deliberately broken to find
what a "successful" workflow run doesn't tell you. Found and fixed a silent wrong-date
booking bug (dates drifted 2-3 months off with no error), a calendar-type mismatch that
silently rejected valid bookings, and closed the gap where a fully-booked slot used to
fail without telling the customer anything. The full pipeline, including the post-visit
review request and retention emails, has since been built and tested end-to-end.
The scope covered two halves of the same problem: a landing page with a reservation
form customers would actually use, and an automation that takes that submission and
safely turns it into a CRM record — without duplicate contacts, duplicate bookings,
or silent failures.
Several non-obvious bugs surfaced across two build days — Day 1 on the core
Found/Not Found logic, Day 2 on upgrading that logic further, then on ownership and
follow-up accountability, then on the appointment-scheduling layer itself — each one the
kind that looks fine in a quick demo and quietly breaks in production. All of them were
root-caused and fixed before this was called done. A later session added a review
response routing layer on top of the booking system, covered in Part 6. A further
session then built and tested the full post-visit review request and retention email
sequence, covered in Part 7.
Short on time? The most instructive bug is
Bug 07 — a date field that silently
booked real appointments months off from what the customer submitted, with zero error
thrown. It's the clearest example of the theme running through this whole build:
a "successful" run isn't proof of a correct one.
// the full pipeline: booking → review → retention
The booking automation below is one stage of a larger customer-lifecycle system.
The full pipeline is scoped as three connected stages. Stage 1 (booking) is fully
built and tested. A fourth, adjacent piece — review response routing, covering what
happens once a review actually comes in rather than requesting one — has also been
built and is documented in Part 6. Stages 2 (outbound review request) and 3
(retention) have since been built and tested in the live GHL workflow canvas,
triggered off the same Pipeline Stage Changed event used in Part 3, and are documented
in Part 7.
// this case study documents Stage 1 in full, including the Calendar trust-boundary step — originally scoped as a Google Calendar all-day event, later rebuilt as a native GHL Appointment with its own ID-capture workflow (see Part 4 below). Part 6 documents a separate, already-built review response routing layer that reacts to inbound reviews rather than requesting them — distinct from Stage 2. Stages 2 and 3 have since been built and tested end-to-end (see Part 7).
// the landing page
Built entirely inside GHL's Funnel/Site editor as a single-scroll structure, with
attention paid to visual cohesion rather than stitching together disconnected template
sections.
→ Page structure
Hero — bold headline, live-fire photography, dual CTA (Book Now / Order Now)
Welcome/About — personal framing + chef photo, avoids generic stock branding
Signature Dishes — three-image grid
Testimonials — placeholder quotes, flagged for real reviews later
Map + address — embedded for local discovery
Reservation form — First Name, Last Name, Phone, Select Date, Email
The design decision that mattered most wasn't visual — it was which fields were marked
required. Destroy-testing (below) showed that without Phone, Email,
First Name, and Select Date all mandatory, blank submissions could slip through and
create incomplete, unusable contact records.
// the automation
On paper, the base structure is simple:
// getting each box to actually do what its label suggested is where the real work happened
// three bugs found by breaking it on purpose
Bug 01
A misleading action label
⚠ What happened
GHL's "Find opportunity" action can behave two completely different ways depending on config
Its on-screen description changes to match — sometimes reads as a create action, sometimes as a pure search
Trusted the first description and recommended removing the separate Create step — had to walk that back
→ The lesson
Never assume a component's behavior from its label or a single glance at documentation-style text
The actual configured state has to be verified directly, every time
Bug 02
"Found" meant "ever had a reservation," not "has an open one"
⚠ What happened
No filter fields were set on Find Opportunity — any past opportunity counted as "Found"
Would have silently broken repeat customers — a closed-out reservation from last month would block a new card forever
✓ The fix
Added match condition: Pipeline Is Reservations
Added match condition: Status Is Open
Narrowed "Found" to mean specifically an open, active reservation — fixed same-day duplicates and repeat customers in one change
Bug 03
Contact matching doesn't catch typos
⚠ What happened
Found/Not Found logic depends on GHL recognizing repeat submissions as the same contact
Contact matching keys on email by default — one typo on a second submission silently creates a new contact
This bypasses all Found/Not Found logic entirely, since the system sees a different person
✓ The fix
Settings → Business Profile → Contact Deduplication Preferences
Primary match field: Email · Secondary match field: Phone
A typo'd email on a repeat visit now still gets caught by phone match
// destroy-testing results
A deliberate "how can I break this" pass, run before trusting the automation with anything real.
#
Test
Result
1
Invalid phone/email format
✓ Blocked by front-end validation
2
Blank required fields
✓ Blocked after marking required
3
Duplicate contact, same email
✓ Correctly matched, no overwrite
4
Duplicate submission, same valid data twice
✓ Second Create Opportunity skipped
5
Repeat customer, new booking after a closed reservation
✓ New card created, post-fix
// closing the silent-failure gap
Originally the "Found" branch only notified staff — the customer who submitted the
form got no response at all, which could look like a silent failure from their side.
A second notification was added so the customer isn't left to self-diagnose:
"Hi [Name], thanks for reaching out! It looks like you may already have an open
reservation with us. Don't worry — one of our staff will personally check this and
make sure everything's in order..."
// part 3 — round-robin assignment & escalation ladder
Once the Found/Not Found logic was solid, the automation was extended past "notify
staff" into an actual ownership and accountability system — every new
reservation lead gets assigned to a person, that person gets reminded, and if nothing
happens within 48 hours, it escalates to the business owner directly.
// Both the "Found" (owner-check) and "Not Found" (always assigns) paths converge into this same shared chain
Bug 04
Building the whole workflow twice
⚠ What happened
Found and Not Found branches were each built out independently — Task, Email, Wait, Check, Escalation, repeated in full down both sides
Any change to timing or the escalation ladder had to be made twice, with no guarantee the copies stayed in sync
→ The distinction
Legitimate duplication: messaging genuinely differs (double-booking tone vs. new-lead welcome tone) — this should stay separate
Accidental duplication: the escalation ladder mechanics are identical in both branches — this shouldn't be copy-pasted
Bug 05
Ownerless leads with no path to assignment
⚠ What happened
The Found branch assumed every existing opportunity already had an owner and skipped straight to notifying "the owner"
In practice the pipeline had a mix — manually created, imported, or workflow-created opportunities with no owner at all
Round-robin assignment only ran on Not Found — any ownerless "Found" opportunity fell through the crack permanently
✓ The fix
Added condition: Opportunity Owner Is Not Empty right after the Found branch
Owner set → proceed straight to the shared Task step
Owner empty → round-robin Assign to User first, then continue into the same shared step
Bug 06
Standard Builder can't merge branches back together
⚠ What happened
Fixing Bug 05 required both ownership paths to converge into the same downstream Task node
GHL's Standard Builder doesn't support dragging a connection from one branch into an existing node elsewhere on the canvas
Attempting it just detached the connection with no valid target
✓ The fix
Switched the workflow to Advanced Builder
Used the dedicated Go To action, built exactly for routing a branch to an existing step elsewhere on the canvas
Merged both ownership paths into one shared chain, eliminating a third copy of the logic before it was ever built
Both branches converge into the same two-round escalation pattern:
Round
Trigger
Action if unresolved
1
24 hours, no stage change
Internal Notification to assigned owner
2
48 hours total, still no stage change
Escalation to business owner directly
// the "stage changed" branch on each check is intentionally left with no further action — a stage change means a human already resolved the lead manually, so the ladder simply stops there
Built & Tested
Trust boundary: confirmed reservations → native Appointment record
→ The reasoning
A stage change out of "New Reservation" only happens after a staff member actually calls and confirms — that's the same signal that stops the escalation ladder above
This makes stage change a reliable trust boundary: everything after it can be treated as human-verified, not a raw form submission
Calendar logging should sit right on that boundary — never earlier — so the calendar only ever reflects verified bookings, not unconfirmed or spam submissions
✓ What was actually built
Superseded the original Google Calendar plan — GHL's own Book Appointment action turned out to carry more useful structure (real times, native availability checking, status lifecycle) than an all-day placeholder event
Trigger: Pipeline Stage Changed → Branch: Calendar Event ID is empty → Book Appointment, using the customer's actual submitted date and time, not a guessed slot
A second, independent workflow (Appointment Status trigger → Find Opportunity) captures the new appointment's ID the moment it's created and writes it back onto the Opportunity as Calendar Event ID — required because a Pipeline-Stage-Changed trigger has no appointment context of its own to pull an ID from (see Part 4)
Cancellation trigger is scoped tightly on purpose: Pipeline Stage Changed, filtered to In Pipeline: Reservations AND Pipeline Stage: No Show/Cancelled — so the workflow only ever fires on the specific stage change that means "this reservation is cancelled," not on every stage change in the pipeline
Cancellation reads Calendar Event ID as a precondition — Calendar Event ID is not empty — before touching appointment status, so cancellation logic never fires against a reservation that was never actually booked
// part 4 — wiring up real appointments, and the bug that shipped wrong dates silently
With ownership and escalation solid, the last piece was turning a confirmed reservation
into an actual bookable appointment — not just a CRM card. This is where the automation
found its most dangerous bug yet: one that didn't throw an error at all.
Bug 07
A locked date field that silently corrupted appointment dates — no error, wrong booking
⚠ What happened
GHL's Book Appointment action strictly requires MM-DD-YYYY or DD-MMM-YYYY — nothing else
The form's "Select Date" field is a native Date picker custom field type, which internally stores YYYY-MM-DD regardless of what format the form widget displays to the customer
Feeding that field straight into Book Appointment produced two different failure modes depending on the date, not one consistent error
⚠ The two failure modes
Loud failure: action skipped outright with "Invalid date format" — annoying, but safe, since no appointment was created
Silent failure: several test bookings "succeeded" with no error at all — but the resulting date was wrong in a consistent, non-random way (see table and pattern below)
Contact
Submitted date
Actually booked
Outcome
Test1
07-11-2026
Nov 20, 2026
Silently wrong — no error thrown
Test2
07-09-2026
Sep 20, 2026
Silently wrong — no error thrown
Test3
07-10-2026
Oct 20, 2026
Silently wrong — no error thrown
Test4
07-14-2026
—
✓ Caught — "Invalid date format," skipped
// the silent failures were strictly worse than the loud ones — a skipped booking is annoying but visible; a booking on the wrong month reads as a completely normal, confirmed reservation until a customer shows up on the wrong day
Bug 07 — cont'd
The real failure pattern, once cross-checked
⚠ Not a simple swap
First assumption was a day/month swap — but the numbers don't support that: 07-11-2026 booked to Nov 20, not Nov 7 as a true swap would produce
The actual pattern: the resulting month consistently matched the day originally submitted (11 → Nov, 09 → Sep, 10 → Oct), and the resulting day was consistently 20 in every case
That "20" lines up with the leading digits of the year (2026) — pointing to a positional misread of the raw YYYY-MM-DD string rather than a clean field swap
→ Best-available explanation
Most likely cause: the raw YYYY-MM-DD value was being parsed against a shorter expected format before the formatter step was correctly wired in, causing fixed character positions to be read against the wrong parts of the string
The exact byte-level mechanism inside GHL's parser wasn't fully diagnosable from the tools available (see dead ends below) — but the pattern was consistent and reproducible across multiple tests, which is stronger evidence than a one-off anomaly would be
The fix (explicit Date/time formatter, correctly wired) eliminated the failure regardless of the precise internal cause
Bug 07 — cont'd
The platform's own logs weren't enough to root-cause it
⚠ The dead ends
Execution Logs show pass/fail status and an error message per step, but never the actual resolved input value that was attempted — no way to see what string a step actually tried to parse
The form's own "Submissions" export showed clean, correctly-formatted dates — which made the bug look like it shouldn't have been possible at all
Attempting to change the field's type from Date picker to Text directly was blocked — GHL disables the Field Type dropdown once a custom field is in use
→ What actually found it
Bypassed the logs entirely and read the raw value straight off the contact record's custom field — this was the only place the true stored value (not a display-formatted version of it) was visible
That confirmed the field's real internal format was YYYY-MM-DD, not the long-form date shown anywhere else in the UI
Bug 07 — the fix
Format at the boundary, don't fight the field
✓ The fix
Added GHL's built-in Date/time formatter action directly into the booking workflow, between Branch and Book Appointment
Configured FROM: YYYY-MM-DD → TO: MM-DD-YYYY, converting the field's true internal format into the one Book Appointment actually requires
Repointed Book Appointment's Start Date & Time field at the formatter's output instead of the raw custom field
⚠ One more snag before it worked
The formatter action was initially added to the canvas but never actually inserted into the connected execution path — it existed, but nothing ran through it
Confirmed via Execution Logs, which showed Book Appointment still failing with zero trace of the formatter step ever running
Fix: re-added the action directly on the connector line between Branch and Book Appointment, verified with a fresh test that the formatter step now appears and executes before Book Appointment does
// re-tested with a brand-new contact after the fix — Book Appointment executed successfully and the calendar entry matched the submitted date exactly, closing out the bug
// part 5 — calendar type mismatch, and closing the loop on failed bookings
With appointment creation working for the happy path, two more gaps surfaced: what
happens when GHL rejects the calendar type itself, and what happens when a real slot
conflict occurs — no available time, not just a formatting error.
Bug 08
Class Booking calendars silently rejected valid appointments
⚠ What happened
Chief Sizzling Grill — Reservations was originally configured as a Class Booking calendar type
Book Appointment calls against it failed consistently, while a separate Personal calendar on the same sub-account booked correctly using the same workflow logic
Same trigger, same Format Date step, same Book Appointment action — the only variable was calendar type, which pointed the fault away from the workflow and onto the calendar's own configuration
✓ The fix
Switched Chief Sizzling Grill — Reservations from Class Booking to Personal calendar type
Reservation bookings began succeeding immediately, with no other change to the workflow
Slot capacity was reconfigured directly on the calendar to 10 bookings per slot — an intentional setting, not a leftover default, matching a restaurant reservation's real capacity (a time slot serving multiple parties, not one booking per moment)
Built & Tested
Closing the loop when no slot is available
→ The reasoning
Book Appointment can fail for a reason that isn't a bug at all — the requested date/time is genuinely full (all 10 slots taken)
A Wait step after Book Appointment allows the action time to resolve before the workflow checks whether it actually succeeded
Calendar Event ID is used as the single source of truth for success — if the appointment was created, GHL returns an ID; if not, that field stays empty
✓ What was actually built
Condition after Wait: Calendar Event ID is empty?
Empty (failed — no available slot): Email sent to the customer explaining the requested time is unavailable, with a direct link to the booking widget so they can pick an open slot themselves — then Update Opportunity to Lost, removing it from the active pipeline
Not empty (success): confirmation Email sent, opportunity remains active in the pipeline
This keeps the failure case from going silent — the customer always gets an answer and a next step, and the pipeline never carries a phantom "still open" opportunity for a booking that never actually happened
// this replaces an earlier assumption — that any Book Appointment failure meant a formatting bug (Bug 07) — with a proper distinction between "the workflow is broken" and "the workflow is working correctly and the slot is genuinely full"
// why reservations aren't auto-closed
Once the Calendar step was designed, an obvious next question came up: could the system
automatically detect when a visit is over, and close the reservation itself — instead of
relying on staff to remember? Worth documenting the reasoning for not
building that, rather than leaving it looking like an oversight.
⚠ The honest limit
GHL has no sensor at the restaurant — it cannot know a visit actually happened, only assume it based on some signal
A time-based auto-close (e.g. "mark Completed a few hours after the reservation time") is the closest thing to automatic, but it's a guess, not a detection — a no-show would get marked Completed exactly the same way a real visit would
The only genuinely automatic signal would be an external event feeding into GHL — most realistically, a payment confirmation from a connected POS/payment system
→ Why that doesn't apply here
Customers pay in cash at the table — a payment that never touches any system connected to GHL
A payment-based auto-close only works if the money itself passes through a system GHL can see; cash paid directly to staff leaves no digital trace to trigger off of
Building a POS integration would be solving for a signal that doesn't exist in this business's actual payment flow — engineering effort spent on a problem the real world doesn't hand you
// conclusion: for this specific business, closing a reservation is correctly a human decision, not a gap to be automated away — the existing "Calendar Event ID already exists" safety-net (below) is the honest, proportionate handling for when that human step gets missed, rather than building an auto-close system to chase a signal that isn't there
// part 6 — review response routing & the Reviews AI safeguard
A separate piece, built after the booking system was stable: what happens once a real
review actually comes in. Rather than requesting reviews (that's Stage 2, covered in
Part 7), this system reacts to inbound reviews — surfacing the
ones that need urgent attention, while letting GHL's native Reviews AI handle the
drafting work.
The trigger itself, New Review Received, is a native GHL trigger tied
to whichever review platforms are connected on the account — in this build, Google
(Facebook is also supported natively). It doesn't poll or check anything on a schedule;
it fires automatically the moment someone leaves a review directly on the connected
Google or Facebook business profile, and GHL pulls that review in as an event. This is
a distinct system from the Stage 2 review request in Part 7 — this trigger
reacts to a review a customer already posted externally, it doesn't send anything out.
// the workflow only surfaces urgency for negative reviews on purpose — see reasoning below
Limitation
"Review Received" is contactless — no direct message to the reviewer is possible
⚠ What this means
GHL states this explicitly in the trigger description: reviews aren't linked to a specific contact record
Standard customer-facing actions (Send Email, Send SMS to a contact) don't apply here — there's no contact for them to resolve against
✓ The design response
Used Internal Notification instead — a native action built specifically for alerting staff/owner, with no contact dependency
The workflow's job was reframed correctly: not "reply to the reviewer" (that's a separate, manual step — see below), but "tell a human a reply needs attention"
Design decision
One branch, not three — narrowing "notify" down to what's actually urgent
→ The reasoning
An earlier draft of this workflow branched into three paths — Positive (4-5★), Neutral (3★), Negative (1-2★) — each with its own notification
All three notifications would fire on every single review, regardless of how much attention each one actually warranted — a 5-star review doesn't need the same urgency as a 1-star one
Collapsed to a single condition: Rating ≤ 2. Everything else ends the workflow with no action
✓ Why this is the stronger design
Reviews AI drafts a reply automatically for every review regardless of rating — the branching was never gating whether a draft exists, only whether the owner needs to be interrupted about it
Positive/neutral reviews can be checked at the owner's own pace inside Reputation → Reviews; negative ones get pushed to them immediately
Not every event needs an automated alert — deciding what's worth interrupting a business owner for is itself part of the design, not a shortcut
Reviews AI configuration
Suggestive mode — AI drafts automatically, a human sends manually
→ The three modes available
Auto Responses: AI generates and posts a reply with zero human involvement
Suggestive: AI generates a draft the moment a review comes in — a human must click to review, edit, and send it before anything goes public
Off: no AI assistance, fully manual
✓ The choice, and why
Set to Suggestive under Reputation → Settings → Reviews AI, at Settings → Reputation in the sub-account
Auto-replying to negative reviews unsupervised is a real reputational risk — an angry customer getting an AI-generated reply with no human review is worse than a slightly delayed, considered response
Precise split: reply generation is automatic, reply sending is manual — the speed benefit of not writing from scratch, without losing control over what's published under the business's name
Pricing: $0.01 per AI-generated response (first 3 free), enabled at the agency level first, then toggled per sub-account
Limitation
Reviews AI isn't a workflow action — it only lives in the Reputation tab
⚠ The platform ceiling
There's no native workflow action to "generate an AI reply" or "auto-post an AI reply" that can be dropped into the automation canvas
Reviews AI operates independently, surfaced only inside Reputation → Reviews, disconnected from anything built in the Workflow Builder
→ What this means in practice
The workflow and Reviews AI are two separate systems that meet at one point: the internal notification tells a human to go check the Reputation tab, where the AI draft is already waiting
Not a single continuous automated pipeline — routing and alerting is automated, drafting is automated, but the connection between them currently requires a manual step
Safety check
Confirmed sandbox isolation before enabling anything
⚠ Why this mattered
Chief Sizzling Grill is a real, physical small eatery — not purely fictional — which raised a real question: could this sandbox build accidentally touch its actual public presence?
The eatery does appear as an unclaimed pin on Google Maps, but has no verified Google Business Profile, no owner account, and no connection to the Google account used for this GHL build
✓ Verified before proceeding
Checked business.google.com directly under the build's Google account — no existing listing, confirming GHL's "connected" status referred only to the linked Google account, not a live business profile
Confirmed the real eatery's Google presence (if any) sits under a separate, unrelated Google account — no shared access, no possibility of this sandbox workflow touching real public reviews
Workflow left in Draft, never published — combined with the isolation above, nothing here can fire against real customers or incur real cost
With booking (Stage 1) and inbound review routing (Part 6) both built, the remaining
piece was Stages 2 and 3: getting a review request out after the visit, then following
up afterward regardless of whether that request converts. This section documents the
sequence as built, triggered off the same Pipeline Stage Changed event used in Part 3,
with the Calendar Event ID field anchoring the Wait 2 timing calculation. Tested
end-to-end with a live submission.
Step
Purpose
Timing
Email 1
Booking confirmation
Immediately after successful booking
Wait 1
Relative wait
Until 1 day before appointment date
Email 2
Pre-visit reminder
Fires at the Wait 1 mark
Wait 2
Relative wait
Until 2 hours after appointment date/time
Send review request
Native GHL action
Fires post-visit
Update opportunity
Status → Won
Immediately after review request
Wait 3
Fixed wait
5 days
Email 3
General retention follow-up
Fires after Wait 3
Design decision
Email 3 is review-agnostic on purpose
→ The reasoning
Email 3 is written to work whether or not the customer filled out the review request, and regardless of what rating they gave
Branching Email 3's copy on review-completion status or star rating would require the workflow to wait on and read review outcome data before sending — extra state to track for a message that doesn't strictly need it
✓ Why this is the stronger design
A single, review-agnostic retention message avoids conditional branching entirely — fewer paths through the workflow, fewer places for logic to drift out of sync (the same lesson as Bug 04's shared-chain fix in Part 3)
Consistent with the KISS approach used elsewhere in this build: don't add a decision point unless the message genuinely needs to differ on either side of it
Design note
Update Opportunity → Won sits before the retention wait, not after
→ The reasoning
The review request firing is the signal that the visit is considered complete — the same "human/system-confirmed" logic used for the Calendar trust boundary in Part 3, applied one step further downstream
Marking the opportunity Won at that point (rather than waiting for Email 3 to fire) keeps the pipeline's status accurate in near-real-time instead of leaving a completed visit sitting in an ambiguous "still open" state for 5 more days
✓ Verified
Tested end-to-end with a real submission — timing behavior, including the Wait 2 offset against the Calendar Event's actual start time, resolved correctly
// built and tested as a workflow triggered off the same Pipeline Stage Changed event used in Part 3, sharing the existing Calendar Event ID field as the anchor for the Wait 2 timing calculation
// known limitations — documented honestly
Named on purpose, not glossed over, as part of the learning process.
Capacity is enforced directly on the calendar (10 bookings per slot on the Personal-type calendar, see Bug 08), rather than a custom counter built in the workflow — a naive counter would be vulnerable to phantom slots from skipped or duplicate runs, so letting GHL's own calendar engine own capacity was the more reliable choice
Status field integrity is an assumption, not a guarantee — the Found/Not Found logic depends on staff actually moving completed reservations out of "Open." Skip that consistently, and every past customer eventually reads as a false "Found." If this happens, the stale Calendar Event ID left on that Opportunity can also cause a genuinely new reservation to silently skip calendar creation — the same safety-net notification below is the catch for this case too
Cancellation still resolves to the contact's most recent appointment when it updates status — GHL's native Update Appointment Status action doesn't accept a specific appointment ID as input, only "the appointment that triggered this workflow" or "most recent for this contact." This is a real platform ceiling, not a workaround-able config choice. In practice it's a narrower risk than it sounds: Bug 02's Pipeline/Status filters plus Bug 03's phone-fallback contact matching mean a contact can only ever have one open reservation Opportunity at a time, so the automated booking path never creates a second candidate appointment for "most recent" to choose between — there's nothing to disambiguate. The one way this could still resolve to the wrong appointment is a staff member manually creating an unrelated appointment for that same contact outside the reservation flow — a training/process gap, not something workflow logic can close. The Calendar Event ID capture built in Part 4 doesn't mechanically enable precise ID-targeting, but it gives the record a verifiable, auditable link between Opportunity and appointment, and puts the ID in place for a future API-based upgrade, where GHL's API (unlike the visual builder) does support targeting a specific appointment ID directly
The "Review Received" trigger is contactless by design — no reservation-linked review data, and no way to message a reviewer directly through this workflow. Internal Notification was the correct native substitute, not a workaround
Reviews AI's automatic drafting happens the moment a review is received, regardless of rating — the workflow's branching controls notification urgency only, not whether a draft gets created. Positive/neutral review drafts still require a manual check-in at the owner's own pace, since no alert fires for them
Gemini AI, Sheets, and Gmail integrations on the broader scenario are still in progress
Rapid-fire, millisecond-level duplicate submissions were deprioritized as low-probability, since the form resets and requires full re-entry after each submission
The escalation ladder has since been execution-tested directly — canvas tracing was confirmed against a live run showing the correct branch entered and exited. Actual email delivery still can't be verified end-to-end, since the builder sub-account has live email sending blocked for sandbox agencies; branching and merge logic are confirmed, delivery is not
Book Appointment's date input depends on a Date/time formatter step staying correctly wired between Branch and Book Appointment (see Bug 07) — since GHL silently no-ops if that connection is ever broken by a future edit, this join is the single most fragile point in the booking workflow and is worth spot-checking after any change to that canvas
// what this demonstrates
The common thread across all of this: reading past a tool's surface-level label to verify
actual configured behavior, stress-testing edge cases instead of just the happy path, and
treating a "successful" execution as suspect, not just a failed one — Bug 07's wrong-date
bookings never threw an error, so verifying output against input mattered as much as
watching for failures. Part 6 adds a second, related thread: knowing where to draw the
line between automation and human judgment isn't a limitation to route around — the
decision to keep AI reply-sending manual, and to only interrupt the owner for genuinely
urgent cases, is itself a deliberate design choice, not a shortfall.
// tools used
GoHighLevelGoHighLevel Native FormsGHL Workflow BuilderGHL Reputation Management / Reviews AIGoogle Calendar (linked for staff-facing sync — bookings created via GHL's native Appointment system automatically appear here; not part of the workflow logic itself)Google SheetsGmail