Asana is a great task manager for individuals and teams. I’ve used it on the job, to help me track new issues. The main feature I miss is the ability to save all your tasks to a local file. Asana’s built-in export truncates the output at 500 records. This limits your ability to work with the data in other applications, or switch to another service.

Fortunately, you can export all your entries using the Asana API and a bit of Python.

Install

Download and add the official Asana API module from the Python Package Index (PyPI). It will install some additional packages, per its requirements list.

pip3 install asana

If you prefer to get it from GitHub, be sure it’s the official version from Asana, not the older unmaintained one.

Using the API

The script I wrote (see below) downloads all tasks for a single workspace (e.g. “Home” or “Business”), which the user enters as a command-line parameter.

Asana made changes to their API this year, including authentication. The old Asama API used API keys. It’s been deprecated. The new supported methods are Oauth2 or personal access token. I’m using a token. You can generate your own by going into Asana’s My Profile Settings, selecting the Appss tab, then Manage Developer Apps.

Here’s an example connection using the access token method:

client = asana.Client.access_token(MY_PERSONAL_ACCESS_TOKEN)
me = client.users.me()

Asana supports two methods of iterating through collections. I couldn’t get the more elegant method (items iterator) to work. Instead, I used the raw API to fetch each page of results. The API documentation strongly encourages you to specify a pagination size. Otherwise, you can run into errors when returning large result sets. I specified a page size like this:

client.options['page_size'] = 100

I created a loop to paginate through the tasks in each project. I found that I didn’t have to keep track of the specific offset values, like in their examples. I simply checked for a “next page” value in the results. If None, it means you’ve reached the last page.

The final script:

#!/usr/bin/env python3
"""asana2csv.py
Export all tasks from my Asana workspace to CSV
Requires asana library.  See https://github.com/Asana/python-asana
Requires workspace as parameter.  Mine are Home, Computer, Business, Employer
2017/06: updated for python3
"""

import sys
import csv
import datetime
import asana

# NOTE: API keys are deprecated, may no longer work.
# See: https://asana.com/developers/documentation/getting-started/auth#personal-access-token
# Use Oauth2 or Personal Access Token instead.  I chose the latter.
# Generate access token in Asana: My Profile Settings | Apps | Manage Developer Apps
#MY_API_KEY = '4mwYZsan.gvL8FNZfYFHO5xPWE3cE9wE'
MY_PERSONAL_ACCESS_TOKEN = '<ENTER_YOUR_TOKEN_HERE>'

OUT_FILE_PATH = '/home/paul/Documents'
OUT_FILE_NAME_ROOT = 'asana_tasks'
CSV_OUT_HEADER = ['proj_name', 'ms_name', 'task_title', 'completed_on', 'priority']
REC_LIMIT = 99999
# New Asana API requires pagination for large collections, specify item_limit to be safe
ITEM_LIMIT = 100
PAGE_SIZE = 50

CSV_OUT_HEADER = ['Task', 'Project', 'Workspace', 'DueDate', 'CreatedAt', \
    'ModifiedAt', 'Completed', 'CompletedAt', 'Assignee', 'AssigneeStatus', \
    'Parent', 'Notes', 'TaskId']

def write_csv_records(csv_out_file_name, csv_header_list, csv_record_list):
    """Write out selected columns into CSV file."""
    with open(csv_out_file_name, 'w', encoding='utf8') as csv_file:
        csvwriter = csv.writer(csv_file, lineterminator='\n', quoting=csv.QUOTE_MINIMAL)
        csvwriter.writerow(csv_header_list)
        for item in csv_record_list:
            csvwriter.writerow(item)
    return

def get_workspace_dict(workspaces):
    """Return a dictionary with id and name of each workspace."""
    this_dict = {}
    for workspace in workspaces:
        this_dict[workspace['id']] = workspace['name']
    return this_dict

def process_project_tasks(client, project, ws_dict):
    """Add each task for the current project to the records list."""
    task_list = []
    while True:
        tasks = client.tasks.find_by_project(project['id'], {"opt_fields":"name, \
            projects, workspace, id, due_on, created_at, modified_at, completed, \
            completed_at, assignee, assignee_status, parent, notes"})
        for task in tasks:
            ws_name = ws_dict[task['workspace']['id']]
            # Truncate most of long notes -- I don't need the details
            if len(task['notes']) > 80:
                task['notes'] = task['notes'][:79]
            assignee = task['assignee']['id'] if task['assignee'] is not None else ''
            created_at = task['created_at'][0:10] + ' ' + task['created_at'][11:16] if \
                    task['created_at'] is not None else None
            modified_at = task['modified_at'][0:10] + ' ' + task['modified_at'][11:16] if \
                    task['modified_at'] is not None else None
            completed_at = task['completed_at'][0:10] + ' ' + task['completed_at'][11:16] if \
                task['completed_at'] is not None else None
            rec = [task['name'], project['name'], ws_name, task['due_on'], created_at, \
                modified_at, task['completed'], completed_at, assignee, \
                task['assignee_status'], task['parent'], task['notes'], task['id']]
            rec = ['' if s is None else s for s in rec]
            task_list.append(rec)
        if 'next_page' not in tasks:
            break
    return task_list

def main():
    """Main program loop."""
    def usage():
        """Show usage if user does not supply parameters."""
        text = """
usage: asana2csv.py <WORKSPACE_NAME>
"""

        print(text)

    # Check command line parameters; require name of workspace
    if (len(sys.argv) < 2) or (len(sys.argv) > 2):
        usage()
        sys.exit(0)
    my_workspace = sys.argv[1]

    now_day = str(datetime.date.today())
    my_filename = '_'.join([OUT_FILE_NAME_ROOT, my_workspace, now_day + '.csv'])
    csv_out_file = '/'.join([OUT_FILE_PATH, my_filename])

    # Old API used keys and basic auth; new API uses Oauth2 or tokens
    #asana_api = asana.asanaAPI(MY_API_KEY, debug=True)
    #client = asana.Client.basic_auth(MY_API_KEY)
    client = asana.Client.access_token(MY_PERSONAL_ACCESS_TOKEN)
    client.options['page_size'] = 100
    my_client = client.users.me()

    # Build dictionary of workspace names associated with each workspace ID.
    # This isn't strictly needed for querying a single workspace.
    ws_dict = get_workspace_dict(my_client['workspaces'])

    # Find the requested workspace; iterate through all projects and tasks
    this_workspace = next(workspace for workspace in my_client['workspaces'] if \
        workspace['name'] == my_workspace)
    all_projects = client.projects.find_by_workspace(this_workspace['id'], iterator_type=None)

    db_list = []
    for project in all_projects:
        print(project['name'])
        db_list.extend(process_project_tasks(client, project, ws_dict))

    write_csv_records(csv_out_file, CSV_OUT_HEADER, db_list)

    return

if __name__ == '__main__':
    main()

Unassigned Tasks

My method will only collect and export tasks that have been assigned to projects. Here’s one way to search for unassigned tasks: Use Asana’s advanced search (click inside the search window at top, “Advanced Search” opens as an option). Enter “No Project” (upper/lower case letters exactly). “No Project will drop down as a matching option. Select it to filter those tasks. I created a catch-all project, “NOT IN PROJECT”, for these miscellaneous tasks.

Resources