So, I wrote a script to backup my Reminders.app tasks, since the “upgraded” Reminders data format that started in Catalina (I think?) removes the Export menu command from the Reminders.app.
It worked well, up until I started using some of the other features of the upgraded format, like assigning a reminder to someone, or (even worse) ANY sub-reminder. The new format lets you “indent” reminders to be sub-reminders of another reminder. When you do that, however, sub-reminders seem to be unreachable by AppleScript. The app’s dictionary doesn’t indicate that reminders can contain other reminders. And, getting the reminders of a list only returns the top-level reminders.
So, in case anyone has thoughts, or is interested in the script itself, I’ll show that here. If you have multiple Reminders accounts, it asks which account you want to backup lists for. If you only have one account, it just uses that without asking. It then asks which list(s) you want to back up (select multiple by shift-clicking a range or command-clicking individual list names). It saves the resulting data as a text file in your Documents folder. I have another script that reads that text file, and will restore the items into the specified list(s), even creating the list if it does not exist.
-- Reminders - Lists Backup
(*
Creates a backup text file of the (top-level) reminders of the CHOSEN list(s) of the chosen account.
NOTE: Does NOT completely handle the new features in the "upgraded" Reminders format in iOS 13 and macOS Catalina and later. It cannot backup sub-reminders, or assignee info, etc. It only can see and backup features that existed in the original format.
DEV NOTE: even if properties are 'missing value', back them up so that the restore script does not have to check whether hash key exists. Restore will just disregard them, but won't get error trying to read.
HISTORY:
2023-03-27 ( Krioni ): created. Based off "Reminders - Account Backup" script.
*)
property ScriptName : "Reminders Lists Backup"
property oneAccountName : ""
property listNames : {} -- a LIST of the reminder lists to backup.
property promptChooseAccount : "Choose which account to backup Reminders items for:"
property promptChooseList : "Choose which reminder LIST(s) to backup Reminders items for:"
on run
if length of oneAccountName is 0 then
tell me to activate
tell application "Reminders"
set accountNames to name of every account
end tell
if (count of accountNames) is equal to 1 then
set oneAccountName to item 1 of accountNames
else
set chooseAccountDialog to (choose from list accountNames with title ScriptName with prompt promptChooseAccount OK button name "Backup" cancel button name "Cancel" without multiple selections allowed and empty selection allowed)
if class of chooseAccountDialog is boolean then return false -- user canceled
set oneAccountName to item 1 of chooseAccountDialog
end if
end if
if (count of listNames) is 0 then
tell me to activate
tell application "Reminders"
set listNames to name of every list of account oneAccountName
end tell
set chooseListsDialog to (choose from list listNames with title ScriptName with prompt promptChooseList OK button name "Backup" cancel button name "Cancel" with multiple selections allowed without empty selection allowed)
if class of chooseListsDialog is boolean then return false -- user canceled
set listNames to chooseListsDialog
end if
set backupTS to (current date)
set backupFolderPath to (path to documents folder) as string
set timestampString to timestampISO8601(backupTS)
if (count of listNames) is equal to 1 then
set listDesc to " (" & item 1 of listNames & ")"
else
set listDesc to " (" & (count of listNames) & " lists)"
end if
set backupFilePath to backupFolderPath & "RemindersBackup - Account " & oneAccountName & listDesc & " - " & timestampString & ".txt"
tell application "Reminders"
set oneAccount to account oneAccountName
set backupData to {}
repeat with oneListName in listNames
set oneListRef to list oneListName of oneAccount
set oneListName to name of oneListRef
set oneListColor to color of oneListRef
set oneListEmblem to emblem of oneListRef
set activeReminders to (reminders of oneListRef whose completed is false)
repeat with oneReminder in activeReminders
set oneListReminderProps to properties of reminders of oneListRef
set oneListReminders to {}
repeat with rawProps in oneListReminderProps
-- only backup properties that can be restored (so, e.g. NOT container)
set oneReminderProps to {name:name of rawProps} ¬
& {id:id of rawProps} ¬
& {creation date:creation date of rawProps} ¬
& {modification date:modification date of rawProps} ¬
& {body:body of rawProps} ¬
& {completed:completed of rawProps} ¬
& {completion date:completion date of rawProps} ¬
& {due date:due date of rawProps} ¬
& {allday due date:allday due date of rawProps} ¬
& {remind me date:remind me date of rawProps} ¬
& {priority:priority of rawProps} ¬
& {flagged:flagged of rawProps}
copy oneReminderProps to end of oneListReminders
end repeat
end repeat
set oneListBackup to {listName:oneListName, listColor:oneListColor, listEmblem:oneListEmblem, listReminders:oneListReminders}
copy oneListBackup to end of backupData
end repeat
--return backupData
set backupDataString to my coerceToString(backupData)
end tell
writeToFile({outputText:backupDataString, fullFilePath:backupFilePath})
return backupDataString
end run
on coerceToString(incomingObject)
-- version 2.2
if class of incomingObject is string then
set {text:incomingObject} to (incomingObject as string)
return incomingObject
else if class of incomingObject is integer then
set {text:incomingObject} to (incomingObject as string)
return incomingObject as string
else if class of incomingObject is real then
set {text:incomingObject} to (incomingObject as string)
return incomingObject as string
else if class of incomingObject is Unicode text then
set {text:incomingObject} to (incomingObject as string)
return incomingObject as string
else
-- LIST, RECORD, styled text, or unknown
try
try
set some_UUID_Property_54F827C7379E4073B5A216BB9CDE575D of "XXXX" to "some_UUID_Value_54F827C7379E4073B5A216BB9CDE575D"
-- GENERATE the error message for a known 'object' (here, a string) so we can get
-- the 'lead' and 'trail' part of the error message
on error errMsg number errNum
set {oldDelims, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {"\"XXXX\""}}
set {errMsgLead, errMsgTrail} to text items of errMsg
set AppleScript's text item delimiters to oldDelims
end try
-- now, generate error message for the SPECIFIED object:
set some_UUID_Property_54F827C7379E4073B5A216BB9CDE575D of incomingObject to "some_UUID_Value_54F827C7379E4073B5A216BB9CDE575D"
on error errMsg
if errMsg starts with "System Events got an error: Can’t make some_UUID_Property_54F827C7379E4073B5A216BB9CDE575D of " and errMsg ends with "into type specifier." then
set errMsgLead to "System Events got an error: Can’t make some_UUID_Property_54F827C7379E4073B5A216BB9CDE575D of "
set errMsgTrail to " into type specifier."
set {od, AppleScript's text item delimiters} to {AppleScript's text item delimiters, errMsgLead}
set objectString to text item 2 of errMsg
set AppleScript's text item delimiters to errMsgTrail
set objectString to text item 1 of objectString
set AppleScript's text item delimiters to od
else
--tell me to log errMsg
set objectString to errMsg
if objectString contains errMsgLead then
set {od, AppleScript's text item delimiters} to {AppleScript's text item delimiters, errMsgLead}
set objectString to text item 2 of objectString
set AppleScript's text item delimiters to od
end if
if objectString contains errMsgTrail then
set {od, AppleScript's text item delimiters} to {AppleScript's text item delimiters, errMsgTrail}
set AppleScript's text item delimiters to errMsgTrail
set objectString to text item 1 of objectString
set AppleScript's text item delimiters to od
end if
--set {text:objectString} to (objectString as string) -- what does THIS do?
end if
end try
return objectString
end if
end coerceToString
on timestampISO8601(incomingDate)
-- version 1.0
if class of incomingDate is not date then
try
set incomingDate to date incomingDate
on error
set incomingDate to (current date)
end try
end if
set numHours to (time of incomingDate) div hours
set textHours to text -2 through -1 of ("0" & (numHours as string))
set numMinutes to (time of incomingDate) mod hours div minutes
set textMinutes to text -2 through -1 of ("0" & (numMinutes as string))
set numSeconds to (time of incomingDate) mod minutes
set textSeconds to text -2 through -1 of ("0" & (numSeconds as string))
set numDay to day of incomingDate as number
set textDay to text -2 through -1 of ("0" & (numDay as string))
set numYear to year of incomingDate as number
set textYear to text -4 through -1 of (numYear as string)
set numMonth to (month of (incomingDate)) as number
set textMonth to text -2 through -1 of ("0" & (numMonth as string))
set customDateString to textYear & "-" & textMonth & "-" & textDay & "--" & textHours & "-" & textMinutes & "-" & textSeconds
return customDateString
end timestampISO8601
on writeToFile(prefs)
-- version 1.2
set defaultPrefs to {outputText:"", fullFilePath:null, fileName:null, fileDirectory:(path to desktop) as string, appendText:false, appendLine:true}
set prefs to prefs & defaultPrefs
set outputText to outputText of prefs
-- determine file path
if fullFilePath of prefs is not null then
set outputFile to fullFilePath of prefs
else if fileName of prefs is not null then
set outputFile to fileDirectory of prefs & fileName of prefs
else
set outputFile to ((path to desktop) as string) & "ASFileWrittenBy_htcLib.txt"
end if
-- now write output text to file
try
try
set fileReference to open for access file outputFile with write permission
on error
set fileReference to open for access POSIX file outputFile with write permission
end try
if appendText of prefs is false then
set eof of the fileReference to 0
else if appendLine of prefs is true then
set outputText to return & outputText
end if
write outputText to fileReference starting at eof
close access fileReference
return true
on error
try
close access file outputFile
return true
end try
end try
return false
end writeToFile
So, if anyone has come across this and figured out a way to access sub-reminders or other “new” properties that don’t appear in the dictionary for Reminders.app, I would love to know.