🚀 Executive Summary

TL;DR: Manually transferring completed Todoist tasks to an Evernote daily log is a time-consuming chore. This guide provides a Python script to automate syncing completed tasks to a timestamped daily note in Evernote, significantly improving workflow efficiency with zero manual effort.

🎯 Key Takeaways

  • The solution leverages `todoist-api-python` and `evernote3` libraries to programmatically interact with Todoist and Evernote APIs, fetching completed tasks and updating notes.
  • API tokens are securely managed using `python-dotenv` by storing them in a `config.env` file, preventing hardcoding of credentials directly in the source code.
  • The `pytz` library is crucial for robust timezone handling, standardizing on UTC to prevent ‘wrong day’ issues when fetching tasks from Todoist, which logs events in UTC.

Syncing Todoist Completed Tasks to a Daily Log in Evernote

Syncing Todoist Completed Tasks to a Daily Log in Evernote

Alright, let’s talk about a real time-saver. I’m a big believer in logging what I’ve accomplished, but manually copying my completed tasks from Todoist to my Evernote daily log was a mindless chore. I calculated I was wasting nearly two hours a month on it—time that’s better spent on actual engineering, not admin. Automating this sync was one of the best ‘quality of life’ improvements I’ve made to my workflow. It gives me a perfect, timestamped record of my daily output with zero manual effort.

This guide will walk you through setting up a Python script to do just that. It’s a “set it and forget it” solution that will save you that same headache.

Prerequisites

Before we dive in, make sure you have the following ready:

  • A Todoist account.
  • An Evernote account (the script works with both free and paid plans).
  • Python 3.x installed on your machine or a server where you’ll run the script.
  • Basic comfort with Python and managing API keys.

The Guide: Step-by-Step

Step 1: Get Your API Keys

