Simple Markdown Zettelkasten: Drafts.app

tl;dr: I wrote a Drafts Action Group to manipulate MMD metadata and build workflows from Drafts to my Simple Markdown Zettelkasten.

Drafts.app is one of the most used apps on my phone.

I use Drafts heavily to take notes during meetings or sermons, store thoughts and quotes from books, comment on links from the web, or just record my own thoughts about the world.

Some assumptions:

  • I use Drafts purely as a capture tool. I don’t permanently store the zettel in Drafts, I save them to Dropbox.
  • I won’t browse my zettel archive in Drafts, so it doesn’t have to be able to follow a freelink.1

Working backwards from the “data structure” of a Simple Markdown Zettelkasten to the implementation proved to be the thing I had been missing all this time. I started re-writing individual actions for all the different kinds of notes I post, but quickly realized that I was re-writing the same js scripts for “get the title” or “update a metadata field”.

By unchecking the radio buttons for visibility, I can create a “library” action that empowers and simplifies all the other actions.

While there wasn’t built in functions for dealing with Multimarkdown metadata, Drafts Action steps can share variables, so I ended up rewriting a lot of my shared code into one “hidden” script Action called “Zettelkasten Utilities.” This action just instantiates a few javascript functions that make dealing with the MMD metadata easier, and then future actions can use them. If you want, you can read the current source code here.2

The other trick was so obvious I wish I had done it earlier. Testing a Draft action can involve a lot of tedious clicking back in and out of the Drafts Actions panel. For a bit I was editing the Action on one laptop, and testing on another. Then I had taken to copying a function out into a Chrome console to tweak and play with it… finally I up and made a “test.js” file and wrote some basic tests using console.assert() to work my way through some of the edge cases, just running it using nodeJS. Much better.

The file changed a lot as I was testing, but here’s a stub demonstrating the console.assert() statements and a mock Draft object. (The only thing I had to change between Drafts and nodeJS was to comment out when I called a Drafts specific call like Draft.update().)

// mock
var newDraft = function() {
  draft = {};
  draft.content = `Creating a note

  Most of the time, we don't have the #time or #focus to do all the work to store a note.

  That's where automation is wonderful. Saving #time.`

  return draft;
}

// < .. implementation .. >

// test

draft = newDraft();

console.assert(zk_metadata(draft) == "", "metadata is empty");
console.assert(zk_body(draft) == draft.content, "body is content");

zk_extract_tags(draft);
console.log(draft)

With this library in place, writing two specific actions for my most common use cases became much easier and robust.

Saving a Zettel

The first problem to solve is simply transforming a normal draft into a Simple Markdown Zettelkasten. With the help of the “library,” this is simple. (I actually wrote this as “pseudocode” and then implemented the functions later.)

// courtesy of drdrang
// https://forums.getdrafts.com/t/specify-draft-title/6469/7
function cleanTitle(title) {
  return title
    .toLowerCase()
    .replace(/[^a-z0-9_\s]/g, '')
    .replace(/\s+/g, '-');
}

path = "/wiki/archive/"
filename = `${zk_id(draft)} ${cleanTitle(zk_title(draft))}.md`

zk_update_metadata(draft, "id", zk_id(draft));
zk_extract_tags(draft);

var db = Dropbox.create();
var check = db.write(path + filename, draft.content, "append");

if (!check) {
  alert("Couldn't save to " + path);
}

I owe cleanTitle(title) to the awesome drdrang. All the magic is in the library file. zk_title() will grab the title either from the metadata or the Draft title and zk_extract_tags() goes through the whole draft finding any #hashtags and moving them to the tags: MMD metadata. Quick and easy.

You could expand/change this Action to respond to tags within Drafts.app. For instance, I will likely add a conditional to add tags based on whether this note has to do with my day job.

As it is, we have stable IDs based on the Draft creation date, way to pull out tags… we are ready to implement more complex flows.

Book Notes

I actually started here with the book note, which is the place that I first really saw the value of this flow back when I implemented it using vimwiki’s file structures.

There are two Actions, “Add Book…” and “Cite Book…”.

Add Book

First we need some record of books that I’m currently listening to or reading. This one actually uses no part of the Zettelkasten library. It just prompts the user to fill in some data and puts it into a template found in another Draft.

let template = Draft.find("UUID of Template");

// create new draft and assign content/tags
let d = Draft.create();
for (let tag of template.tags) {
  if (tag != "template") {
    d.addTag(tag);
  }
}

var p = Prompt.create();

p.addTextField("title", "Title", "")
p.addTextField("author", "Author's Name", "Last Name, First Name");
p.addTextField("publisher", "Publisher", "Unknown Publisher");
p.addTextField("year", "Year Published", "2020");
p.addButton("Confirm", 1, true);

p.show();

