Workflowy Scheduling in Python

Workflowy is one of my favourite personal management tools; it’s an outlining tool that allows you to create large hierarchies of bullet points. What differentiates it from, say, doing the same in a Word document is:

  • The ability to “focus in” on a single bullet point, hiding it’s sibling and parent items just showing you itself and the child items
  • Tagging items to relate content
  • Note taking against individual bullet points
  • Rapid client side search functionality
  • The ability to star pages and searches for quick access later on
  • Excellent, well thought out keyboard shortcuts

It looks a little like this:

Workflowy

I would love nothing more than to use Workflowy as my primary to do list, the way it allows you to decompose items and brainstorm is second to none, however it does not have some functionality with is essential in a task management app (mainly as it’s not part of the tool’s intention) namely:

  • Scheduled items
  • Repeating tasks
  • Reminders

So for the longest time I’ve used tools like NirvanaHQ and Todoist as my “primary” apps (both awesome btw) with Workflowy when I want to brainstorm a larger issue. This does however create a sense of detachment and “which tool do I do my notes in” frustration that slows down actually doing the work.

However let’s think about this, really all I need is something that does the scheduling on my behalf automatically. Now Workflowy don’t expose any API as such, so I can’t write a background script to use that, however we can turn to Selenium for automating the process for us.

I’ve built a script to do this which can be found on GitHub here. This works by:

  • Launching Firefox
  • Logging in to Workflowy for me
  • Finding all items with a tag matching todays date (e.g: #2016-02-18)
  • Giving each of those items a tag #Focus

Then to find everything I need to do today I search for #Focus.

From here all I have to do is schedule the script to run. I’ve currently got it setup to run as a scheduled task when I log in to my machine.

The script itself:

import time
import settings
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
 
 
class WorkflowyScheduler(object):
    workflowy_url = "https://workflowy.com"
    browser = webdriver.Firefox()
 
    @classmethod
    def schedule_items_for_today(cls):
        todays_date_tag = cls.__get_todays_date_tag()
 
        cls.browser.get(cls.workflowy_url)
 
        cls.__login()
        cls.__search(todays_date_tag)
        cls.__mark_results_with_tag("#Focus")
        cls.__save_changes()
 
        cls.browser.close()
 
    @classmethod
    def __login(cls):
        cls.__click_button("div.header-bar a.button--top-right")
        cls.__wait_for_element_to_appear("#id_username")
        cls.__fill_text_box("#id_username", settings.workflowy_username)
        cls.__fill_text_box("#id_password", settings.workflowy_password)
        cls.__click_button("input.button--submit")
 
    @classmethod
    def __search(cls, search_term: str):
        cls.__wait_for_element_to_appear("#searchBox")
        cls.__fill_text_box("#searchBox", search_term)
 
    @classmethod
    def __mark_results_with_tag(cls, tag: str):
        for element in cls.browser.find_elements_by_css_selector("div.name.matches"):
            text = element.text
            if tag not in text:
                text_box = element.find_element_by_css_selector("div.content")
                text_box.click()
                text_box.send_keys(Keys.END)
                text_box.send_keys(" " + tag)
 
    @classmethod
    def __save_changes(cls):
        cls.browser.find_element_by_css_selector("div.saveButton").click()
        cls.__wait_for_element_to_appear("div.saveButton.saved")
 
    @classmethod
    def __click_button(cls, css_selector: str):
        cls.browser.find_element_by_css_selector(css_selector).click()
 
    @classmethod
    def __wait_for_element_to_appear(cls, css_selector):
        WebDriverWait(cls.browser, 10).until(
                lambda driver: driver.find_element_by_css_selector(css_selector))
 
    @classmethod
    def __fill_text_box(cls, css_selector: str, text_to_input: str):
        cls.browser.find_element_by_css_selector(css_selector).send_keys(text_to_input)
 
    @classmethod
    def __get_todays_date_tag(cls) -> str:
        return "#%s" % time.strftime("%Y-%m-%d")
 
 
if __name__ == "__main__":
    WorkflowyScheduler.schedule_items_for_today()

Some points to note:

As is the nature of web pages and Selenium; elements I need to interact with won’t necessarily be rendered when my code gets to them (the page could still be loading, waiting for an ajax response, etc). To get around this we need to instruct Selenium to wait until the element is visible. To do so:

def __wait_for_element_to_appear(cls, css_selector):
    WebDriverWait(cls.browser, 10).until(
            lambda driver: driver.find_element_by_css_selector(css_selector))

The meat of the logic after logging in and searching is here:

def __mark_results_with_tag(cls, tag: str):
    for element in cls.browser.find_elements_by_css_selector("div.name.matches"):
        text = element.text
        if tag not in text:
            text_box = element.find_element_by_css_selector("div.content")
            text_box.click()
            text_box.send_keys(Keys.END)
            text_box.send_keys(" " + tag)

Especially interesting is that you are sending keys rather than replacing the text in an element. This means I can send the “END” character code, ensuring I push the cursor to the end of the text, before adding the tag.

From here I want to make 2 additional scripts that run in the same timeframe:

  • Find and duplicate repeating items with a #Focus tag - I’m thinking a tag #Repeating, then something like #DailyAt17:00 as the tags to find
  • Duplicating the item means we can leave the repeated task definition alone
  • Notify by email / slack when an item reminder is triggered - Probably horribly inefficient with a Selenium script, not to mention distracting if it’s checking every other hour!
  • However ultimately quite useful, bullet points have static links in Workflowy too so the email can link directly to the item

If you feel like tackling any of those yourself, feel free to fork and send a pull request here!