Asana Productivity Todoist Trello Zenkit

Comparing Trello, Asana, Zenkit and Todoist

These are the four platforms where I have data. I hope to one day move it to a single platform.

Kanban uses the term card, Todoist says task. I use both.

Beauty5/10 – has a few backgrounds to choose from. Images attached to cards become small thumbnails.9/10 – An ever-growing number of pretty backgrounds thanks to Unsplash. Attached images become medium thumbnails on corresponding cards.5/10 – has a few backgrounds to choose from. Images attached to cards become small thumbnails.
TagsColored background and globalColored font and global; paidColored background and siloed per boardColored background and siloed per board; can have multiple categories. Can be used to color the whole card. See below.
AttachmentsCan be attached to cards or commentsCan only be attached to comments; paidCan be attached to cards or commentsCan be attached to cards or comments
Repeating tasks☑️☑️❌ – Power-up. Appears on due day.☑️
Reminders☑️☑️☑️ – Added as a separate field, but you can add many such fields
Links between cards/boardsPaste the URL yourself.Paste the URL yourself.☑️ – Add it as an attachment. No URL pasting requiredPaste the URL yourself.

Zenkit does really interesting things with tags that I think are worth addressing in greater depth. You can have different batches of tags. For example, each task might want a group of tags that refer to its location, then a separate group of tags to represent times of day.

It doesn’t make a distinction between a list heading and a category, so you can see the tasks organized by any batch of tags. Let’s expound on the list above with the two batches of tags. Suppose your headings are the classic to-do, doing and done. You created the two batches of tags described above, but the column heads are a third batch. By default, I walk in and see the do-doing-done columns. I can choose a filter and organize it by time of day, then the do/doing/done columns will appear on the cards like any other tag.

Another interesting thing it does with tags is that it’ll let you define a batch that will color the task itself. Again, suppose you want your morning tasks red, midday, orange, evening, yellow, and night, green. There’s a setting where you select which batch of tasks you want to define the color of the cards. If there are multiple tags from that field applied (say something can be done morning or midday), it guesses which color you want.

I think Zenkit is the most interesting, but it’s missing global tags. I recently turned my attention from it to Todoist, which is a little like going from a luxury car to a compact sedan. I may be able to split the difference with something like Zapier or IFTTT.

Asana also has global tags, but there are a handful of features the other three have that it lacks. The one that comes to mind is that you can’t copy lists to other boards, you can only copy whole boards.

The reason I’m so hung up on this global tags business is that I always have like 30 boards going. If I have a couple hours off work during the middle of conventional working hours, I want to easily pull up a list of all the things that can only be done during hours I’m normally working.

PHP Programming Todoist Zenkit

Zenkit API to Todoist CSV

I’m of two minds about posting this. The code is ugly. I’m posting it anyway because there aren’t a lot of resource for this this and was kind of a bear. I hope to save somebody else some trouble.

As is well-documented, I’m a fan of productivity tools. Todoist added boards to support a kanban format and let me tell you, I am here for it.

My productivity software journey of the past couple years goes like this: Trello and Todoist -> Asana -> Trello alone -> Zenkit -> Todoist alone. I have data living in all four places. I’m trying to move as much as I can to Todoist (it has global tags which Trello lacks, but its missing some other features I’m pretty fond of). Definitely Todoist will last forever and I won’t come running back to Trello.

Don’t forget to put in your API key.

 * Create a Todoist CSV file from the Zenkit API. Resulting file requires some
 * manual work.
 * @license: GPL
 * @author KJ Coop
 * Zenkit is more complicated then Todoist, so we kind of have to wedge things
 * in. For example, Zenkit only has groups of labels, and you decide which ones
 * you want to use a section heads at any given moment. Consequently, when
 * Zenkit hands off its data, it's not easy to see which labels Todoist should
 * use as section heads and which it should use as labels. It's probably
 * possible to detect which labels are grouped together in the Zenkit output,
 * but I haven't put any effort into it (yet?).
 * In Todoist, to add a label, you append the title with @Desired_Label. Because
 * we don't know whether we want Todoist to show any given label as a label or
 * a section head, it appends all the label-like strings as labels.
 * Consequently, if you want to keep your kanban format and of course you do or
 * what is even the point of being alive, you have to open up the finished
 * CSV file an manually manipulate it.
 * @todo Things I haven't put any work into:
 *    - Descriptions - Zenkit has them whereas Todoist does not
 *    - Attachments - in Zenkit, they can be on a file. In Todoist, they're
 *      necessarily in a comment
 *    - Comments
 *    - Users
 * Links:
 *    -
 *    -
 *    -

