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:
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!