Checks your deadline sheet each morning and emails you as each licence, permit, tax filing or renewal comes due, with a calendar event.
Uses
Gmail
Google Calendar
Google Sheets
Never miss a filing or renewal deadline
A missed licence renewal, permit, insurance policy or tax filing costs fines, cover or the right to trade. Every morning at 09:00, each filing in your tracker sheet is checked against today's date and its Status cell is set to Overdue, Due today, Due soon or Up to date, with the date it was checked alongside. When a filing moves into a new status you get an email about it - as it comes inside your warning window, on the day it is due, and once more if it goes overdue - so you are reminded three times at most rather than every morning. A calendar event is booked on the due date the first time a filing is seen, and again whenever its due date moves.
Finish a filing by moving its due date on, which resets the row and books the next event, or by typing your own word such as Filed into its Status cell, after which the row is left alone. A due date that cannot be read is marked 'Check due date' rather than skipped, a reminder that fails to send is tried again the next morning, and a row that cannot be written back fails the run, so a problem never passes for a quiet day.
Before the first run you need a sheet whose tab starts with the columns 'Filing', 'Due Date', 'Status', 'Last Checked', 'Calendar Event' and 'Event Date', with due dates written as YYYY-MM-DD, and you fill in only the first two.
trackerSheetUrl to blank.Set trackerSheetUrl to blank.trackerTab to Filings.Set trackerTab to Filings.reminderEmail to blank.Set reminderEmail to blank.timeZone to blank.Set timeZone to blank.dueSoonDays to 30.Set dueSoonDays to 30.calendarId to primary.Set calendarId to primary.eventStartTime to 09:00.Set eventStartTime to 09:00.eventMinutes to 30.Set eventMinutes to 30.dueSoonWindow to dueSoonDays multiplied by 1.Set dueSoonWindow to dueSoonDays multiplied by 1.dueSoonWindow is NaN or dueSoonWindow is less than 0If dueSoonWindow is NaN or dueSoonWindow is less than 0trackerSheetUrl and save it as sheetUrl.Trim trackerSheetUrl and save it as sheetUrl.trackerTab and save it as tab.Trim trackerTab and save it as tab.reminderEmail and save it as sendTo.Trim reminderEmail and save it as sendTo.timeZone and save it as zone.Trim timeZone and save it as zone.calendarId and save it as calendar.Trim calendarId and save it as calendar.eventStartTime and save it as startTime.Trim eventStartTime and save it as startTime.today to blank.Set today to blank.zoneErrorIf a step below fails, save the error as zoneErrortoday.Get today's date and save it as today.timeErrorIf a step below fails, save the error as timeErroreventMinutes multiplied by 1 minutes and save it as timeProbe.Move 2026-01-01T@startTime:00 by eventMinutes multiplied by 1 minutes and save it as timeProbe.tracker to an empty record.Set tracker to an empty record.readErrorIf a step below fails, save the error as readErrorsheetUrl. Save it as tracker.Read the rows of sheetUrl. Save it as tracker.readError.message. Check the link in the 'trackerSheetUrl' step and that the Google account connected to Lodol can open the sheet, then run it again. Nothing was changed or sent.Stop and report a problem: Could not read the tab '@tab' of your tracker spreadsheet: readError.message. Check the link in the 'trackerSheetUrl' step and that the Google account connected to Lodol can open the sheet, then run it again. Nothing was changed or sent.headers to tracker.headers.Set headers to tracker.headers.filings to tracker.rows.Set filings to tracker.rows.headers is emptyIf headers is emptyheadersOk to headers[0] is Filing and headers[1] is Due Date and headers[2] is Status and headers[3] is Last Checked and headers[4] is Calendar Event and headers[5] is Event Date.Set headersOk to headers[0] is Filing and headers[1] is Due Date and headers[2] is Status and headers[3] is Last Checked and headers[4] is Calendar Event and headers[5] is Event Date.headersOk is noIf headersOk is noheaders joined by , . Fix the headers and run it again - nothing was changed.Stop and report a problem: The tab '@tab' needs its first six columns headed exactly Filing, Due Date, Status, Last Checked, Calendar Event, Event Date, in that order. Its header row reads: headers joined by , . Fix the headers and run it again - nothing was changed.filings is emptyIf filings is emptycheckedCount to 0.Set checkedCount to 0.overdueCount to 0.Set overdueCount to 0.dueTodayCount to 0.Set dueTodayCount to 0.dueSoonCount to 0.Set dueSoonCount to 0.upToDateCount to 0.Set upToDateCount to 0.unreadableCount to 0.Set unreadableCount to 0.unreadableRows to an empty list.Set unreadableRows to an empty list.parkedCount to 0.Set parkedCount to 0.remindersSent to 0.Set remindersSent to 0.eventsBooked to 0.Set eventsBooked to 0.emailFailures to an empty list.Set emailFailures to an empty list.calendarFailures to an empty list.Set calendarFailures to an empty list.writeFailures to an empty list.Set writeFailures to an empty list.filing in filings:For each filing in filings:filing.Filing and save it as name.Trim filing.Filing and save it as name.filing["Due Date"] and save it as dueRaw.Trim filing["Due Date"] and save it as dueRaw.filing.Status and save it as oldStatus.Trim filing.Status and save it as oldStatus.filing["Calendar Event"] and save it as eventLink.Trim filing["Calendar Event"] and save it as eventLink.filing["Event Date"] and save it as eventDate.Trim filing["Event Date"] and save it as eventDate.rowNumber to filing.row_number.Set rowNumber to filing.row_number.name is blank and dueRaw is blankIf name is blank and dueRaw is blankname is blankIf name is blankname to Row rowNumber.Change name to Row rowNumber., Up to date, Due soon, Due today, Overdue, Check due date] doesn't…If [, Up to date, Due soon, Due today, Overdue, Check due date] doesn't…parkedCount.Add 1 to parkedCount.checkedCount.Add 1 to checkedCount.dueDate to blank.Set dueDate to blank.daysLeft to 0.Set daysLeft to 0.weekday to blank.Set weekday to blank.dateOk to false.Set dateOk to false.dueRaw isn't emptyIf dueRaw isn't emptydateErrorIf a step below fails, save the error as dateErrordueRaw read as a date in zone split by T and save it as dueDate.Get the first item of dueRaw read as a date in zone split by T and save it as dueDate.today to dueDate and save it as daysLeft.Count the days from today to dueDate and save it as daysLeft.dueDate and save it as weekday.Get the day of the week for dueDate and save it as weekday.dateOk to dueRaw multiplied by 1 is NaN.Change dateOk to dueRaw multiplied by 1 is NaN.dateOk to false.Change dateOk to false.newStatus to Check due date.Set newStatus to Check due date.dateOk is yesIf dateOk is yesdaysLeft is less than 0If daysLeft is less than 0newStatus to Overdue.Change newStatus to Overdue.overdueCount.Add 1 to overdueCount.newStatus to Due today.Change newStatus to Due today.dueTodayCount.Add 1 to dueTodayCount.newStatus to Due soon.Change newStatus to Due soon.dueSoonCount.Add 1 to dueSoonCount.newStatus to Up to date.Change newStatus to Up to date.upToDateCount.Add 1 to upToDateCount.unreadableCount.Add 1 to unreadableCount.name (row rowNumber) to the end of unreadableRows and save it as unreadableRows.Add name (row rowNumber) to the end of unreadableRows and save it as unreadableRows.name (row rowNumber): the due date '@dueRaw' could not be read. Write it as YYYY-MM-DD, for example 2026-10-15. The row is marked 'Check due date'; no reminder was sent and no calendar event was booked.Show in the output: name (row rowNumber): the due date '@dueRaw' could not be read. Write it as YYYY-MM-DD, for example 2026-10-15. The row is marked 'Check due date'; no reminder was sent and no calendar event was booked.wasStatus to oldStatus.Set wasStatus to oldStatus.wasStatus is blankIf wasStatus is blankwasStatus to new.Change wasStatus to new.eventBooked to false.Set eventBooked to false.dateOk is yes and daysLeft is greater than or equal to 0 and eventLink is…If dateOk is yes and daysLeft is greater than or equal to 0 and eventLink is…calendarErrorIf a step below fails, save the error as calendarErrorstartsAt to dueDateT@startTime:00.Set startsAt to dueDateT@startTime:00.startsAt shifted by eventMinutes multiplied by 1 minutes split by . and save it as endsAt.Get the first item of startsAt shifted by eventMinutes multiplied by 1 minutes split by . and save it as endsAt.name' from startsAt to endsAt. Save it as booking.Create the event 'Filing due: name' from startsAt to endsAt. Save it as booking.
Calendar: calendar
Time zone: zoneCompliance filing due: name
Due date: dueDate
Tracker: sheetUrl
Booked by the compliance tracker workflow. If the filing is done early, delete this event and update the tracker row.booking.html_link and save it as eventLink.Trim booking.html_link and save it as eventLink.eventDate to dueDate.Change eventDate to dueDate.eventBooked to true.Change eventBooked to true.eventsBooked.Add 1 to eventsBooked.name - calendarError.message to the end of calendarFailures and save it as calendarFailures.Add name - calendarError.message to the end of calendarFailures and save it as calendarFailures.name: calendarError.message. The row is still updated and the event will be tried again on the next run.Show in the output: Could not book a calendar event for name: calendarError.message. The row is still updated and the event will be tried again on the next run.statusCell to newStatus.Set statusCell to newStatus.sendReminder to false.Set sendReminder to false.newStatus is not oldStatus and newStatus is Overdue or newStatus is "Due…If newStatus is not oldStatus and newStatus is Overdue or newStatus is "Due…sendReminder to true.Change sendReminder to true.sendReminder is yesIf sendReminder is yesdayWord to days.Set dayWord to days.daysLeft is 1 or daysLeft is -1If daysLeft is 1 or daysLeft is -1dayWord to day.Change dayWord to day.subject to blank.Set subject to blank.opening to blank.Set opening to blank.closing to blank.Set closing to blank.newStatus is OverdueIf newStatus is OverdueoverdueBy to daysLeft multiplied by -1.Set overdueBy to daysLeft multiplied by -1.subject to Overdue: name was due on dueDate.Change subject to Overdue: name was due on dueDate.opening to name was due on weekday dueDate and is now overdueBy dayWord overdue.Change opening to name was due on weekday dueDate and is now overdueBy dayWord overdue.closing to This is the last reminder for this filing. Its row stays marked Overdue in the tracker until you move its due date on to the next one or type Filed into its Status cell..Change closing to This is the last reminder for this filing. Its row stays marked Overdue in the tracker until you move its due date on to the next one or type Filed into its Status cell..subject to Due today: name.Change subject to Due today: name.opening to name is due today, weekday dueDate.Change opening to name is due today, weekday dueDate.closing to If it is still open tomorrow morning, the tracker marks it Overdue and sends one more reminder..Change closing to If it is still open tomorrow morning, the tracker marks it Overdue and sends one more reminder..subject to Due in daysLeft dayWord: name.Change subject to Due in daysLeft dayWord: name.daysLeft is 1If daysLeft is 1subject to Due tomorrow: name.Change subject to Due tomorrow: name.opening to name is due in daysLeft dayWord, on weekday dueDate.Change opening to name is due in daysLeft dayWord, on weekday dueDate.closing to You will hear again on the day it is due, and once more if it goes overdue. When it is filed, move the row's due date on to the next one, or type Filed into its Status cell, and the tracker stops watching it..Change closing to You will hear again on the day it is due, and once more if it goes overdue. When it is filed, move the row's due date on to the next one, or type Filed into its Status cell, and the tracker stops watching it..details to Filing: nameSet details to Filing: name
Due date: weekday, dueDate
Tracker status: newStatus.eventLink isn't emptyIf eventLink isn't emptydetails to detailsChange details to details
Calendar event: eventLink.details to detailsChange details to details
Tracker: sheetUrl.emailBody to openingSet emailBody to opening
details
closing.sendErrorIf a step below fails, save the error as sendErrorsendTo.Send an email to sendTo.
Subject: subjectemailBodyremindersSent.Add 1 to remindersSent.name: wasStatus -> newStatus. Reminder sent to sendTo.Show in the output: name: wasStatus -> newStatus. Reminder sent to sendTo.name - sendError.message to the end of emailFailures and save it as emailFailures.Add name - sendError.message to the end of emailFailures and save it as emailFailures.statusCell to oldStatus.Change statusCell to oldStatus.name: wasStatus -> newStatus.Show in the output: name: wasStatus -> newStatus.rowValues to [empty, empty, statusCell, today].Set rowValues to [empty, empty, statusCell, today].eventBooked is yesIf eventBooked is yeseventDate to the end of rowValues with eventLink appended and save it as rowValues.Add eventDate to the end of rowValues with eventLink appended and save it as rowValues.writeErrorIf a step below fails, save the error as writeErrorrowNumber of tab in sheetUrl.Overwrite row rowNumber of tab in sheetUrl.name (row rowNumber) - writeError.message to the end of writeFailures and save it as writeFailures.Add name (row rowNumber) - writeError.message to the end of writeFailures and save it as writeFailures.checkedCount is 0If checkedCount is 0summary to Checked checkedCount filings: overdueCount overdue, dueTodayCount due today, dueSoonCount due soon, upToDateCount up to date.Set summary to Checked checkedCount filings: overdueCount overdue, dueTodayCount due today, dueSoonCount due soon, upToDateCount up to date.unreadableCount is greater than 0If unreadableCount is greater than 0summary to summary, unreadableCount with a due date that could not be read: unreadableRows joined by , .Change summary to summary, unreadableCount with a due date that could not be read: unreadableRows joined by , .parkedCount is greater than 0If parkedCount is greater than 0summary to summary; parkedCount left alone because their Status was set by hand.Change summary to summary; parkedCount left alone because their Status was set by hand.summary to summary. Sent remindersSent reminder(s) and booked eventsBooked calendar event(s).Change summary to summary. Sent remindersSent reminder(s) and booked eventsBooked calendar event(s).emailFailures isn't empty or calendarFailures isn't empty or writeFailures…If emailFailures isn't empty or calendarFailures isn't empty or writeFailures…problems to summary.Set problems to summary.emailFailures isn't emptyIf emailFailures isn't emptyproblems to problems Reminders that could not be sent, which will be tried again on the next run: emailFailures joined by ; .Change problems to problems Reminders that could not be sent, which will be tried again on the next run: emailFailures joined by ; .calendarFailures isn't emptyIf calendarFailures isn't emptyproblems to problems Calendar events that could not be booked, which will be tried again on the next run: calendarFailures joined by ; .Change problems to problems Calendar events that could not be booked, which will be tried again on the next run: calendarFailures joined by ; .writeFailures isn't emptyIf writeFailures isn't emptyproblems to problems Rows that could not be written back, which will be checked again on the next run: writeFailures joined by ; .Change problems to problems Rows that could not be written back, which will be checked again on the next run: writeFailures joined by ; .problems.Stop and report a problem: problems.summary.Finish the run: summary.