đ Executive Summary
TL;DR: This guide provides a Python script to automatically sync tasks from Todoist to Microsoft To Do, addressing the inefficiency of manual task duplication across platforms. It leverages the Todoist and Microsoft Graph APIs to create a unified task management experience for SysAdmins, Developers, and DevOps Engineers.
đŻ Key Takeaways
- API Authentication requires a Todoist API Token and an Azure AD application registration for Microsoft Graph, providing Client ID, Client Secret, Tenant ID, and an initial refresh token for non-interactive access.
- The Python script orchestrates the sync by fetching Todoist tasks, obtaining/refreshing Microsoft Graph access tokens, identifying or creating a designated Microsoft To Do list, and creating new tasks while preventing duplicates based on task titles.
- For continuous synchronization, the script can be scheduled using tools like
cron. Key considerations include handling API rate limits with exponential backoff, managing Microsoft Graph access token expiry via refresh tokens, and ensuring correct Azure AD application permissions (Tasks.ReadWrite, offline_access).
Syncing Todoist Tasks with Microsoft To Do via API
Introduction
In todayâs fast-paced digital environment, managing tasks across various platforms is a common challenge for SysAdmins, Developers, and DevOps Engineers. While tools like Todoist excel in personal productivity and rapid task capture, Microsoft To Do often integrates seamlessly into organizational ecosystems, especially for those heavily invested in Microsoft 365. The friction of manually duplicating tasks, switching contexts, and the risk of overlooking critical items due to platform segregation can significantly impede efficiency.
Imagine a world where your quick ânote to selfâ in Todoist automatically appears in your work-focused Microsoft To Do list, without a single manual copy-paste. This tutorial aims to bring that vision to life. We will walk you through the process of building a simple Python script to automatically sync your Todoist tasks with Microsoft To Do using their respective APIs, providing a unified task management experience and liberating you from tedious manual synchronization.
Prerequisites
Before we dive into the integration, ensure you have the following:
- Python 3.x: Installed on your system. You can download it from the official Python website.
- pip: Pythonâs package installer, usually included with Python 3.x.
- Todoist Account: With an active subscription (or a free account if the features you need are covered).
- Todoist API Token: This is your personal authentication key for the Todoist API.
- Microsoft 365 Account: Required to use Microsoft To Do and its underlying Microsoft Graph API.
- Azure AD Application Registration: Youâll need an Azure Active Directory application registered in your tenant with appropriate permissions for Microsoft Graph API. This will provide you with a Client ID, Client Secret, and Tenant ID.
- Microsoft Graph API Refresh Token: For non-interactive access to Microsoft To Do, you will need an initial refresh token obtained via an OAuth2 authorization flow. This tutorial assumes you have a refresh token.
Step-by-Step Guide: Syncing Tasks via API
Step 1: Obtain API Credentials and Set Up Azure AD Application
1.1 Todoist API Token
Log in to your Todoist account. Navigate to âSettingsâ > âIntegrationsâ > âDeveloperâ. You will find your âAPI tokenâ there. Copy this token; itâs essential for authenticating your requests to the Todoist API.
1.2 Microsoft Graph API Credentials (Azure AD App)
To interact with Microsoft To Do, youâll use the Microsoft Graph API. This requires registering an application in Azure Active Directory:
- Go to the Azure portal and sign in.
- Navigate to âAzure Active Directoryâ > âApp registrationsâ > âNew registrationâ.
- Give your application a name (e.g., âTodoistToToDoSyncâ).
- For âSupported account typesâ, select âAccounts in any organizational directory (Any Azure AD directory â Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)â.
- For âRedirect URIâ, select âWebâ and enter
http://localhost:8000(or any valid redirect URI for development; for production, use a secure endpoint). - Once registered, note down the âApplication (client) IDâ and âDirectory (tenant) IDâ from the applicationâs âOverviewâ page.
- Go to âCertificates & secretsâ > âNew client secretâ. Add a description and select an expiry. Copy the âValueâ of the secret immediately after creation, as it wonât be visible again.
- Go to âAPI permissionsâ > âAdd a permissionâ > âMicrosoft Graphâ > âDelegated permissionsâ. Search for and add
Tasks.ReadWriteandoffline_access. - Grant admin consent for the added permissions (if available and necessary for your tenant).
Note on Refresh Token: For non-interactive scripts, you need a refresh token. This is typically obtained by performing an initial OAuth2 authorization flow (e.g., using a tool like Postman, or a small web application to generate the initial token). Once you have a refresh token, you can use it to get new access tokens without user interaction. This tutorial assumes you have already obtained and will store your refresh token.
Step 2: Set Up Your Development Environment
Create a project directory and set up your Python environment.
mkdir todoist_todo_sync
cd todoist_todo_sync
python3 -m venv venv
source venv/bin/activate <!-- On Windows, use `venv\Scripts\activate` -->
Install the necessary Python packages:
pip install requests python-dotenv
Create a file named requirements.txt:
requests
python-dotenv
Create a configuration file named config.env to store your API keys and secrets securely:
TODOIST_API_TOKEN="your_todoist_api_token_here"
MS_GRAPH_CLIENT_ID="your_azure_ad_client_id"
MS_GRAPH_CLIENT_SECRET="your_azure_ad_client_secret"
MS_GRAPH_TENANT_ID="your_azure_ad_tenant_id"
MS_GRAPH_REFRESH_TOKEN="your_microsoft_graph_refresh_token"
MS_TODO_LIST_NAME="Todoist Sync" <!-- The name of the To Do list to sync to -->
Replace the placeholder values with your actual credentials.
Step 3: Develop the Sync Script
Create a Python file named sync_script.py. This script will handle authentication, task retrieval, and task synchronization.
3.1 Authenticate with Microsoft Graph API
First, we need a way to obtain a fresh access token for Microsoft Graph using our refresh token.
import os
import requests
from dotenv import load_dotenv
load_dotenv('config.env')
# Microsoft Graph API Configuration
MS_GRAPH_CLIENT_ID = os.getenv("MS_GRAPH_CLIENT_ID")
MS_GRAPH_CLIENT_SECRET = os.getenv("MS_GRAPH_CLIENT_SECRET")
MS_GRAPH_TENANT_ID = os.getenv("MS_GRAPH_TENANT_ID")
MS_GRAPH_REFRESH_TOKEN = os.getenv("MS_GRAPH_REFRESH_TOKEN")
def get_ms_graph_access_token():
url = f"https://login.microsoftonline.com/{MS_GRAPH_TENANT_ID}/oauth2/v2.0/token"
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"client_id": MS_GRAPH_CLIENT_ID,
"client_secret": MS_GRAPH_CLIENT_SECRET,
"refresh_token": MS_GRAPH_REFRESH_TOKEN,
"grant_type": "refresh_token",
"scope": "Tasks.ReadWrite offline_access User.Read" # User.Read is often implicitly needed
}
try:
response = requests.post(url, headers=headers, data=data)
response.raise_for_status() # Raise an exception for HTTP errors
token_data = response.json()
print("Successfully obtained Microsoft Graph access token.")
return token_data.get("access_token"), token_data.get("refresh_token")
except requests.exceptions.RequestException as e:
print(f"Error getting MS Graph access token: {e}")
print(f"Response: {response.text if 'response' in locals() else 'No response'}")
return None, None
# Update the refresh token if a new one is returned (good practice for long-lived tokens)
def update_refresh_token(new_refresh_token):
if new_refresh_token and new_refresh_token != MS_GRAPH_REFRESH_TOKEN:
print("New refresh token received. Updating config.env...")
with open('config.env', 'r') as f:
lines = f.readlines()
with open('config.env', 'w') as f:
for line in lines:
if line.startswith('MS_GRAPH_REFRESH_TOKEN='):
f.write(f'MS_GRAPH_REFRESH_TOKEN="{new_refresh_token}"\n')
else:
f.write(line)
global MS_GRAPH_REFRESH_TOKEN
MS_GRAPH_REFRESH_TOKEN = new_refresh_token
print("Refresh token updated.")
# In a real scenario, you'd call update_refresh_token
# For simplicity, we'll just get the token for now.
# access_token, new_refresh_token = get_ms_graph_access_token()
# if access_token:
# update_refresh_token(new_refresh_token)
Logic Explanation: This function uses your client ID, client secret, and existing refresh token to request a new access token from Microsoftâs OAuth 2.0 endpoint. Access tokens are short-lived, so using a refresh token is crucial for long-running scripts. It also handles updating the refresh token in your config.env if a new one is issued, ensuring continued access.
3.2 Fetch Todoist Tasks
Next, weâll retrieve tasks from your Todoist inbox or a specific project.
# Todoist API Configuration
TODOIST_API_TOKEN = os.getenv("TODOIST_API_TOKEN")
def get_todoist_tasks():
url = "https://api.todoist.com/rest/v2/tasks"
headers = {
"Authorization": f"Bearer {TODOIST_API_TOKEN}"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
tasks = response.json()
print(f"Fetched {len(tasks)} tasks from Todoist.")
return tasks
except requests.exceptions.RequestException as e:
print(f"Error fetching Todoist tasks: {e}")
return []
Logic Explanation: This function makes a GET request to the Todoist APIâs tasks endpoint, authenticating with your personal API token. It retrieves all active tasks associated with your account.
3.3 Sync Tasks to Microsoft To Do
Now, letâs put it all together. Weâll get your To Do list ID, fetch existing tasks from To Do, and then iterate through Todoist tasks to create new ones in Microsoft To Do if they donât already exist.
MS_TODO_LIST_NAME = os.getenv("MS_TODO_LIST_NAME")
def get_ms_todo_list_id(access_token):
url = "https://graph.microsoft.com/v1.0/me/todo/lists"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
lists = response.json().get("value", [])
for todo_list in lists:
if todo_list.get("displayName") == MS_TODO_LIST_NAME:
print(f"Found Microsoft To Do list: '{MS_TODO_LIST_NAME}' with ID: {todo_list['id']}")
return todo_list["id"]
# If list not found, create it
print(f"Microsoft To Do list '{MS_TODO_LIST_NAME}' not found. Creating it...")
create_url = "https://graph.microsoft.com/v1.0/me/todo/lists"
create_data = {"displayName": MS_TODO_LIST_NAME}
create_response = requests.post(create_url, headers=headers, json=create_data)
create_response.raise_for_status()
new_list = create_response.json()
print(f"Created new list: '{new_list['displayName']}' with ID: {new_list['id']}")
return new_list["id"]
except requests.exceptions.RequestException as e:
print(f"Error getting or creating MS To Do list: {e}")
return None
def get_ms_todo_tasks(access_token, todo_list_id):
url = f"https://graph.microsoft.com/v1.0/me/todo/lists/{todo_list_id}/tasks"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
tasks = response.json().get("value", [])
print(f"Fetched {len(tasks)} tasks from Microsoft To Do list '{MS_TODO_LIST_NAME}'.")
return tasks
except requests.exceptions.RequestException as e:
print(f"Error fetching MS To Do tasks: {e}")
return []
def create_ms_todo_task(access_token, todo_list_id, task_title, due_date=None):
url = f"https://graph.microsoft.com/v1.0/me/todo/lists/{todo_list_id}/tasks"
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
task_data = {
"title": task_title
}
if due_date:
task_data["dueDateTime"] = {
"dateTime": due_date, # Format: YYYY-MM-DDTHH:MM:SSZ (or YYYY-MM-DD)
"timeZone": "UTC" # Or your local timezone
}
try:
response = requests.post(url, headers=headers, json=task_data)
response.raise_for_status()
print(f"Created task in MS To Do: '{task_title}'")
return response.json()
except requests.exceptions.RequestException as e:
print(f"Error creating task in MS To Do ('{task_title}'): {e}")
print(f"Response: {response.text if 'response' in locals() else 'No response'}")
return None
def main():
ms_access_token, new_refresh_token = get_ms_graph_access_token()
if not ms_access_token:
print("Failed to get Microsoft Graph access token. Exiting.")
return
update_refresh_token(new_refresh_token) # Update refresh token if a new one was issued
ms_todo_list_id = get_ms_todo_list_id(ms_access_token)
if not ms_todo_list_id:
print("Failed to get or create Microsoft To Do list. Exiting.")
return
todoist_tasks = get_todoist_tasks()
ms_todo_tasks = get_ms_todo_tasks(ms_access_token, ms_todo_list_id)
# Simple mechanism to avoid duplicates: use task title for comparison
ms_todo_task_titles = {task["title"] for task in ms_todo_tasks}
synced_count = 0
for task in todoist_tasks:
task_title = task["content"]
if task_title not in ms_todo_task_titles:
due_date = None
if task.get("due") and task["due"].get("date"):
# Todoist date format is YYYY-MM-DD, suitable for Graph API
due_date = task["due"]["date"]
# If you need time, Todoist has 'datetime' and 'string' options
# For simplicity, using just date here.
created_task = create_ms_todo_task(ms_access_token, ms_todo_list_id, task_title, due_date)
if created_task:
synced_count += 1
else:
print(f"Warning: Could not sync Todoist task: '{task_title}'")
else:
print(f"Task already exists in MS To Do: '{task_title}'. Skipping.")
print(f"Synchronization complete. {synced_count} tasks synced from Todoist to Microsoft To Do.")
if __name__ == "__main__":
main()
Logic Explanation:
- The
mainfunction orchestrates the process: it first obtains a Microsoft Graph access token. - It then identifies (or creates if not present) the target Microsoft To Do list named âTodoist Syncâ.
- It fetches all active tasks from Todoist.
- It retrieves existing tasks from the designated Microsoft To Do list.
- To prevent duplicates, it compares task titles. If a Todoist taskâs title doesnât exist in the Microsoft To Do list, a new task is created in Microsoft To Do.
- Basic due date handling is included, translating Todoistâs due date to the Graph APIâs expected format.
Step 4: Execute and Schedule the Script
To run your script, simply execute it from your terminal:
python3 sync_script.py
For continuous synchronization, you can schedule this script to run periodically. On Linux/Unix systems, you can use a cron job:
crontab -e
Add a line like the following to run the script every 30 minutes:
*/30 * * * * /usr/bin/python3 /home/user/todoist_todo_sync/sync_script.py >> /tmp/logs/todoist_sync.log 2>&1
Ensure the paths to your Python executable and script are correct. Create the /tmp/logs/ directory if it doesnât exist.
Common Pitfalls
- API Rate Limits: Both Todoist and Microsoft Graph APIs have rate limits. If youâre syncing a large number of tasks frequently, you might hit these limits. Implement exponential backoff for retries to handle this gracefully.
-
Authentication Token Expiry: Microsoft Graph access tokens are short-lived (typically 1 hour). The provided script uses a refresh token to obtain new access tokens. Ensure your refresh token is properly stored and updated in
config.env. If your initial refresh token expires (e.g., after 90 days of inactivity), youâll need to re-authenticate interactively to get a new one. -
Permission Issues (Microsoft Graph): Double-check that your Azure AD application has the necessary delegated permissions (
Tasks.ReadWriteandoffline_access). If admin consent is required for your tenant, ensure it has been granted. - Duplicate Tasks: The current script uses a simple title-based check for duplication. For more robust syncing, you might consider storing a mapping of Todoist task IDs to Microsoft To Do task IDs in a small local database or a custom extended property on the To Do tasks themselves. This would allow for updates and deletions, not just creations.
Conclusion
By leveraging the power of APIs, weâve successfully built a practical solution to bridge the gap between Todoist and Microsoft To Do. This automation not only saves valuable time previously spent on manual data entry but also enhances your productivity by consolidating your task management view. As a SysAdmin, Developer, or DevOps Engineer, such integrations are key to building efficient workflows and reducing operational overhead.
From here, you can expand this script significantly. Consider implementing bi-directional synchronization, adding support for task completion status, priorities, or labels. You might also explore deploying this as a serverless function (e.g., Azure Functions or AWS Lambda) or as a containerized application for more robust and scalable operation. The foundation is laid; the possibilities are endless.
đ Read the original article on TechResolve.blog
â Support my work
If this article helped you, you can buy me a coffee:

Top comments (0)