Backup / migrate Microsoft To Do tasks with PowerShell and Microsoft Graph

Update: this post has a spiritual successor – Extract Microsoft To Do steps/sub-tasks from your web browser (with Asana import example)

For more than a year, I’ve foolishly been using a developer Office 365 subscription for some personal stuff. You know, the subscription where they delete your data if “development activity” isn’t detected every few months. As such, I’ve periodically had to fake some development activity in order to keep the clock ticking.

Not a sustainable situation, and it’s time to sort it out…

For me, this involves moving data from one subscription’s OneDrive to another. I’m fairly confident that Rclone will be able to handle this – it’s an excellent bit of software.

It also means moving Microsoft To Do tasks between subscriptions. Ah.

Not so easy

I couldn’t find an easy way of backing up To Do. There is mention of an Outlook backup option in the docs, but it’s missing on my account. And To Do will happily suck in data from Wunderlist but I can’t see an equivalent to get data out. Where’s the Justice Department when you need them?

Luckily Microsoft Graph has a To Do API in preview and I was able to put together a script to do the lifting for me.

Ironically, this has involved an intense burst of real developer activity…

Enter the Dragon

The full script is over on GitHub.

It provides two functions:

  • Export-MicrosoftTodo saves every Microsoft To Do list and task to an XML file.
  • Import-MicrosoftTodo loads this XML file and restores all lists and tasks.

This is what a backup looks like:

Here’s a restore:

And this is what the client sees:

You can see that the completed status of the tasks has been copied. This is also true of created/modified dates, reminders, notes, and so on.

The script is quite long, so I won’t paste the whole thing here, but here are a few interesting bits:

Emotional support

Thankfully, for the comfort of our technology-addled minds, Microsoft To Do lets you decorate your lists with little emojis. Internally, it looks like if the first character of the name is an emoji, it gets special treatment in the UI.

I was having difficulty creating these special list names but the fix was simply to add charset=utf-8 to Invoke-RestMethod‘s ContentType:

$params = @{
    "Method"         = "Post"
    "Uri"            = ($graphBaseUri + "/me/todo/lists")
    "Authentication" = "OAuth"
    "Token"          = $accessToken
    "Body"           = @{
        "displayName" = $list.displayName
    } | ConvertTo-Json
    # utf-8 makes emojis work. Life priorities are correct.
    "ContentType"    = "application/json; charset=utf-8"
}
Invoke-RestMethod @params

Before and after:

Jason Bateman

The basic aim of the script is to retrieve data from one API endpoint and to later submit the same data to another endpoint.

I found that PowerShell’s – er – adorable magic got in the way a bit. Specifically, the JSON (de-)serialisation done by Convert*-Json / Invoke-RestMethod didn’t preserve empty properties, and the conversion to/from a DateTime object didn’t match the format expected by the API – and I couldn’t see an easy way to override this behaviour.

My solution was to use an alternative JSON parser available in .NET to grab the appropriate bit of the HTTP response, remove a few properties, and store the resulting JSON as a string, to be later POSTed back to the API verbatim:

$response = Invoke-WebRequest -Uri $uri -Authentication OAuth -Token $accessToken
# Invoke-RestMethod / ConvertFrom-Json mangles the response, which I resent,
# so we're using an alternative parser and storing the JSON as a string
# https://stackoverflow.com/a/58169326/12055271
$json = [Newtonsoft.Json.JsonConvert]::DeserializeObject($response.Content)
ForEach ($task in $json.value) {
    # Don't need ID - Graph API can generate a new one
    $task.Remove("id") | Out-Null
    # Don't need ETag
    $task.Remove("@odata.etag") | Out-Null
    $results += $task.ToString()
}

I also chose to save the exported data to disk using PowerShell’s CLI XML format – rather than JSON – as an easy way of guarantee the string stays as it is.

Token effort

The script needs an OAuth2 token in order to authenticate with your Microsoft account.

An easy way to get going (and slightly hacky but fine for personal use) is to grant yourself all Tasks.* permissions in Graph Explorer and copy its token, as demoed here:

(Thanks GoToGuy for this blog post.)

Please read the following license agreement carefully

