Checks your deadline sheet each morning and emails you as each licence, permit, tax filing or renewal comes due, with a calendar event.

UsesGmailGoogle CalendarGoogle 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.

ScheduleRun every day at 09:00.Run every day at 09:00.
ValuesSet trackerSheetUrl to blank.Set trackerSheetUrl to blank.
ValuesSet trackerTab to Filings.Set trackerTab to Filings.
ValuesSet reminderEmail to blank.Set reminderEmail to blank.
ValuesSet timeZone to blank.Set timeZone to blank.
ValuesSet dueSoonDays to 30.Set dueSoonDays to 30.
ValuesSet calendarId to primary.Set calendarId to primary.
ValuesSet eventStartTime to 09:00.Set eventStartTime to 09:00.
ValuesSet eventMinutes to 30.Set eventMinutes to 30.
MathSet dueSoonWindow to dueSoonDays multiplied by 1.Set dueSoonWindow to dueSoonDays multiplied by 1.
Decisions & LoopsIf dueSoonWindow is NaN or dueSoonWindow is less than 0If dueSoonWindow is NaN or dueSoonWindow is less than 0
Yes
Decisions & LoopsStop and report a problem: The 'dueSoonDays' step needs a whole number of days, 0 or more: how many days before its due date a filing counts as due soon.Stop and report a problem: The 'dueSoonDays' step needs a whole number of days, 0 or more: how many days before its due date a filing counts as due soon.
No
No steps
TextTrim trackerSheetUrl and save it as sheetUrl.Trim trackerSheetUrl and save it as sheetUrl.
TextTrim trackerTab and save it as tab.Trim trackerTab and save it as tab.
TextTrim reminderEmail and save it as sendTo.Trim reminderEmail and save it as sendTo.
TextTrim timeZone and save it as zone.Trim timeZone and save it as zone.
TextTrim calendarId and save it as calendar.Trim calendarId and save it as calendar.
TextTrim eventStartTime and save it as startTime.Trim eventStartTime and save it as startTime.
ValuesSet today to blank.Set today to blank.
Decisions & LoopsIf a step below fails, save the error as zoneErrorIf a step below fails, save the error as zoneError
Try
Dates & TimesGet today's date and save it as today.Get today's date and save it as today.
On error
Decisions & LoopsStop and report a problem: '@zone' is not a time zone this workflow recognises. Use a name such as Europe/London or America/New_York in the 'timeZone' step, then run it again.Stop and report a problem: '@zone' is not a time zone this workflow recognises. Use a name such as Europe/London or America/New_York in the 'timeZone' step, then run it again.
Decisions & LoopsIf a step below fails, save the error as timeErrorIf a step below fails, save the error as timeError
Try
Dates & TimesMove 2026-01-01T@startTime:00 by eventMinutes 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.
On error
Decisions & LoopsStop and report a problem: Check the 'eventStartTime' step, which needs a 24-hour clock time such as 09:00 or 14:30, and the 'eventMinutes' step, which needs a whole number of minutes, then run it again.Stop and report a problem: Check the 'eventStartTime' step, which needs a 24-hour clock time such as 09:00 or 14:30, and the 'eventMinutes' step, which needs a whole number of minutes, then run it again.
ValuesSet tracker to an empty record.Set tracker to an empty record.
Decisions & LoopsIf a step below fails, save the error as readErrorIf a step below fails, save the error as readError
Try
Google SheetsGoogle SheetsRead the rows of sheetUrl. Save it as tracker.Read the rows of sheetUrl. Save it as tracker.
On error
Decisions & LoopsStop 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.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.
ValuesSet headers to tracker.headers.Set headers to tracker.headers.
ValuesSet filings to tracker.rows.Set filings to tracker.rows.
Decisions & LoopsIf headers is emptyIf headers is empty
Yes
Decisions & LoopsStop and report a problem: The tab '@tab' is empty or could not be found. Check the tab name in the 'trackerTab' step and that its header row is on row 1, then run it again.Stop and report a problem: The tab '@tab' is empty or could not be found. Check the tab name in the 'trackerTab' step and that its header row is on row 1, then run it again.
No
No steps
ValuesSet 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.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.
Decisions & LoopsIf headersOk is noIf headersOk is no
Yes
Decisions & LoopsStop 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.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.
No
No steps
Decisions & LoopsIf filings is emptyIf filings is empty
Yes
Decisions & LoopsStop and report a problem: The tab '@tab' has no filings under its header row. Add them from row 2 down, then run it again.Stop and report a problem: The tab '@tab' has no filings under its header row. Add them from row 2 down, then run it again.
No
No steps
ValuesSet checkedCount to 0.Set checkedCount to 0.
ValuesSet overdueCount to 0.Set overdueCount to 0.
ValuesSet dueTodayCount to 0.Set dueTodayCount to 0.
ValuesSet dueSoonCount to 0.Set dueSoonCount to 0.
ValuesSet upToDateCount to 0.Set upToDateCount to 0.
ValuesSet unreadableCount to 0.Set unreadableCount to 0.
ValuesSet unreadableRows to an empty list.Set unreadableRows to an empty list.
ValuesSet parkedCount to 0.Set parkedCount to 0.
ValuesSet remindersSent to 0.Set remindersSent to 0.
ValuesSet eventsBooked to 0.Set eventsBooked to 0.
ValuesSet emailFailures to an empty list.Set emailFailures to an empty list.
ValuesSet calendarFailures to an empty list.Set calendarFailures to an empty list.
ValuesSet writeFailures to an empty list.Set writeFailures to an empty list.
Decisions & LoopsFor each filing in filings:For each filing in filings:
TextTrim filing.Filing and save it as name.Trim filing.Filing and save it as name.
TextTrim filing["Due Date"] and save it as dueRaw.Trim filing["Due Date"] and save it as dueRaw.
TextTrim filing.Status and save it as oldStatus.Trim filing.Status and save it as oldStatus.
TextTrim filing["Calendar Event"] and save it as eventLink.Trim filing["Calendar Event"] and save it as eventLink.
TextTrim filing["Event Date"] and save it as eventDate.Trim filing["Event Date"] and save it as eventDate.
ValuesSet rowNumber to filing.row_number.Set rowNumber to filing.row_number.
Decisions & LoopsIf name is blank and dueRaw is blankIf name is blank and dueRaw is blank
Yes
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
Decisions & LoopsIf name is blankIf name is blank
Yes
ValuesChange name to Row rowNumber.Change name to Row rowNumber.
No
No steps
Decisions & LoopsIf [, 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…
Yes
ValuesAdd 1 to parkedCount.Add 1 to parkedCount.
Decisions & LoopsSkip to the next item.Skip to the next item.
No
No steps
ValuesAdd 1 to checkedCount.Add 1 to checkedCount.
ValuesSet dueDate to blank.Set dueDate to blank.
ValuesSet daysLeft to 0.Set daysLeft to 0.
ValuesSet weekday to blank.Set weekday to blank.
ValuesSet dateOk to false.Set dateOk to false.
Decisions & LoopsIf dueRaw isn't emptyIf dueRaw isn't empty
Yes
Decisions & LoopsIf a step below fails, save the error as dateErrorIf a step below fails, save the error as dateError
Try
ListsGet the first item of dueRaw 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.
Dates & TimesCount the days from today to dueDate and save it as daysLeft.Count the days from today to dueDate and save it as daysLeft.
Dates & TimesGet the day of the week for dueDate and save it as weekday.Get the day of the week for dueDate and save it as weekday.
ValuesChange dateOk to dueRaw multiplied by 1 is NaN.Change dateOk to dueRaw multiplied by 1 is NaN.
On error
ValuesChange dateOk to false.Change dateOk to false.
No
No steps
ValuesSet newStatus to Check due date.Set newStatus to Check due date.
Decisions & LoopsIf dateOk is yesIf dateOk is yes
Yes
Decisions & LoopsIf daysLeft is less than 0If daysLeft is less than 0
Yes
ValuesChange newStatus to Overdue.Change newStatus to Overdue.
ValuesAdd 1 to overdueCount.Add 1 to overdueCount.
Else if @daysLeft is 0
ValuesChange newStatus to Due today.Change newStatus to Due today.
ValuesAdd 1 to dueTodayCount.Add 1 to dueTodayCount.
Else if @daysLeft is less than or equal to @dueSoonWindow
ValuesChange newStatus to Due soon.Change newStatus to Due soon.
ValuesAdd 1 to dueSoonCount.Add 1 to dueSoonCount.
Otherwise
ValuesChange newStatus to Up to date.Change newStatus to Up to date.
ValuesAdd 1 to upToDateCount.Add 1 to upToDateCount.
No
ValuesAdd 1 to unreadableCount.Add 1 to unreadableCount.
ListsAdd 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.
ValuesShow 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.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.
ValuesSet wasStatus to oldStatus.Set wasStatus to oldStatus.
Decisions & LoopsIf wasStatus is blankIf wasStatus is blank
Yes
ValuesChange wasStatus to new.Change wasStatus to new.
No
No steps
ValuesSet eventBooked to false.Set eventBooked to false.
Decisions & LoopsIf 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…
Yes
Decisions & LoopsIf a step below fails, save the error as calendarErrorIf a step below fails, save the error as calendarError
Try
ValuesSet startsAt to dueDateT@startTime:00.Set startsAt to dueDateT@startTime:00.
ListsGet the first item of 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.
Google CalendarGoogle CalendarCreate the event 'Filing due: 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.
TextTrim booking.html_link and save it as eventLink.Trim booking.html_link and save it as eventLink.
ValuesChange eventDate to dueDate.Change eventDate to dueDate.
ValuesChange eventBooked to true.Change eventBooked to true.
ValuesAdd 1 to eventsBooked.Add 1 to eventsBooked.
On error
ListsAdd 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.
ValuesShow 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.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.
No
No steps
ValuesSet statusCell to newStatus.Set statusCell to newStatus.
ValuesSet sendReminder to false.Set sendReminder to false.
Decisions & LoopsIf 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…
Yes
ValuesChange sendReminder to true.Change sendReminder to true.
No
No steps
Decisions & LoopsIf sendReminder is yesIf sendReminder is yes
Yes
ValuesSet dayWord to days.Set dayWord to days.
Decisions & LoopsIf daysLeft is 1 or daysLeft is -1If daysLeft is 1 or daysLeft is -1
Yes
ValuesChange dayWord to day.Change dayWord to day.
No
No steps
ValuesSet subject to blank.Set subject to blank.
ValuesSet opening to blank.Set opening to blank.
ValuesSet closing to blank.Set closing to blank.
Decisions & LoopsIf newStatus is OverdueIf newStatus is Overdue
Yes
MathSet overdueBy to daysLeft multiplied by -1.Set overdueBy to daysLeft multiplied by -1.
ValuesChange subject to Overdue: name was due on dueDate.Change subject to Overdue: name was due on dueDate.
ValuesChange 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.
ValuesChange 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..
Else if @newStatus is "Due today"
ValuesChange subject to Due today: name.Change subject to Due today: name.
ValuesChange opening to name is due today, weekday dueDate.Change opening to name is due today, weekday dueDate.
ValuesChange 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..
Otherwise
ValuesChange subject to Due in daysLeft dayWord: name.Change subject to Due in daysLeft dayWord: name.
Decisions & LoopsIf daysLeft is 1If daysLeft is 1
Yes
ValuesChange subject to Due tomorrow: name.Change subject to Due tomorrow: name.
No
No steps
ValuesChange opening to name is due in daysLeft dayWord, on weekday dueDate.Change opening to name is due in daysLeft dayWord, on weekday dueDate.
ValuesChange 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..
ValuesSet details to Filing: nameSet details to Filing: name Due date: weekday, dueDate Tracker status: newStatus.
Decisions & LoopsIf eventLink isn't emptyIf eventLink isn't empty
Yes
ValuesChange details to detailsChange details to details Calendar event: eventLink.
No
No steps
ValuesChange details to detailsChange details to details Tracker: sheetUrl.
ValuesSet emailBody to openingSet emailBody to opening details closing.
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 sendTo.Send an email to sendTo. Subject: subjectemailBody
ValuesAdd 1 to remindersSent.Add 1 to remindersSent.
ValuesShow in the output: name: wasStatus -> newStatus. Reminder sent to sendTo.Show in the output: name: wasStatus -> newStatus. Reminder sent to sendTo.
On error
ListsAdd 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.
ValuesChange statusCell to oldStatus.Change statusCell to oldStatus.
Else if @dateOk is yes and @newStatus is not @oldStatus
ValuesShow in the output: name: wasStatus -> newStatus.Show in the output: name: wasStatus -> newStatus.
Otherwise
No steps
ValuesSet rowValues to [empty, empty, statusCell, today].Set rowValues to [empty, empty, statusCell, today].
Decisions & LoopsIf eventBooked is yesIf eventBooked is yes
Yes
ListsAdd eventDate 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.
No
No steps
Decisions & LoopsIf a step below fails, save the error as writeErrorIf a step below fails, save the error as writeError
Try
Google SheetsGoogle SheetsOverwrite row rowNumber of tab in sheetUrl.Overwrite row rowNumber of tab in sheetUrl.
On error
ListsAdd 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.
Decisions & LoopsIf checkedCount is 0If checkedCount is 0
Yes
Decisions & LoopsFinish the run: Every row on the tab is either blank or has a Status you set by hand, so there was nothing to check.Finish the run: Every row on the tab is either blank or has a Status you set by hand, so there was nothing to check.
No
No steps
ValuesSet summary 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.
Decisions & LoopsIf unreadableCount is greater than 0If unreadableCount is greater than 0
Yes
ValuesChange summary 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 , .
No
No steps
Decisions & LoopsIf parkedCount is greater than 0If parkedCount is greater than 0
Yes
ValuesChange summary 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.
No
No steps
ValuesChange 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).
Decisions & LoopsIf emailFailures isn't empty or calendarFailures isn't empty or writeFailures…If emailFailures isn't empty or calendarFailures isn't empty or writeFailures…
Yes
ValuesSet problems to summary.Set problems to summary.
Decisions & LoopsIf emailFailures isn't emptyIf emailFailures isn't empty
Yes
ValuesChange problems 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 ; .
No
No steps
Decisions & LoopsIf calendarFailures isn't emptyIf calendarFailures isn't empty
Yes
ValuesChange problems 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 ; .
No
No steps
Decisions & LoopsIf writeFailures isn't emptyIf writeFailures isn't empty
Yes
ValuesChange problems 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 ; .
No
No steps
Decisions & LoopsStop and report a problem: problems.Stop and report a problem: problems.
No
No steps
Decisions & LoopsFinish the run: summary.Finish the run: summary.