How to View “All Tasks” Grouped by Project with Todoist

by | Oct 17, 2020 | Automation, Code, Todoist

I absolutely love Todoist. But one of the things that really bothers me is the “View All” view. I especially wish I could adjust the view to show everything segmented / separated by project.

Well if you’re willing to do a bit of technical stuff 🤓, I can show you how to accomplish this. You only have to set it up one time.

But first let me explain how it works.

If you want to separate your tasks by project manually, the only way to accomplish this is to create a Filter that specifically lists out each project separated by commas. Basically this is like connecting a bunch of filter queries together with commas.

So you’re writing a query that includes every project you want included ##Project 1, ##Project 2, etc.

Then you’ll get a nice view of all the tasks separated by project in the order you wrote your filter query.

So obviously this would suck to maintain manually since every time you add a new project, change it’s name, remove a project, etc., your filter would break and you would need to manually update it.

Well let me introduce you to the power of APIs. I jumped into Todoist’s API documentation and figured out how to automatically grab all my projects and update a filter with a query that will show me all my tasks from all my projects, separated by project.

Don’t worry, you don’t need to know how to code. I’m going to show you how to upload a special bit of code into Google Cloud and have it run and update automatically for you… for free!

Basically every hour (or whatever you want to set the interval to) something called a Google Cloud Function will run a piece of code that grabs all your projects and all their tasks and updates a custom “View All” filter in Todoist.

Filter ID

Ok the first thing you’re going to need is to pick a filter (or create a new one) that you want to be your special “View All” view. I made one called ⛏️ View All. I like to use emojis on everything I use frequently since the human brain can find and parse information visually much faster than reading text. But call yours whatever you want.

You’ll need to do this next part in a web browser (todoist.com/app). Once you have a filter you want to use you need to click on it. In the address bar you should notice the URL will say something like todoist.com/app/#filter%2F2290740. Copy the number that appears after #filter%2F and save it somewhere for later. This is your filter ID. In my case my filter ID is 2290740.

Todoist API Key

Now you need to get your Todoist API key. This is a special key that allows programs to make changes to your Todoist account via the Todoist API. Don’t share this with anyone.

To find yours go to todoist.com/prefs/integrations from a browser or you can get to it by clicking on:

  • The gear icon in the upper-right corner
  • Settings
  • Integrations

Scroll down and you’ll find your API key. It should look something like b0imfnc8mwsae3aaw6465jr50qtlx. Copy and save it somewhere. We’ll need it later.

Create a Google Cloud Function

Head over to console.cloud.google.com and make sure you’re logged in. If you don’t already have a Google Cloud account just follow my guide on how to create and setup a free Google Cloud Platform account.

Once you’re there click the menu and under the “Compute” section click on “Cloud Functions.” Then click “Create Function.”

Give your function a good descriptive name like todoist-filter-view-all.

Now under Trigger and Authentication choose Allow unauthenticated invocations. This means that anything or anyone with the URL for this trigger will be able to invoke it. This can be useful if you want to use a service like IFTTT or Zapier to trigger your Cloud Function. You can set it to Require Authentication if you don’t plan on using an external service to trigger it.

Now click “Save”

Click the “Variables, Networking and Advanced Settings” dropdown and set the “Memory allocated” dropdown to 128 MiB. This is optional but since this is a very simple cloud function you might as well lower it down to 128MiB to be more efficient.

Click “Next”!

Configure Your Cloud Function

Next is the fun part where all the action happens. We’re going to write the code that this Cloud Function will execute every time it is called. This code is what will update your Todoist filter with all that “view all” goodness and segment it by project. Yay! 🥳

In the “Runtime” dropdown select Python 3.7. At the time of this writing the only Python options are 3.7 and 3.8 and 3.8 didn’t work for me. It’s possible you may have higher versions. If so, just use whatever is available to you and hopefully it will work for you! 🤞😬 If not hit me up on the Twitters and I’ll see what I can do.

Now change the value under “Entry point” to update.

Add the Todoist Python Library

The good folks at Doist created a library that makes our job really easy. In order to add it click on requirements.txt and add a new line todoist-python.

Write the Codes

Ok now we just need to write the code that will make your shiny new cloud function do cool stuff. Click on main.py and erase everything in the code editor window to the right of it. We want to start with a clean slate.

Now copy and paste the following code in. But replace the key value with your own Todoist API key and replace the filter_id with the filter ID you saved earlier.

def update(request):
    import requests
    from todoist.api import TodoistAPI
    import json
    
    key = "12b3456abcdefg"
    filter_id = "123456789"
    api = TodoistAPI(key)
    api.sync()
    full_query = ""
    for item in api.state['projects']:
        name = item.data.get("name")
        query = "#" + name + ", "
        full_query = full_query + query
    full_query = full_query[:-2] #trim the last 2 characters (", ")
    filter = api.filters.get_by_id(filter_id)
    filter.update(query=full_query)
    api.commit()

If you only want to show tasks that are due today you can change line 13 from query = "#" + name + ", " to query = "#" + name + " & today, ". Just be very careful to maintain the spacing and indentations. These are very important in Python.

Hit that deploy button and you’re off to the races.

Give it some time… like go grab a cup of tea… use the john… deploying takes a few minutes. Once it’s done it’s time to test that bad boy out! Click on the eclipses (3 dots) and click “Test function.”