A few notes on the design of the script:

  • It worked well for me and 5000(!) tasks, but please do your own testing. You can create a test Microsoft account with a secondary email address, or make an Azure tenant for free.
  • Tested with PowerShell 7 only. Get with the times.
  • Export-MicrosoftTodo currently backs up every task and Import-MicrosoftTodo restores every task.
  • If you run Import-MicrosoftTodo twice you’ll end up with duplicates.
  • The account used for export/import is the one that generated the OAuth token. You can backup from one account and restore to another simply by providing different tokens.
  • The script does not currently migrate linkedResources – these “represent[…] an item in a partner application related to a todoTask.” Shrug.
  • Nor does it share any lists as part of data import.
  • Currently, the script needs to be run interactively, in order to receive the OAuth token and to confirm a restore.
  • I’d be open to making improvements in these areas if there’s interest! The script could backup individual lists, for example, or backup someone else’s account (with the appropriate permissions).
  • Unfortunately, I don’t think there’s currently any way to retain list groups.

And in conclusion

Thanks for reading!

This has been a fun project and hopefully you can get some use out of the methods used or the script itself.

12 thoughts on “Backup / migrate Microsoft To Do tasks with PowerShell and Microsoft Graph

  1. This looks like a great solution, Dan. I’ve seen no other way to export Microsoft To Do data in a structured format, other than their proprietary Outlook export which doesn’t help.

    I was using Wunderlist extensively before I migrated everything to To Do last year… and now I realize it was a one-way valve.

    Is there any chance I could hire you to help me get my data out of To Do and into a structured XML file that I can then migrate to Asana? Please shoot me an email if you get this message. Thanks in advance!

      1. Hi both!

        Unfortunately, I don’t think To Do Steps are migrated. When I came to move my personal data, I noticed this, and ended up manually copying some items I cared about.

        Support for Steps would be a great addition to the script but it’s not something I can give an ETA on right now.

  2. As a non-coder just starting to learn R and minor HTML/git/vim/terminal actions, do you anticipate any of your steps for export being irreversibly risky, or should error messages along the way help guide me to resolve hurdles?

    Thank you!

    1. Hi Steve,

      The export process only reads data and it should be safe to use.

      Just make sure that you’ve read the jokey “license agreement” section of the post which explains the script’s limitations.

      The import process is slightly more risky in theory, as it writes data, but the only danger I’m aware of is that you might end up with duplicate data if you run multiple imports.

  3. When I start the scrpit with .\backup….ps1 and agree with M on execution nothing happens. What am I doing wrong?

    1. first make sure you have powershell 7 installed

      next you need to source the script, so if you are already in the directory you would do something like this to source

      . .\backup_restore_microsoft_todo.ps1

      the first ” . ” is the sourcing, the second is the path to local file

      Then you can run

      Export-MicrosoftTodo

      you will be prompted to enter you oauth token and it should work.

  4. Thx for the script…exactly what I needed to port from work acct to personal acct (I mixed the two way back when). However the script is consistently throwing an error (below). Process: On work laptop (highly restricted, can’t run Powershell), I logon with work acct, grab access token, paste into an email. On personal laptop, open email, copy token , paste into Powershell (where I have rights) at the OAuth prompt (FYI…shows one “*”), hit Enter and errors out. Total elapsed time: 30-45 sec. Thoughts?

    Line |
    14 | $me = Invoke-RestMethod -Uri ($graphBaseUri + “/me”) -Authenticat …
    | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    | {“error”:{“code”:”InvalidAuthenticationToken”,”message”:”CompactToken parsing failed with error code:
    | 80049217″,”innerError”:{“date”:”2021-11-12T16:44:58″,”request-id”:”c30a7e5f-112a-407d-xxx-xxxxxxxxx”,”client-request-id”:”c30a7e5f-112a-407d-xxx-xxxxxxxxx”}}}

    1. So I plugged away at this on my own. Got it to work but only after I modified all instances of how variable $accessToken was saved to this:

      $accessToken = ConvertTo-SecureString “pastedOAuthTokenhere” -AsPlainText -Force

      Also interesting was that all lists/tasks loaded except one…even though it showed as having processed 101 tasks. May edit XML file and try just that one again. Thx again for script…got me there in the end.

  5. I’m a newbie and I’m making some progress now , I can perform the step of pasting the token , But after pasting the token, the following error message appears

    Invoke-RestMethod: C:\Users\jasonwang115\Desktop\todo\Todo.ps1:14
    Line |
    14 | $me = Invoke-RestMethod -Uri ($graphBaseUri + “/me”) -Authenticat …
    | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    | Bad Request Bad Request HTTP Error 400. The request is badly formed.

    I saw other brothers mention changing line 14 to
    $accessToken = ConvertTo-SecureString “pastedOAuthTokenhere” -AsPlainText -Force

    But it doesn’t work for me, and there is an extra line 21 error

    Just one last step, please help

Leave a Reply

Your email address will not be published. Required fields are marked *