How to Create Real Recurring Location-based iOS Reminders (after iOS 13)

In How to Create Real Recurring Location-based iOS Reminders, I described a method using IFTTT to mark location-based reminders as incomplete.  Shortly after that post, I upgraded my iPhone to iOS 13 and quickly discovered that the update broke then IFTTT iOS Reminders service.  All triggers and actions in the service no longer worked as expected (or at all), including IFTTT’s Reminder Completed in List trigger.  So I was back to dismissing my recurring location-based reminders without marking them as complete.  And on top of that, despite the additional functionality of the new Reminders app, I started encountering all sorts of issues, including the fact that Reminders in Notification Center started randomly turning itself off.  While I haven’t found a solution to that and other new Reminders issues, I did come up with a solution to replace IFTTT’s broken Reminder Completed in List trigger.  Read on to discover how…

Requirements:

Note, there are fewer requirements than the original post.

  1. An always-on Mac
  2. iCloud synchronized Reminders (on both iPhone and Mac)
  3. [OPTIONAL] An IFTTT account

We will use IFTTT as an example, but you can supply a webhook to any service that listens for webhook calls.  For example, mine calls my Node Red installation on my Raspberry Pi, but that’s a much steeper learning curve than IFTTT, so we’ll stick to IFTTT in this blog post.  For information on how to set up IFTTT‘s webhook service so that you can post webhook calls to trigger IFTTT applets, refer to IFTTT’s documentation on its webhook service.

Setup:

  1. In your Reminders app from either your iCloud-reminder-synched iOS device or your computer, create a new list with a unique name
  2. On a mac computer that you always keep running, download the completion_notifier.scpt Applescript (from the github gist shown below).  Save it in a convenient place.  I suggest creating a Scripts folder in your home directory.
  3. In a new Terminal.app window, enter the following command (customizing it as indicated below):
    • osascript ~/Scripts/completion_notifier.scpt "~/Scripts/recently_completed_reminders.txt" "" -1
    • Make these changes to the command based on your circumstances:
      • Change the script path to wherever you saved the script from step 2.  Please use a path that contains no folders that have a space in their name, as that might cause the script to not work properly (as I didn’t make it sophisticated enough to handle spaces).
      • Change the `recently_completed_reminders.txt` file name and path to whatever you like.
    • For your information…
      • osascript is a command-line executable that comes on MacOS and allows you to execute AppleScripts from the command line.
      • Note that your browser might change the double-quotes to smart quotes, so you may have to delete and re-type them in the terminal.
      • The purpose of executing this command is both to create the `recently_completed_reminders.txt` file, initialized with a starting time so that when it runs in the cron-job, it will find newly completed reminders since its last execution, and to prompt your system to grant permission to the script to have access to your Reminders.
      • We will customize the last 2 arguments when we set up the cron job, so leave their values as-is for now.
  4. If you are presented with a dialog (or dialogs) asking you if you want to allow access to your reminders, click OK.
    • reminders_access.png
      reminder_access2.png
  5. Next, we need to set up this script so that it will run every so often to check for newly completed reminders.  The more frequently you set it to run, the sooner your automations will run after you complete a reminder.
    1. Obtain your webhook URL from IFTTT by clicking on the Documentation link at the top right.  Enter a name for this action that will receive calls from your applescript and then copy the updated URL at the bottom starting from https (ignore the curl command in front of it).
    2. In Terminal.app, execute the following command:
      1. crontab -e
    3. This opens up an editor where we need to enter our repeated command.  The default editor interface is called `​vi`, so I will quickly show you what to type just to accomplish our task (do not use the enter/return key):
      • i
        0 0 0 ? * * * osascript ~/Scripts/completion_notifier.scpt "~/Scripts/recently_completed_reminders.txt" "https://maker.ifttt.com/trigger/your_webhook_name/with/key/your_key" "-1" "reminder list name 1" "reminder list name 2"
        esc (i.e. the escape key on your keyboard)
        :wq
      • Edit the paths and file names as you did in step 3
      • This runs the script once a day at midnight.  If you would like to change the frequency, you must change the values where it shows “o o o ? * * * “.  Use the cron expression generator to determine what to enter.
      • You can change the “-1” to 1 or 2 in order to ignore reminders with that priority or lower (priorities are 1 through 3 and show in the app as a number of exclamation points).  The value “-1” turns off this feature and all reminders are included whether they have a set priority or not.  There is currently no way to only ignore reminders that have no set priority.
      • If you want to limit the script to only take into account certain reminder lists, you can add each list to the end of the command where I have 2 examples “reminder list name 1” and “reminder list name 2”.  If you want it to consider all lists, you can remove those 2 sample list names (and their surrounding quotes).  Make sure that any list names you supply are surrounded in quotes and match exactly the actual list names in the reminders app.  You can event enter separate lines for different lists or to call different/multiple webhooks.
  6. You may again be prompted one more time with a dialog asking to allow access to your Reminders once the cron job runs the script.  It will stay on the screen until the next time you log back in.  After that, you should not be asked again.