First things first, we need our keys to the kingdom. Treat these like passwords; never commit them directly into your code.

  1. Todoist API Token: Log in to your Todoist account, go to Settings > Integrations > Developer, and copy your API token.
  2. Evernote Developer Token: This one requires a few more clicks. Go to the Evernote Developers portal (https://dev.evernote.com/get-token/). I recommend generating a token for the production server unless you’re just testing. Make sure it has permissions to read and write notes.

Pro Tip: Store these keys somewhere secure like a password manager right away. You’ll need them in a moment, but you don’t want them lying around in a text file on your desktop.

Step 2: Project Setup and Dependencies

I’ll skip the standard virtual environment setup since you likely have your own workflow for that. The key is to create an isolated project directory to keep things clean. Once you’re in your project folder, you’ll need to install a few Python libraries. We’ll be using:

  • todoist-api-python: The official client for the Todoist API.
  • evernote3: A solid, community-maintained client for the Evernote API.
  • python-dotenv: For loading our secret keys from a configuration file instead of hardcoding them.
  • pytz: To handle timezones correctly, which is a common source of bugs in these kinds of integrations.

You can install these using pip, your friendly Python package installer.

Step 3: Create the Configuration File

In your project’s root directory, create a file named config.env. This is where we’ll securely store our API keys. Your script will read from this file so you never have to expose your credentials in the source code itself. In my production setups, this file is managed by our secrets management system and never checked into version control.

Add your keys to the config.env file like this:

TODOIST_API_TOKEN="your_todoist_api_token_here"
EVERNOTE_DEV_TOKEN="your_evernote_developer_token_here"
EVERNOTE_NOTEBOOK_NAME="Daily Logs"

Step 4: The Python Script – The Core Logic

Now for the fun part. Create a file named sync_tasks.py. I’ll break down the code section by section so you understand the logic behind each part.

First, we’ll import the necessary libraries and load our environment variables from the config.env file.

import os
import datetime
import pytz
from dotenv import load_dotenv
from todoist_api_python.api import TodoistAPI
from evernote.api.client import EvernoteClient
import evernote.edam.type.ttypes as Types
import evernote.edam.notestore.ttypes as NoteStoreTypes

# Load environment variables from config.env
load_dotenv('config.env')

# --- Configuration ---
TODOIST_TOKEN = os.getenv('TODOIST_API_TOKEN')
EVERNOTE_TOKEN = os.getenv('EVERNOTE_DEV_TOKEN')
NOTEBOOK_NAME = os.getenv('EVERNOTE_NOTEBOOK_NAME')
TIMEZONE = 'UTC' # I recommend UTC to avoid daylight saving issues

Next, we write the main function that orchestrates the entire process. This function will initialize the API clients, get the tasks, and update the note.

def main():
    # Initialize API clients
    try:
        todoist_api = TodoistAPI(TODOIST_TOKEN)
        # For production, use client = EvernoteClient(token=EVERNOTE_TOKEN)
        # For sandbox, use client = EvernoteClient(token=EVERNOTE_TOKEN, sandbox=True)
        evernote_client = EvernoteClient(token=EVERNOTE_TOKEN, sandbox=False)
        note_store = evernote_client.get_note_store()
    except Exception as e:
        print(f"Error initializing API clients: {e}")
        return

    # Get today's date in the specified timezone
    local_tz = pytz.timezone(TIMEZONE)
    today = datetime.datetime.now(local_tz).date()

    # Find the target notebook
    notebook_guid = find_notebook_guid(note_store, NOTEBOOK_NAME)
    if not notebook_guid:
        print(f"Notebook '{NOTEBOOK_NAME}' not found.")
        return

    # Get or create the daily log note
    daily_note = get_or_create_daily_note(note_store, notebook_guid, today)
    if not daily_note:
        print("Could not get or create the daily note.")
        return

    # Get completed tasks from Todoist for today
    completed_tasks = get_completed_tasks_for_today(todoist_api, local_tz)
    if not completed_tasks:
        print("No tasks completed today. Exiting.")
        return

    # Append tasks to the note
    append_tasks_to_note(note_store, daily_note, completed_tasks)

if __name__ == "__main__":
    main()

Here are the helper functions called by main(). I’ve added comments to explain what each one does.

def find_notebook_guid(note_store, notebook_name):
    """Finds the GUID of a notebook by its name."""
    notebooks = note_store.listNotebooks()
    for notebook in notebooks:
        if notebook.name == notebook_name:
            return notebook.guid
    return None

def get_completed_tasks_for_today(api, local_tz):
    """Fetches tasks completed since the beginning of today from Todoist."""
    today_start = local_tz.localize(datetime.datetime.combine(datetime.date.today(), datetime.time.min))
    
    try:
        # The 'activity' endpoint is the right way to get completed items
        activity = api.get_activity(event_type='item_completed', limit=100)
        
        completed_tasks = []
        for event in activity['events']:
            event_date = datetime.datetime.fromisoformat(event['event_date'].replace('Z', '+00:00'))
            if event_date >= today_start:
                completed_tasks.append(event['extra_data']['content'])
        
        return completed_tasks
    except Exception as e:
        print(f"Error fetching tasks from Todoist: {e}")
        return []

def get_or_create_daily_note(note_store, notebook_guid, today):
    """Finds a daily note for today or creates a new one if it doesn't exist."""
    note_title = f"Daily Log: {today.strftime('%Y-%m-%d')}"
    
    # Search for an existing note
    filter = NoteStoreTypes.NoteFilter()
    filter.notebookGuid = notebook_guid
    filter.words = f'intitle:"{note_title}"'
    
    spec = NoteStoreTypes.NotesMetadataResultSpec(includeTitle=True)
    notes_metadata = note_store.findNotesMetadata(filter, 0, 1, spec)
    
    if notes_metadata.notes:
        note_guid = notes_metadata.notes[0].guid
        return note_store.getNote(note_guid, True, False, False, False)
    else:
        # Create a new note if one isn't found
        print(f"Creating new note: {note_title}")
        note = Types.Note()
        note.title = note_title
        note.notebookGuid = notebook_guid
        note.content = '<?xml version="1.0" encoding="UTF-8"?>'
        note.content += '<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">'
        note.content += '<en-note><div>Daily log started.</div></en-note>'
        return note_store.createNote(note)

def append_tasks_to_note(note_store, note, tasks):
    """Appends a list of tasks to an Evernote note, avoiding duplicates."""
    sync_header = f"--- Todoist Sync at {datetime.datetime.now().strftime('%H:%M:%S')} ---"
    
    # Check if a sync has already run today to avoid duplicate entries
    if "--- Todoist Sync at" in note.content:
        print("A sync has already been performed on this note today. Appending new items if any.")
    
    # Build the HTML content for the tasks
    task_list_html = "".join([f"<li><en-todo/>{task}</li>" for task in tasks])
    
    # Construct the full content to append
    content_to_append = f"<h2>{sync_header}</h2><ul>{task_list_html}</ul>"
    
    # Evernote's content is XML-based (ENML), so we need to insert our new content
    # before the closing </en-note> tag.
    note.content = note.content.replace('</en-note>', f'{content_to_append}</en-note>')
    
    try:
        note_store.updateNote(note)
        print(f"Successfully appended {len(tasks)} tasks to note '{note.title}'.")
    except Exception as e:
        print(f"Error updating Evernote note: {e}")

Pro Tip: Notice the logic in append_tasks_to_note. It adds a timestamped header each time it runs. This is a simple form of idempotency. If you accidentally run the script twice, you’ll see two timestamped lists, which makes it easy to spot and clean up any duplicates, rather than silently adding the same tasks over and over.

Step 5: Automate the Script with Cron

A script is only useful if you don’t have to remember to run it. On a Linux or macOS system, a cron job is the standard way to schedule tasks. You can set it to run every evening to capture your day’s work.

To edit your cron jobs, you’d typically run a command in your terminal to open the cron table. A line to run our script every day at 11 PM would look like this:

0 23 * * * python3 sync_tasks.py

Remember to use the correct path to your python executable and script. I recommend using an absolute path in a real cron job, but for the sake of these instructions, we’re keeping it simple. This command assumes your `sync_tasks.py` script is in the directory from which cron executes.

Common Pitfalls

I’ve set up a few of these, and here’s where I usually mess up:

  • Timezone Mismatches: This is the biggest one. Todoist logs everything in UTC. If your script runs on a server in a different timezone and you don’t explicitly handle it (like we did with pytz), you’ll get tasks from the “wrong” day. I always standardize on UTC in my scripts to prevent this.
  • API Rate Limits: For this simple script, you won’t hit any limits. But if you expand it to sync hundreds of items frequently, be aware that both Todoist and Evernote have rate limits. The clients we’re using don’t handle this automatically, so you’d need to add retry logic.
  • Incorrect Evernote Token Permissions: If your script can read notes but fails on the update, double-check that you generated your Evernote Developer Token with full read/write permissions.

Conclusion

And there you have it. With a single Python script and a cron job, you’ve created a permanent, automated log of your accomplishments. This is a foundational piece of automation that you can build on. You could extend it to pull in your calendar events, git commits, or any other data source with an API to create a truly comprehensive daily journal.

This is exactly the kind of small, high-impact project we love in DevOps. It solves a real problem, saves time, and provides lasting value. Happy automating!

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ How can I automatically log Todoist completed tasks into Evernote?

Automate this by developing a Python script that utilizes the `todoist-api-python` and `evernote3` libraries. The script fetches completed tasks from Todoist’s `activity` endpoint and appends them as ENML content to a daily log note in a specified Evernote notebook, then schedules it via a cron job.

âť“ What are the advantages of this custom Python script over existing integration platforms?

This custom Python script offers fine-grained control over the integration logic, avoids reliance on third-party services (like Zapier or IFTTT) which might have limitations, costs, or less flexibility, and provides a self-hosted, open-source solution for deep customization.

âť“ What is a common issue when implementing this Todoist-Evernote sync and how is it resolved?

A common pitfall is timezone mismatches, where Todoist’s UTC logging conflicts with the local server time. This is resolved by explicitly handling timezones using the `pytz` library and standardizing on UTC within the script to ensure tasks are correctly attributed to the current day.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading