Emails each client booked for tomorrow a spoken reminder as an audio attachment, and never reminds the same booking twice.
Uses
Claude
Cal.comElevenLabs
Gmail
Google Sheets
Voice-note reminders for tomorrow's appointments
Every day at 09:00, your upcoming Cal.com bookings are read and the ones starting tomorrow are picked out. For each client, the AI writes a short reminder, an ElevenLabs voice you choose speaks it, and the client gets an email with the words, their start time and the audio attached.
The spoken note says very little: a greeting with the client's first name, the name of their appointment and your business name. It never says a time, a date, a price or an address, because a voice note naming the wrong time is worse than none. The start time is written in the email instead, in the client's own time zone from their booking, and a script that has lost the appointment's name is not sent. Each booking is written to a 'Reminder log' tab once its email has gone out and every run reads that log first, so nobody is reminded twice; if the log cannot be read, nothing is sent. If a script, voice note or email fails, or there are more clients than your daily limit, the run is marked as failed and names everyone who did not get a reminder.
You need Cal.com, ElevenLabs, Gmail, Google Sheets and Anthropic connected, and a Google Sheet with a 'Reminder log' tab. Fill in the sheet's link, the voice and the name clients know you by.
logSheetUrl to blank.Set logSheetUrl to blank.logTabName to Reminder log.Set logTabName to Reminder log.voiceId to blank.Set voiceId to blank.businessName to blank.Set businessName to blank.maxReminders to 30.Set maxReminders to 30.remindFromHours to 15.Set remindFromHours to 15.remindToHours to 39.Set remindToHours to 39.bookingsToScan to 100.Set bookingsToScan to 100.remindedBookingIds.Start an empty list and save it as remindedBookingIds.logReadErrorIf a step below fails, save the error as logReadErrorremindedBookingIds to the values from the Google Sheets โRead column by numberโ step.Change remindedBookingIds to the values from the Google Sheets โRead column by numberโ step.bookingSearch.List bookings. Save it as bookingSearch.bookings to bookingSearch.bookings.Set bookings to bookingSearch.bookings.sentCount to 0.Set sentCount to 0.skippedAlreadyReminded to 0.Set skippedAlreadyReminded to 0.skippedNoEmail to 0.Set skippedNoEmail to 0.skippedIncomplete to 0.Set skippedIncomplete to 0.skippedUnverifiedScript to 0.Set skippedUnverifiedScript to 0.failedCount to 0.Set failedCount to 0.remindersTried to 0.Set remindersTried to 0.skippedOverLimit to 0.Set skippedOverLimit to 0.notLogged to 0.Set notLogged to 0.problems to an empty list.Set problems to an empty list.booking in bookings:For each booking in bookings:bookingId to blank.Set bookingId to blank.idErrorIf a step below fails, save the error as idErrorbooking.uid and save it as bookingId.Trim booking.uid and save it as bookingId.bookingId to blank.Change bookingId to blank.bookingId is emptyIf bookingId is emptyskippedIncomplete.Add 1 to skippedIncomplete.remindedBookingIds contains bookingIdIf remindedBookingIds contains bookingIdskippedAlreadyReminded.Add 1 to skippedAlreadyReminded.hoursAway to -1.Set hoursAway to -1.startErrorIf a step below fails, save the error as startErrorbooking.start and save it as hoursAway.Count the hours from the current date and time to booking.start and save it as hoursAway.hoursAway to -1.Change hoursAway to -1.hoursAway multiplied by 1 is less than remindFromHours multiplied by 1 orโฆIf hoursAway multiplied by 1 is less than remindFromHours multiplied by 1 orโฆappointmentTitle to blank.Set appointmentTitle to blank.titleErrorIf a step below fails, save the error as titleErrorbooking.title and save it as appointmentTitle.Trim booking.title and save it as appointmentTitle.appointmentTitle to blank.Change appointmentTitle to blank.appointmentTitle is emptyIf appointmentTitle is emptyskippedIncomplete.Add 1 to skippedIncomplete.clientEmail to blank.Set clientEmail to blank.emailErrorIf a step below fails, save the error as emailErrorbooking.attendees.email and save it as clientEmail.Trim the first item of booking.attendees.email and save it as clientEmail.clientEmail to blank.Change clientEmail to blank.clientEmail is a valid email address is noIf clientEmail is a valid email address is noskippedNoEmail.Add 1 to skippedNoEmail.clientName to blank.Set clientName to blank.nameErrorIf a step below fails, save the error as nameErrorbooking.attendees.name and save it as clientName.Trim the first item of booking.attendees.name and save it as clientName.clientName to blank.Change clientName to blank.clientName is emptyIf clientName is emptyclientName to there.Change clientName to there.startsAt to blank.Set startsAt to blank.startsAtErrorIf a step below fails, save the error as startsAtErrorbooking.start and save it as startsAt.Trim booking.start and save it as startsAt.startsAt to blank.Change startsAt to blank.remindersTried is greater than or equal to maxReminders multiplied by 1If remindersTried is greater than or equal to maxReminders multiplied by 1skippedOverLimit.Add 1 to skippedOverLimit.clientName (clientEmail): not reminded, this run reached its limit of maxReminders reminders - raise 'maxReminders' to cover your busiest day to the end of problems and save it as problems.Add clientName (clientEmail): not reminded, this run reached its limit of maxReminders reminders - raise 'maxReminders' to cover your busiest day to the end of problems and save it as problems.clientName by a space and save it as nameParts.Split clientName by a space and save it as nameParts. between in appointmentTitle and save it as appointmentName.Get the text before between in appointmentTitle and save it as appointmentName.appointmentName is blankIf appointmentName is blankappointmentName to appointmentTitle.Change appointmentName to appointmentTitle.clientTimeZone to blank.Set clientTimeZone to blank.timeZoneErrorIf a step below fails, save the error as timeZoneErrorbooking.attendees[0].timeZone and save it as clientTimeZone.Trim booking.attendees[0].timeZone and save it as clientTimeZone.clientTimeZone to blank.Change clientTimeZone to blank.startTimeText to blank.Set startTimeText to blank.clientTimeZone isn't blankIf clientTimeZone isn't blankzoneErrorIf a step below fails, save the error as zoneErrorstartsAt readable as โMonday, November 2 at 2:05 PMโ and save it as localStart.Make startsAt readable as โMonday, November 2 at 2:05 PMโ and save it as localStart.@localStart (@clientTimeZone) and save it as startTimeText.Build the text @localStart (@clientTimeZone) and save it as startTimeText.startTimeText is blankIf startTimeText is blankstartsAt readable as โMonday, November 2 at 2:05 PM (UTCZ)โ and save it as startTimeText.Make startsAt readable as โMonday, November 2 at 2:05 PM (UTCZ)โ and save it as startTimeText.requiredPhrase to your appointmentName is booked for tomorrow.Set requiredPhrase to your appointmentName is booked for tomorrow.You are writing a very short reminder that will be read aloud as a voice note to a client about an appointment. Write 2 to 3 short sentences in a warm, plain s... and save it as prompt.Build the text You are writing a very short reminder that will be read aloud as a voice note to a client about an appointment. Write 2 to 3 short sentences in a warm, plain s... and save it as prompt.prompt to promptChange prompt to prompt
The script must contain this exact phrase, word for word: requiredPhrase.remindersTried.Add 1 to remindersTried.script to blank.Set script to blank.scriptProblem to blank.Set scriptProblem to blank.scriptErrorIf a step below fails, save the error as scriptErrorprompt. Save it as modelReply.Ask Claude: prompt. Save it as modelReply.
Maximum response length: 200modelReply.response_text and save it as script.Trim modelReply.response_text and save it as script.script to blank.Change script to blank.scriptProblem to scriptError.message.Change scriptProblem to scriptError.message.script doesn't contain requiredPhrase (ignoring case)If script doesn't contain requiredPhrase (ignoring case)skippedUnverifiedScript.Add 1 to skippedUnverifiedScript.clientName (clientEmail): the AI's script did not name the appointment, so nothing was sent. scriptProblem to the end of problems and save it as problems.Add clientName (clientEmail): the AI's script did not name the appointment, so nothing was sent. scriptProblem to the end of problems and save it as problems.audioRef to blank.Set audioRef to blank.voiceProblem to blank.Set voiceProblem to blank.voiceErrorIf a step below fails, save the error as voiceErrorvoiceId. Save it as speech.Convert text to speech with voice voiceId. Save it as speech.scriptspeech.document_ref and save it as audioRef.Trim speech.document_ref and save it as audioRef.audioRef to blank.Change audioRef to blank.voiceProblem to voiceError.message.Change voiceProblem to voiceError.message.audioRef is emptyIf audioRef is emptyfailedCount.Add 1 to failedCount.clientName (clientEmail): ElevenLabs could not make the voice note, so nothing was sent. voiceProblem to the end of problems and save it as problems.Add clientName (clientEmail): ElevenLabs could not make the voice note, so nothing was sent. voiceProblem to the end of problems and save it as problems.emailSent to false.Set emailSent to false.sendErrorIf a step below fails, save the error as sendErrorclientEmail.Send an email to clientEmail.
Subject: Reminder: appointmentName tomorrowscript
Your appointment starts startTimeText.
The same reminder is attached as a short voice note.
businessNamesentCount.Add 1 to sentCount.emailSent to true.Change emailSent to true.failedCount.Add 1 to failedCount.clientName (clientEmail): the email could not be sent - sendError.message to the end of problems and save it as problems.Add clientName (clientEmail): the email could not be sent - sendError.message to the end of problems and save it as problems.emailSent is noIf emailSent is nobookingId to the end of remindedBookingIds and save it as remindedBookingIds.Add bookingId to the end of remindedBookingIds and save it as remindedBookingIds.logWriteErrorIf a step below fails, save the error as logWriteErrorlogTabName in logSheetUrl.Add a row to logTabName in logSheetUrl.notLogged.Add 1 to notLogged.clientName (clientEmail): the reminder went out but could not be written to the '@logTabName' tab. Add bookingId to column A before running this again today, or they will get a second email to the end of problems and save it as problems.Add clientName (clientEmail): the reminder went out but could not be written to the '@logTabName' tab. Add bookingId to column A before running this again today, or they will get a second email to the end of problems and save it as problems.Voice reminders sent: @sentCount. Already reminded on an earlier run: @skippedAlreadyReminded. Skipped, no usable email address: @skippedNoEmail. Skipped, book... and save it as summary.Build the text Voice reminders sent: @sentCount. Already reminded on an earlier run: @skippedAlreadyReminded. Skipped, no usable email address: @skippedNoEmail. Skipped, book... and save it as summary.problems with a new line and save it as problemList.Join problems with a new line and save it as problemList.problems isn't emptyIf problems isn't emptysummaryStop and report a problem: summary
problemList.summary.Show in the output: summary.