Appointment Self-Booking Portal
This books against one calendar
The pattern here offers the free time on a single calendar and writes the appointment to it. It is not a scheduling product: there is no round robin across a team, no travel time, no working hours per person, no waiting list. Those are all reachable from here, and none of them is in this page. Read Going further before you promise a customer anything wider.
Before you start
This use case has configuration prerequisites, including an Experience site with a public page and a guest user that is allowed to write the appointment. Make sure you review the Set up section of this page before building!
Learning focus points
Turning booked records into the disabled intervals a picker can read
Running a whole booking journey as a screen flow on a public page
Letting a visitor write an appointment without showing them the calendar
Objective
Stop booking appointments by email.
A customer asks for a call. Someone answers with three times. Two are gone by the time the customer reads the message, so they propose two more, and one of those collides with something that was booked an hour ago. Four messages later there is a meeting, and everybody has spent a day on a decision that takes fifteen seconds.
The information that would end this exists already. It is in the calendar. The customer simply cannot see it, and the only person who can is the one writing the emails.
Solution
A page the customer opens themselves, showing the free time and nothing else. They pick a slot, leave their name, and the appointment is in the calendar before they close the tab.
The free time is not a list anybody maintains. The flow reads what is already booked, turns each booking into an interval the picker treats as dead, and offers everything that is left.
How It Works (User Perspective)
The customer opens the booking page. No login, no account.
They see a week of half hour slots. The ones already taken are visibly there but cannot be clicked, so the page reads as a calendar rather than as a form that rejects them.
They pick a free slot and give a name and an email address.
The appointment is written to the calendar and confirmed on screen.

What we'll create
1 screen flow carrying the whole journey
1 query of what is already booked
1 loop turning those bookings into disabled intervals
1 Avonni Date Time Picker offering what is left
1 record create writing the appointment
1 public page on an Experience site to run it on
Set up
Prerequisites
A calendar to read and write
Standard
Eventrecords work, and so does a custom object. Whatever you use needs a start, an end and something to filter on, so the flow reads one person's bookings rather than the whole org's.Decide that filter before you build. It is the difference between a portal that offers a consultant's free time and one that offers the free time of everybody in the company at once.
An Experience site with a public page
A site with guest access, and a page that requires no login.
The guest user profile needs read access to the calendar object and create access to write the appointment. Grant exactly those two and nothing else.
The Avonni Flow Screen Components package
The Date Time Picker used here is a flow screen component. There is no Experience Builder component that does this on its own, which is why the whole journey is a flow rather than a page of components.
Build the flow
Read what is already booked
Add a Get Records on your calendar object. Filter it to the calendar this page books against, and sort by start time.
Keep this query narrow. A booking page that reads six months of history to draw one week is slow for no reason, and on a public page it is slow for everyone at once.
Create the collection the picker will read
The picker does not accept a list of records. It accepts a collection of intervals, each one a start and an end.
Create two variables of the Apex-Defined type DateTimePickerDisabledInterval, which ships with the package: one single value, and one collection. The single one is scratch space inside the loop, the collection is what the screen will read.
Turn each booking into an interval
Add a Loop over the records from the query, and inside it one Assignment with three lines: set the scratch variable's startDate from the record's start, set its endDate from the record's end, then add the scratch variable to the collection.
That is the whole translation. Nothing else in the flow has to know what a booking is.

Build the booking screen
Set the working window
Under Visible Dates and Times, set Start Time and End Time to the hours you actually take appointments, and Time Slot Duration to the length of one appointment. Every slot on the page is generated from these three values.
Set Start Date and End Date to the window you are willing to be booked in, and Default Visible Date to a day inside it.
Do not skip that last one. If the picker opens on a week that falls entirely outside the start and end dates, it does not show an empty week, it fails with a component error.
Attach the busy intervals
Set Disabled Intervals to the collection the loop built.
This is the step that gets missed. Disabled Intervals looks exactly like the three fields above it, which are lists you fill in by hand, and it is the only one here holding a reference. Everything on this page depends on it being a reference: fill it in by hand and you have published a calendar that was accurate on the day you built it.
Turn on Show Disabled Dates so the taken slots stay visible. A booked slot that disappears looks like a slot that never existed, and the customer wonders why Tuesday morning is missing.

Write the appointment
Create the record
Add a Create Records on your calendar object. The start comes from the picker's value, the length is the slot duration you set, and the subject is worth building from the visitor's name so the calendar entry reads as something rather than as Appointment.
Set the same field the Get Records filters on, or the booking you just wrote will not be treated as busy the next time somebody opens the page.
Put it on the site
Going further
Close the gap between picking and booking. Two people can open the page at the same time and pick the same slot, because the intervals were built when each page loaded. Before the Create Records, add a second Get Records on that exact slot and route to an apology screen if anything comes back. It costs one decision and it is the difference between a booking page and a booking page you can leave running.
Book across a team. The pattern extends by changing what the first query returns. Read several calendars, build the intervals from all of them, and a slot is offered only when everybody is free. Round robin is the same query with a different rule about who gets written into the record.
Leave room between appointments. Widen each interval as you build it, rather than trying to express a gap in the picker. Fifteen minutes on each side of a booking is fifteen minutes nobody can book against.
Let people change their mind. A cancel or reschedule link is the same flow with the record in hand, and it removes most of the email that this page was built to end.
Related components
Date Time Picker
Scoped Notification
Was this helpful?