if (p.buttonPressed == 1) {
  d.setTemplateTag("title_text", p.fieldValues["title"])
  d.setTemplateTag("author_text", p.fieldValues["author"])
  d.setTemplateTag("publisher_text", p.fieldValues["publisher"])
  d.setTemplateTag("year_text", p.fieldValues["year"])

  d.content = d.processTemplate(template.content);
  d.update();

  // load new draft
  editor.load(d);
  editor.activate();
}

The template is really simple.

title: [[title_text]]
author: [[author_text]]
publisher: [[publisher_text]]
year: [[year_text]]
id: [[created|%Y%m%d%H%M%S]]

That’s it. I believe this Action is heavily copied from New from Template by @hinnerkhaardt.

Cite Book

I typically am reading or listening to a couple books at a time. I will jot down thoughts and file them after the listening session. Before, I would just append them to one giant book note, but that’s not the zettelkasten way.

Now that we have a few books loaded, we can use their metadata to generate a citation, and because the IDs are stable based on the Draft creation date, we can link them even though they haven’t been written to the archive yet.

// The following just builds a prompt to choose a book.
let workspace = Workspace.find("Booknotes");
let booknotes = workspace.query("inbox");

let bookTitles = [];
for (let booknote of booknotes) {
  bookTitles.push(booknote.title.replace("title: ", ""));
}

var p = Prompt.create();

p.addLabel("title", "Choose booknote:");

p.addSelect(
  "selectBook",
  "Book:",
  bookTitles,
  [],
  false);

p.addButton("Choose Book", 1, true);

p.show();

// After we've chosen a book
if (p.buttonPressed == 1) {

  let bookIndex = booknotes.findIndex(bk => bk.title.includes(p.fieldValues["selectBook"]));
  let book = booknotes[bookIndex];

  // - Adds a citation to the book to the current note.

  let author = zk_get_metadata(book, "author");
  let title = zk_get_metadata(book, "title");
  let publisher = zk_get_metadata(book, "publisher");
  let year = zk_get_metadata(book, "year");

  draft.content = draft.content + `\n\n${author}. _${title}_. ${publisher}, ${year}.`;

  // - Links the current note to the ID of the booknote.
  draft.content = draft.content + `\n[[${zk_id(book)}]] ${zk_title(book)}`
  draft.update();

  // - Adds a link to this zettel in the main booknote, if there’s a title, uses that as a summary
  book.content = book.content + `\n[[${zk_id(draft)}]] ${zk_title(draft)}`
  book.update();
}

Here’s an example of a zettel where I cited a book I’m reading:

tags: #stress, #effort, #mindset, #cool
id: 2020313101825

Many people believe that perfection should be effortless. This unreasonable
expectation causes stress.

> A report from researchers at Duke University sounds an alarm about the
> anxiety and depression among female undergraduates who aspire to “effortless
> perfection.” They believe they should display perfect beauty, perfect
> womanhood, and perfect scholarship all without trying (or at least without
> appearing to try). [41][^mindset]

I wonder if this is related to the popular trope that "cool guys" don't care,
or don't have to try. Over and over in 80s movies you have characters that do
not seem to exert any effort to win, it just comes naturally.

[[2020313101751 cool isnt new.md]]

[^mindset]: Dweck, Carol S., _Mindset: the New Psychology of Success_.
  Ballantine Books, 2016.

And here’s the link it created in the note about the book:

[[2020313101825]] Many people believe that perfection should be effortless. This unreasonable expectation causes stress.

And just like that… we’ve got a single zettel note with the full context of our singular thought, linked automagically to a note about the book. You could just as easily use {{multimarkdown transclusion}} syntax to build back the giant file, it’s all up to you!

Where to now?

I can write and tag zettels easily, save them to my archive… nothing is stopping me from building my archive till I hit that legendary point where it starts generating ideas for me (hopefully). This is a good little mobile inbox for my system, and I’m going to stop tweaking and start writing.

I may find myself writing some more actions for diary style entries and for sermon notes. I also have a hankering to write a simple Dropbox search that is able to build a file link based on a simple file search… but it can wait.

At this point, I’m going to be spending some time brushing up on my vim-based browsing/editing scripts. I’ll probably start with implementing a script that finds notes with no backlinks so that I can start processing that “inbox” for unlinked notes. More to come I’m sure…

If you found any of this useful, or you’ve already been down this road, I’d love to hear from you. Drop me a line or hit me up on twitter.

I posted the Zettelkasten Action Group to the Drafts Actions Directory.


  1. As I hinted at in my last post, 1Writer is perfect for that. 

  2. At some point, I’d like to put it into a module and export the functions under a namespace, but this will do for now. 


Changelog
  • 2020-03-20 10:14:00 -0500

    Add a tl;dr

    This post was long, y'all.

  • 2020-03-19 20:45:40 -0500

    Posted the Drafts Action Group

  • 2020-03-19 20:36:59 -0500

    Post: SMZ using Drafts