Then hit “Test the Function” on the next page.

Now run over to Todist and see if it worked!

Make Sure the Filter Works

If your filter view is spitting out tasks that’s a great sign. If it looks like you’re missing some stuff that might be because you have so many projects that you’ve exceeded the 1,024 character limit for filter queries. Right click on your filter and choose “Edit Filter.”

If your filter query is too long it will tell you here.

If this is happening to you, you’ll either need to delete some projects or read ahead and use my advanced script which can filter out certain projects and gives you more granular control.

It’s also possible you may get an error message like this:

If you see this it’s probably because you’re using a character in one of your project names that isn’t supported. For example, you can’t have a project name that includes a comma. If you have a project called Some, Stuff it’s not going to work. Change the offending project title and go test your Cloud Function again.

If your filter looks good then continue on to the next step.

Get Your Trigger URL

Go to your Cloud Function, click on the “Trigger” tab and copy the Trigger URL. We’re going to need it for the next step.

Your Cloud Function will run and update your Todoist filter any time you (or anyone or anything else) visits this URL! How cool is that?

We’re going to automate running this Cloud Function every hour (or whatever interval you choose) but it’s handy to save this URL in case you ever want to manually run an update on your “View All” filter. I added it as a comment to my “Inbox” project to make it easy to find.

Automate Posting to Your Trigger URL

As cool as it is to run your Cloud Function manually with your Trigger URL, it’s even cooler if we automate triggering it.

There are 3 ways to do this. You can use Zapier or IFTTT if you use those services. Just use the time trigger (you can have it run every hour) and the webhook action. Choose to Post to your webhook and don’t worry about the other settings.

The third way to do this is free. Use Google Cloud! Go hit the pancakes (menu), scroll all the way to the “Tools” section and hit “Cloud Scheduler.” You can click the “Pin” icon to make it easier to access later if you want.

Click “Create Job”

Give it a good name and description so you can figure out WTF this is in the future.

Enter the frequency you want. I run mine every hour but put whatever you want. The format is tricky so here are a few examples you can copy and paste in:

  • * * * * * run every minute.
  • */5 * * * * run every 5 minutes
  • */30 * * * * run every 30 minutes
  • 0 * * * * run every hour

Enter your country/timezone, choose HTTP as your target, paste your Cloud Function Trigger URL in “URL”, and choose POST for your “HTTP Method.” Leave the other settings blank. Click “Create” and you’re done!

Your hard work has paid off. Isn’t she beautiful?

The Advanced Script

I wanted a bit more granular control over my “view all” filter. So I made one that is even better.

def update(request):
    import requests
    from todoist.api import TodoistAPI
    import json
    
    key = "12b3456abcdefg"
    filter_id = "123456789"
    api = TodoistAPI(key)
    api.sync()
    full_query = ""
    for item in api.state['projects']:
        name = item.data.get("name")
        parent_id = item.data.get("parent_id")
        if parent_id != None: #if it is a child project
            continue #exit this iteration of the loop
        if name == "Misc" or name == "PROJECTS ON ICE": #projects I want to completely skip
            continue #exit this iteration of the loop
        if name == "🏋️ A: Health": #I don't want to see sub projects for these projects
            sub_projects = "#" #no sub projects
        else:
            sub_projects = "##" #view all sub projects
        query = sub_projects + name + " & (!subtask), "
        full_query = full_query + query
    full_query = full_query[:-2] #trim the last 2 characters (", ")
    filter = api.filters.get_by_id(filter_id)
    filter.update(query=full_query)
    api.commit()

This personally fit my life better. I did a couple of things…

Skip Child Projects

I don’t want child projects in my filter query. I’d rather see those tasks listed out under the parent project section. So if a project is inside of another project I don’t include it in the filter query. If this isn’t something you like then just get rid of these lines:

parent_id = item.data.get("parent_id")
        if parent_id != None: #if it is a child project
            continue #exit this iteration of the loop

Skip Certain Projects

There are certain projects I want to completely skip. I have a project called “PROJECTS ON ICE” and I put projects that are on hold in there.

If this is something you want to do just add the projects to this line:

if name == "Misc" or name == "PROJECTS ON ICE": #projects I want to completely skip

Replace Misc with a project you want to not show. You can add more by continuing to add or name == "your project name"

e.g. if name == "project 1" or name == "project 2" or name == "project 3":

If this isn’t your jam then just delete these lines:

if name == "Misc" or name == "PROJECTS ON ICE": #projects I want to completely skip
            continue #exit this iteration of the loop

Don’t Include Tasks Inside of Sub Projects

Last of all I have certain projects where I don’t want to pull in tasks belonging to sub projects (should any exist.)

If any projects match the criteria the code will generate a query like #Project otherwise it will be ##Project.

if name == "🏋️ A: Health": #I don't want to see sub projects for these projects
sub_projects = "#" #no sub projects

Replace this with projects that apply for you.

If there aren’t any projects that apply you can just erase everything between the quotes:

if name == "": #I don't want to see sub projects for these projects
sub_projects = "#" #no sub projects

If you have questions let me know. I hope this is as big a game changer for you as it has been for me!

0 Comments

Submit a Comment

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