Emails each client booked for tomorrow a spoken reminder as an audio attachment, and never reminds the same booking twice.

UsesClaudeCal.comElevenLabsGmailGoogle Sheets

Voice-note reminders for tomorrow's appointments

ScheduleRun every day at 09:00.Run every day at 09:00.
ValuesSet logSheetUrl to blank.Set logSheetUrl to blank.
ValuesSet logTabName to Reminder log.Set logTabName to Reminder log.
ValuesSet voiceId to blank.Set voiceId to blank.
ValuesSet businessName to blank.Set businessName to blank.
ValuesSet maxReminders to 30.Set maxReminders to 30.
ValuesSet remindFromHours to 15.Set remindFromHours to 15.
ValuesSet remindToHours to 39.Set remindToHours to 39.
ValuesSet bookingsToScan to 100.Set bookingsToScan to 100.
ListsStart an empty list and save it as remindedBookingIds.Start an empty list and save it as remindedBookingIds.
Decisions & LoopsIf a step below fails, save the error as logReadErrorIf a step below fails, save the error as logReadError
Try
ValuesChange remindedBookingIds 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.
On error
Decisions & LoopsStop and report a problem: Could not read the reminder log, so nothing was sent - without it this workflow cannot tell who has already been reminded. Add a tab named '@logTabName' to your spreadsheet with 'Booking ID' as the heading in column A.Stop and report a problem: Could not read the reminder log, so nothing was sent - without it this workflow cannot tell who has already been reminded. Add a tab named '@logTabName' to your spreadsheet with 'Booking ID' as the heading in column A.
Cal.comCal.comList bookings. Save it as bookingSearch.List bookings. Save it as bookingSearch.
ValuesSet bookings to bookingSearch.bookings.Set bookings to bookingSearch.bookings.
ValuesSet sentCount to 0.Set sentCount to 0.
ValuesSet skippedAlreadyReminded to 0.Set skippedAlreadyReminded to 0.
ValuesSet skippedNoEmail to 0.Set skippedNoEmail to 0.
ValuesSet skippedIncomplete to 0.Set skippedIncomplete to 0.
ValuesSet skippedUnverifiedScript to 0.Set skippedUnverifiedScript to 0.
ValuesSet failedCount to 0.Set failedCount to 0.
ValuesSet remindersTried to 0.Set remindersTried to 0.
ValuesSet skippedOverLimit to 0.Set skippedOverLimit to 0.
ValuesSet notLogged to 0.Set notLogged to 0.
ValuesSet problems to an empty list.Set problems to an empty list.
Decisions & LoopsFor each booking in bookings:For each booking in bookings:
ValuesSet bookingId to blank.Set bookingId to blank.
Decisions & LoopsIf a step below fails, save the error as idErrorIf a step below fails, save the error as idError
Try
TextTrim booking.uid and save it as bookingId.Trim booking.uid and save it as bookingId.
On error
ValuesChange bookingId to blank.Change bookingId to blank.
Decisions & LoopsIf bookingId is emptyIf bookingId is empty
Yes
ValuesAdd 1 to skippedIncomplete.Add 1 to skippedIncomplete.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
Decisions & LoopsIf remindedBookingIds contains bookingIdIf remindedBookingIds contains bookingId
Yes
ValuesAdd 1 to skippedAlreadyReminded.Add 1 to skippedAlreadyReminded.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet hoursAway to -1.Set hoursAway to -1.
Decisions & LoopsIf a step below fails, save the error as startErrorIf a step below fails, save the error as startError
Try
Dates & TimesCount the hours from the current date and time to booking.start and save it as hoursAway.Count the hours from the current date and time to booking.start and save it as hoursAway.
On error
ValuesChange hoursAway to -1.Change hoursAway to -1.
Decisions & LoopsIf 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โ€ฆ
Yes
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet appointmentTitle to blank.Set appointmentTitle to blank.
Decisions & LoopsIf a step below fails, save the error as titleErrorIf a step below fails, save the error as titleError
Try
TextTrim booking.title and save it as appointmentTitle.Trim booking.title and save it as appointmentTitle.
On error
ValuesChange appointmentTitle to blank.Change appointmentTitle to blank.
Decisions & LoopsIf appointmentTitle is emptyIf appointmentTitle is empty
Yes
ValuesAdd 1 to skippedIncomplete.Add 1 to skippedIncomplete.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet clientEmail to blank.Set clientEmail to blank.
Decisions & LoopsIf a step below fails, save the error as emailErrorIf a step below fails, save the error as emailError
Try
TextTrim the first item of booking.attendees.email and save it as clientEmail.Trim the first item of booking.attendees.email and save it as clientEmail.
On error
ValuesChange clientEmail to blank.Change clientEmail to blank.
Decisions & LoopsIf clientEmail is a valid email address is noIf clientEmail is a valid email address is no
Yes
ValuesAdd 1 to skippedNoEmail.Add 1 to skippedNoEmail.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet clientName to blank.Set clientName to blank.
Decisions & LoopsIf a step below fails, save the error as nameErrorIf a step below fails, save the error as nameError
Try
TextTrim the first item of booking.attendees.name and save it as clientName.Trim the first item of booking.attendees.name and save it as clientName.
On error
ValuesChange clientName to blank.Change clientName to blank.
Decisions & LoopsIf clientName is emptyIf clientName is empty
Yes
ValuesChange clientName to there.Change clientName to there.
No
No steps
ValuesSet startsAt to blank.Set startsAt to blank.
Decisions & LoopsIf a step below fails, save the error as startsAtErrorIf a step below fails, save the error as startsAtError
Try
TextTrim booking.start and save it as startsAt.Trim booking.start and save it as startsAt.
On error
ValuesChange startsAt to blank.Change startsAt to blank.
Decisions & LoopsIf remindersTried is greater than or equal to maxReminders multiplied by 1If remindersTried is greater than or equal to maxReminders multiplied by 1
Yes
ValuesAdd 1 to skippedOverLimit.Add 1 to skippedOverLimit.
ListsAdd 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.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
TextSplit clientName by a space and save it as nameParts.Split clientName by a space and save it as nameParts.
TextGet the text before between in appointmentTitle and save it as appointmentName.Get the text before between in appointmentTitle and save it as appointmentName.
Decisions & LoopsIf appointmentName is blankIf appointmentName is blank
Yes
ValuesChange appointmentName to appointmentTitle.Change appointmentName to appointmentTitle.
No
No steps
ValuesSet clientTimeZone to blank.Set clientTimeZone to blank.
Decisions & LoopsIf a step below fails, save the error as timeZoneErrorIf a step below fails, save the error as timeZoneError
Try
TextTrim booking.attendees[0].timeZone and save it as clientTimeZone.Trim booking.attendees[0].timeZone and save it as clientTimeZone.
On error
ValuesChange clientTimeZone to blank.Change clientTimeZone to blank.
ValuesSet startTimeText to blank.Set startTimeText to blank.
Decisions & LoopsIf clientTimeZone isn't blankIf clientTimeZone isn't blank
Yes
Decisions & LoopsIf a step below fails, save the error as zoneErrorIf a step below fails, save the error as zoneError
Try
Dates & TimesMake startsAt 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.
TextBuild the text @localStart (@clientTimeZone) and save it as startTimeText.Build the text @localStart (@clientTimeZone) and save it as startTimeText.
On error
No steps
No
No steps
Decisions & LoopsIf startTimeText is blankIf startTimeText is blank
Yes
Dates & TimesMake startsAt 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.
No
No steps
ValuesSet requiredPhrase to your appointmentName is booked for tomorrow.Set requiredPhrase to your appointmentName is booked for tomorrow.
TextBuild 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.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.
ValuesChange prompt to promptChange prompt to prompt The script must contain this exact phrase, word for word: requiredPhrase.
ValuesAdd 1 to remindersTried.Add 1 to remindersTried.
ValuesSet script to blank.Set script to blank.
ValuesSet scriptProblem to blank.Set scriptProblem to blank.
Decisions & LoopsIf a step below fails, save the error as scriptErrorIf a step below fails, save the error as scriptError
Try
ClaudeClaudeAsk Claude: prompt. Save it as modelReply.Ask Claude: prompt. Save it as modelReply. Maximum response length: 200
TextTrim modelReply.response_text and save it as script.Trim modelReply.response_text and save it as script.
On error
ValuesChange script to blank.Change script to blank.
ValuesChange scriptProblem to scriptError.message.Change scriptProblem to scriptError.message.
Decisions & LoopsIf script doesn't contain requiredPhrase (ignoring case)If script doesn't contain requiredPhrase (ignoring case)
Yes
ValuesAdd 1 to skippedUnverifiedScript.Add 1 to skippedUnverifiedScript.
ListsAdd 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.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet audioRef to blank.Set audioRef to blank.
ValuesSet voiceProblem to blank.Set voiceProblem to blank.
Decisions & LoopsIf a step below fails, save the error as voiceErrorIf a step below fails, save the error as voiceError
Try
ElevenLabsElevenLabsConvert text to speech with voice voiceId. Save it as speech.Convert text to speech with voice voiceId. Save it as speech.script
TextTrim speech.document_ref and save it as audioRef.Trim speech.document_ref and save it as audioRef.
On error
ValuesChange audioRef to blank.Change audioRef to blank.
ValuesChange voiceProblem to voiceError.message.Change voiceProblem to voiceError.message.
Decisions & LoopsIf audioRef is emptyIf audioRef is empty
Yes
ValuesAdd 1 to failedCount.Add 1 to failedCount.
ListsAdd 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.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesSet emailSent to false.Set emailSent to false.
Decisions & LoopsIf a step below fails, save the error as sendErrorIf a step below fails, save the error as sendError
Try
GmailGmailSend an email to clientEmail.Send an email to clientEmail. Subject: Reminder: appointmentName tomorrowscript Your appointment starts startTimeText. The same reminder is attached as a short voice note. businessName
ValuesAdd 1 to sentCount.Add 1 to sentCount.
ValuesChange emailSent to true.Change emailSent to true.
On error
ValuesAdd 1 to failedCount.Add 1 to failedCount.
ListsAdd 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.
Decisions & LoopsIf emailSent is noIf emailSent is no
Yes
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ListsAdd bookingId to the end of remindedBookingIds and save it as remindedBookingIds.Add bookingId to the end of remindedBookingIds and save it as remindedBookingIds.
Decisions & LoopsIf a step below fails, save the error as logWriteErrorIf a step below fails, save the error as logWriteError
Try
Google SheetsGoogle SheetsAdd a row to logTabName in logSheetUrl.Add a row to logTabName in logSheetUrl.
On error
ValuesAdd 1 to notLogged.Add 1 to notLogged.
ListsAdd 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.
TextBuild 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.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.
TextJoin problems with a new line and save it as problemList.Join problems with a new line and save it as problemList.
Decisions & LoopsIf problems isn't emptyIf problems isn't empty
Yes
Decisions & LoopsStop and report a problem: summaryStop and report a problem: summary problemList.
No
No steps
ValuesShow in the output: summary.Show in the output: summary.