The above will allow you to trigger anything via IFTTT, but to set up IFTTT to mark geofence reminders as incomplete, refer to the original blog post and complete steps 1 through 9.  Step 10 will be modified by what we’ve done above.  Instead of using IFTTT’s “Reminder completed in list” trigger, we will use a webhook that is what the above process calls.  No json parameters in that case are necessary.  Note that technically, you could modify the AppleScript in this blogpost to mark the reminders as incomplete, thereby making it unnecessary to call IFTTT via webhook, but by calling IFTTT, it makes it possible to do more than just mark reminders as incomplete.

Note, you can test out the script call (starting from osascript to the end of the line) by executing them manually on the command line.  Simply add a test reminder in one of the lists supplied at the end of the command, complete it, and then execute the command.  You can then go to any IFTTT applet you create to be triggered by this webhook and look at its activity (and click “check now”) to ensure that the script made the webhook call.

If you want the webhook to include any json parameters, you will have to edit the AppleScript, but note that multiple reminders may be completed between the times that the script checks and its currently only coded to call the webhook once, after it has found any completed reminder(s).  So if you want to include the reminder title, you will have to edit the code to make multiple webhook calls.

The AppleScript

Look for newly completed reminders (since the last check) and if there is a new one, make a webhook post with date, title, and list JSON values
by Robert W. Leach, based on scripts by Craig Eley, Samantha Hilton, and Nick Morris
This is not foolproof. If you have more than 1 device you use to "complete" reminders and a reminder is completed while offline, after which, a reminder is completed from a second online device, the completion of the reminders in the offline device (once it comes online) will be missed. However, it's done this way so that the script doesn't take forever looking at all reminders each time.
Schedule it to run as frequently as you would like using osascript in a cron job and forget about it. Run once manually in order to "allow" this script to run in the security settings by responding to the resulting dialog.
Cron job every 30 minutes looking for newly completed reminders in list "MyReminders" example (execute `crontab -e` in terminal and enter): */30 * * * * osascript /Users/username/Scripts/completion_notifier.scpt "/Users/username/reminder_completions.txt" "http://maker.ifttt.com/trigger/myeventname/with/key/mykeyfromiftttmaker" -1 "MyReminders"
Run without command line arguments to get the usage
Command line argument examples:
"~/completed_reminders.txt" –No spaces allowed in file path. File does not need to pre-exist (but directories do).
"http://maker.ifttt.com/trigger/{event}/with/key/{key}" –Empty = don't make a webhook call (for testing)
-1 –Priority level cutoff. -1 = all. 1 = !, 2 = !!, 3 = !!!. Considers only reminders with priority greater than this value.
"ToDo" –Empty = all. Example: "Grocery" "ToDo" "Work" "Home" – only provided lists are checked for newly completed items
on run argv
set debug to false
if (count of argv) is less than 3 then
return "Usage: osascript completion_notifier.scpt \"/path/to/outfile.txt\" \"http://webhook_url\" priority_level [reminder list names…]" & linefeed & "Example: osascript completion_notifier.scpt \"/Users/jsmith/completed_reminders.txt\" \"http://test.com/webhook\" -1"
end if
set lastcompletionfile to item 1 of argv
set webhookurl to item 2 of argv
set prilev to item 3 of argv
set liststocheck to {}
if (count of argv) is equal to 4 then
set liststocheck to {item 4 of argv}
else if (count of argv) is greater than 3 then
set liststocheck to {items 4 thru (count of argv) of argv}
end if
tell application "Reminders"
set {startdate, firsttime} to my getStart(lastcompletionfile)
if debug is true then
display dialog "Starting datetime: " & startdate as text
end if
set output to ""
set lastcompletion to startdate
set mostrecentdate to ""
set mostrecenttitle to ""
set mostrecentlist to ""
Find entries completed today from each list
set hasOne to false
repeat with alist in every list
set mylist to name of alist as text
if (count of liststocheck) is 0 or liststocheck contains mylist then
set listid to id of alist as text
if debug is true then
display dialog mylist & linefeed & listid
display dialog mylist
end if
repeat with reminderObj in (reminders in alist whose (completed is true) and (completion date is greater than startdate) and (priority is greater than prilev))
set nameObj to name of reminderObj
set compDateObj to completion date of reminderObj
set timeStr to date string of compDateObj & " at " & time string of compDateObj
set idObj to id of reminderObj
if compDateObj is greater than lastcompletion then
set hasOne to true
set lastcompletion to compDateObj
set mostrecentdate to timeStr
set mostrecenttitle to nameObj
set mostrecentlist to mylist
end if
if debug is true then
display dialog idObj & tab & timeStr & tab & nameObj
end if
set output to output & listid & tab & idObj & tab & timeStr & tab & mylist & tab & nameObj & linefeed
set output to output & timeStr & tab & mylist & tab & nameObj & linefeed
if debug is true then
display dialog listid & tab & idObj & tab & timeStr & tab & mylist & tab & nameObj
display dialog timeStr & tab & mylist & tab & nameObj
end if
end repeat
end if
end repeat
if hasOne is true then
save the newly completed items found this time so that next time, we can look for newer items
set shellTxt to "echo \"" & output & "\" > " & lastcompletionfile
if debug is true then
display dialog shellTxt
end if
do shell script shellTxt
Make the webhook call (if the webhookurl is not an empty string)
if webhookurl is not "" then
Use this example to send json data
set shellScript to "curl -X POST –data-urlencode 'payload={\"channel\": \"#general\", \"username\": \"Completion Notifier Bot\", \"icon_emoji\": \":ghost:\"}' " & webhookurl
set jsonoption to "–data-urlencode 'payload={\"value1\": \"" & mostrecentdate & "\", \"value2\": \"" & mostrecenttitle & "\", \"value3\": \"" & mostrecentlist & "\"}'"
set shellTxt to "curl -X POST " & jsonoption & " \"" & webhookurl & "\" > /dev/null"
if debug is true then
display dialog shellTxt
end if
do shell script shellTxt
end if
else
Save the date this was run so it can be parsed later the next time it's run
if firsttime is true then
set shellTxt to "echo \"" & startdate & tab & "initial run" & tab & "no completed reminders" & linefeed & "\" > " & lastcompletionfile
if debug is true then
display dialog shellTxt
end if
do shell script shellTxt
end if
if debug is true then
display dialog "Nothing is newly completed"
end if
end if
end tell
end run
Takes the file containing the last determined most recent completion date (since the last time this script ran).
Returns from the file, the most recent reminder completion date and a boolean indicating whether write an initial start record into the file
on getStart(lastcompletionfile)
set startdate to the current date
set checkscript to "if [ -e \"`eval echo " & lastcompletionfile & "`\" ]; then echo yes; else echo no; fi"
display dialog checkscript
set fileisthere to first paragraph of (do shell script checkscript)
display dialog lastcompletionfile & " exists? [" & fileisthere & "]"
set gotone to false
if fileisthere is equal to "yes" then
set lastdate to missing value
set datescript to "cut -f 3 " & "\"`eval echo " & lastcompletionfile & "`\""
display dialog datescript
set datelines to paragraphs of (do shell script "cut -f 1 " & "\"`eval echo " & lastcompletionfile & "`\"")
repeat with adateline in datelines
display dialog "Checking [" & adateline & "]"
try
set tmpdate to date adateline
if lastdate is missing value or tmpdate is greater than lastdate then
set lastdate to tmpdate
set gotone to true
display dialog "Later: " & lastdate as text
end if
end try
end repeat
if gotone is true then
display dialog "Setting start date: " & lastdate as text
set startdate to lastdate
end if
end if
display dialog "Returning start date: " & startdate as text
return {startdate, not gotone}
end getStart