require_once(__DIR__ . "/vendor/autoload.php");

use idoit\zenkit\API;

function appendToCsv($filename, $type, $content) {

    $array = [
        '4', // Priority
        '1', // Indent
        '',  // Author
        '',  // Responsible
        '',  // Date
        '',  // Date lang
        '',  // Timezone


    $csv_file = __DIR__.'/'.$filename.'.csv';

    $fp = fopen($csv_file, 'a');

    // Loop through file pointer and a line
    fputcsv($fp, $array);


function listWorkspacesAndBoards() {
    $apiKey = '';

    $api = new API($apiKey);

    $count = 0;

    // Some of the arrays Zenkit returns are like this and we can ignore them.
    $emptyArray = [
        'text' => '',
        'searchText' => '',
        'textType' => 'plain'

    // Output data as array - workspaces and their collections/lists.
    $workspaces = $api->getWorkspaceService()->getAllWorkspacesAndLists();
    foreach ($workspaces as $key => $workspace) {
        $filename = $workspace->id;

        foreach ($workspace->lists as $list_key => $list_item) {
            $elements = $api->getElementService()

            $entries = $api->getEntryService()
            //    ->getEntry($list_item->shortId, 1);

            $listEntries = $entries->listEntries;

            $distinctTitles = [];
            $distinctWholeStrings = [];

            if (!is_array($listEntries)) {

            foreach ($listEntries as $entry) {
                if (isset($entry->displayString)) {
                    $title = '';

                    foreach ($entry->elements as $element) {
                            Don't reset the title at each iteration of this
                            loop. We append labels in subsequent iterations.
                            Fesetting the title resets the whole task. Consider:
                            In Zenkit, you have something like the following:
                                Task 1
                                    * Label A
                                    * Label B
                                Task 2

                            The best sense of the API I can get is that it gives
                                Task 1
                                Label A
                                Label B
                                Task 2

                            The code can see that Task 1 is a task and that the
                            labels are a different kind of string that relates
                            to the last thing it encountered. If doesn't know
                            until Task 2 comes along that it's finished with
                            Task 1. Until it sees a task, it has to keep
                            appending tasks to the last title it encountered.

                        if (isset($element['text']) && $element['text'] != '' && !strpos($element['text'], 'http')) {
                            // Remember that this is a title
                            $title = $element['text'];
                            $distinctTitles[md5($element['text'])] = $element['text'];

                        } else if (isset($element['categoriesSort']) && isset($element['categoriesSort'])) {
                            foreach ($element['categoriesSort'] as $cat) {
                                if (isset($distinctTitles[md5($title)])) {
                                        Add categories as labels. This is an not
                                        ideal map - Zenkit sees no distinction
                                        between labels and column heads,
                                        wbereas Todoist does. We add them all as
                                        labels if you want to change it to a
                                        section head, you must alter the
                                        resulting CSV manually :(
                                    $distinctTitles[md5($title)] .= ' @'.str_replace(' ', '_', $cat->name);
                        } else if ($element == $emptyArray) {
                            // Silence it
                        } else if (isset($element['persons'])) {
                            // Silence it - I'm the only person who matters, obviously
                        } else if (isset($element['files']) && isset($element['filesData'])) {
                            // Not worried about files right now.
                        } else {
                            // IDK LOL OMG

                        // Remember the whole string.
                        $distinctWholeStrings[md5($title)] = $title;


    echo "Distinct Titles: \n";

    foreach ($distinctTitles as $title) {
        appendToCsv($filename, 'task', $title);

try {
    echo "\n";
} catch (idoit\zenkit\BadResponseException $e) {
    echo 'Exception! The status was ' . $e->getCode() . ', response: ' . $e->getResponse()->getBody()->getContents();
} catch (Exception $e) {
    echo 'Exception! Something else went wrong: ' . $e->getMessage();