Caveats

Note that this script is not foolproof.  If you have multiple devices on which you synch and complete reminders and any one of them goes offline (like if you turn on airplane mode), the script will not catch that completion until the device goes back online.  This would be especially susceptible to miss reminders if you edit the code to make a webhook call for each reminder and complete reminders in the following order of events:

  1. Turn on airplane mode on your phone
  2. Complete reminder 1 on your phone
  3. Complete reminder 2 on your (online) computer
  4. Turn off airplane mode on your phone

There’s a chance, depending on how much time passes between reminder completions (and how frequently you set the script to run), that there would be no webhook call for reminder 1, because the script only looks for reminders completed since the last time it checked and the completion date of reminder 1 is before reminder 2.

This is done to make the check quick.  If you’ve been using the reminders app for a long time, checking all reminders for a new reminder could take many minutes.  However, limiting the check to only the most recent reminders is fast.

Additionally, there are various pitfalls to be aware of.  You can either upgrade your reminders everywhere (iOS 13 and macOS Catalina) or you can not upgrade your reminders and continue to use a macOS prior to Catalina, however you may run into problems.  For example, I need to keep my computer on which I run this script at Mojave since I depend on a 32 bit app (which Catalina no longer supports).  And for whatever reason, editing old recurring reminders via AppleScript on that computer seems to corrupt the geofence settings every time it runs, until I set the geofence settings on that computer.  You may experience similar weird Reminders issues that came about when iOS 13 and the new Reminders features were released.

Discussion

This solution only works if you keep a computer running all the time, on which you can install this script.  If you want your webhook calls to run quickly after reminder completion, you can make the script run every minute or less, but I wouldn’t recommend it.  Sometimes AppleScript can get hung up and if that happens, this can bog your computer down.  I have mine set to every 30 minutes, and I have not noticed any problems.

Using a cron job isn’t the only means by which you can initiate the script.  You could create IFTTT applets to create files in a Dropbox folder and attach a script to that folder on your computer.  This would be especially useful for geofence reminders.  If you set up a clever way of initiating the script, please comment below to share!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s