A Tinderbox Zettelkasten starter file

I have been intending for a while to make a Tinderbox file that serves as the home of my long-term reading notes. (I am an academic working in the fields of history and theology).

With the help of @mwra, @eastgate, @PaulWalters, and countless others, I have finally cobbled together a TBX file that works for my purposes. I hope it might be of help to any others looking to use Tinderbox to do the same.

I have tried to document how the file works. Please do see it as a starting point rather than a prescription. If you catch any bugs, or have any suggestions, feel free to post them below. And all best wishes for your own note-taking!

ZKN Demo.tbx (365.3 KB)

Updated version of file from @talazem [edited in by admin]
ZKN Demo.tbx (365.9 KB)

18 Likes

This is a wonderful bit of work, @talazem. I will spend more time exploring the little corners and features, but you clearly have thought the model through carefully.

Only a couple of things. I don’t understand if we can figure the note ID naming convention or not. Personally I would not choose or benefit from the naming convention built into the Tinderbox file. Second, the file has a lot of $Rules that will be operating quite frequently. Might be a performance issue at scale.

This is really neat. I’ve just started reading K. Dini’s “Taking smart notes with DEVONThink” but I’m building the slipbox in TB. Your starter doc is just what I needed.

On bug: in the current downloadable version, the 2 register notes (for keywords and persons) have almost identical “Rules” and “Edicts” (one has an extra \n in a text variable) which is obviously a mistake (and incidentaly causes a flicker in the display, which is the only reason I discovered this issue).
Everything works perfectly if the Rule is erased - I’m assumed you wanted to improve performance?

1 Like

Thank you, Paul, both for your kind words and your previous help.

Within Tinderbox, a note’s name is simply whatever you assign to $Name, as is usually the case in vanilla Tinderbox files. The ZettelNumber is only really used for exporting the zettelkasten into text files. The export mechanism prepends the ZettelNumber onto the resulting text files. This ZettelNumber can be ignored if you have no need to export, and could be removed from the Key Attributes without harm.

This ZettelNumber can be somewhat modified in Utilities>ZettelConfig, as described in the note “Configuring your zettelkasten for export”. But the changes are minimal: the numbe is inspired by Luhmann’s numbering system, which works well if exporting to files or paper.

However, if you want to get rid of it altogether upon export, then, as things stand, you’ll have to modify the actual export code.

I’m glad you have found it useful, @seishonagon. And thank you for catching that bug! I’ve fixed it, and have updated the file in the original post.

2 Likes

Thank you for this work! It has prompted me to get more adventurous in my Tinderbox design.

Wonderful work @talazem
Question: I noticed in your keywords register you were doing the work with the edict of the note. I am having trouble understanding exactly what you did? Would you or someone else help explain the code below?
More specifically what are the ‘aValue’ and ‘aKeywords’ referenced below and how does it work in the code? I have not used this style before. Is that a User defined attribute?
Lastly, I would like to add $NoteURL to the line below each keyword. How can I customize the code?

$MyString=;
$MyList= values(“Keywords”).isort();
$MyList.each(aValue){
$MyString = $MyString + “\n\n” + aValue;
$MyList1= find($Keywords.icontains(aValue));
$MyList1.each(aKeywords){
$MyString = $MyString + “\n” + $ZettelNumber(aKeywords) + " " + $Name(aKeywords);
};
};
$Text = $MyString;

Thanks in advance,
Tom

This is very well done and illuminating. Thanks for going to the trouble of putting it together and sharing it.

3 Likes

Ah, this code looks familiar. No, they aren’t attributes, they are loop variables (as documented). But, first let’s tease the formatting out a bit for clarity (I’ve added an extra line (explained below):

$MyString =;
$MyList = values("Keywords").isort();
$MyList.each(aValue){
   $MyString = $MyString + "\n\n" + aValue;
   $MyList1= find($Keywords.icontains(aValue));
   $MyList1.each(aKeywords){
      $MyString = $MyString + "\n" + $ZettelNumber(aKeywords) + " " + $Name(aKeywords);
   };
};
$Text = $MyString;
$MyString =;

So, we’ve two nested List/Set.each(LoopVar) loops. The loop variable used within the loop is as set by the user. So in the inner loop it is aKeywords and in the outer loop aValue. In each case the variable is a place holder for the list item being processed. On the first loop it is list item #1, then next loop #2, until done.

As there are nested loops, every inner list item is processed for every outer list item. So if the out list is 3 items long and the inner 6 items long, there are 3 x 6 iteration before we leave the loop.

Here, the end point is creating $Text (replacing any existing text). We need to run a process to get that outcome so we need somewhere to accumulate the text as we go. A String attribute makes sense and we might as well use $MyString to save bothering making a new attribute. But by all means make your own user String attribute, replacing all instances of MyString with your attribute’s name (taking care to preserve the $-prefixes). The same goes for $MyList, but as we’ve two loops (two lists to process) we will need a second List-type attribute so make MyList1. Again supply List attributes of your own name is you prefer.

Line 1. We start by ensuring MyString is empty by setting it to nothing.

Line 2. We use MyList to stored a sorted keyword list. First values() returns a list of all unique values for attribute Keywords. The chained .sort command alphabetically. The sorted list is passed to MyList.

Line 3. We start the outer loop based on the list stored in MyList and set a loop variable name of aValue for the current list item.

Line 4. We concatenate ( i.e. join to existing value) the current MyString value (which is nothing at start of first loop) with two line return (‘new line’) characters and then concatenate the value of the current list item, i.e. the current value of aValue.

Line 5. Using the current value of aValue we make a new list by finding all notes whose Keywords contains the exact† value of aValue. This results in a list of those notes’ Path data being stored in MyList1

† Note: for lists and sets, .contains can only do exact matches to complete discrete list values and can’t use regex.

Line 6. We start the inner loop based on the list stored in MyList and set a loop variable name of aKeywords for the current list item.

Line 7. We continue to concatenate to MyString as in the outer loop, adding a line break character, the ZettelNumber value for the note at the path currently stored in aKeywords, then a space character and finally the Name of the note at the path currently stored in aKeywords.

Line 8. We close the inner loop. The loop runs until all items in MyList1 have been processed.

Line 9. We close the outer loop. The loop runs until all items in MyList have been processed.

Line 10. All the loops are complete and all the collected data is in attribute MyString. We simply set $Text to $MyString. Why not just use $Text from the get-go? You could but this makes it easier to pass the outcome to any attribute and designs change. There is no real overhead, though as this is in an edict, perhaps…

Line 11. This reprises Line 1. If the above were a rule, this code would run near continuously (not ideal as there is a find() in there that has to search every one in the doc for every inner loop). So we use an edict. In which case once we’ve set $Text, we don’t need to stored the data twice. Thus we can ‘empty’ MyString until needed again. Sure, you don’t strictly need Line 1 any more but it’s a good belt and braces and has no overhead.

So you will want to add a line break and NoteURL for the path in aKeywords to the inner loop (Line 7.) with code like so: + "\n" + $NoteURL(aKeywords). However it is probably better to add it as a separate code line so you can see what’s happening (so be need a bit more concatenation):

$MyString =;
$MyList = values("Keywords").isort();
$MyList.each(aValue){
   $MyString = $MyString + "\n\n" + aValue;
   $MyList1= find($Keywords.icontains(aValue));
   $MyList1.each(aKeywords){
      $MyString = $MyString + "\n" + $ZettelNumber(aKeywords) + " " + $Name(aKeywords);
      $MyString = $MyString + "\n" + $NoteURL(aKeywords);
   };
};
$Text = $MyString;
$MyString =;

We could equally have used a single line:

$MyString = $MyString + "\n" + $ZettelNumber(aKeywords) + " " + $Name(aKeywords) + "\n" + $NoteURL(aKeywords);

…but I think the two line form is clearer.

5 Likes

Sorry for that long post. the simple ‘trick’ was to see the nesting; in some cases you may want to copy paste the code and insert your own indentation to show the nesting more clearly. If in doubt start with the innermost nesting. Nested code will each be a loop or a conditional ‘if’ statement.

The things that seem like attributes without a reference $-prefix are loop variable, whose purpose should be apparent after reading the documentation. IOW, the first time these will look odd. But now you know the ‘word’ in parentheses after the .each command is that loop’s loop variable. By all means use you own style (within the allowed naming connections". For me, because I adhere to the (non-mandatory) CamelCase naming of attribute, a lower-case ‘a’ tells me this is not an attribute name (as it’s not CamelCase) and that it is a(n) item in the list being looped).

In examples, I try to use the Sandbox attribute or the ‘My’+data-type formulation, e.g. user attribute MyList1 to indicate deliberately the intended data type. In actual work I often use the data-type (or a shorter string) on the end of user attribute names for the same reason; I also put a number of letter on the end if there is a sequence of otherwise same-names attributes, e.g. PartNumber, RefURL3, etc. For Booleans I tend to use a Has or IsPrefix (e.g. HasAnswer) such that the name is self-descriptive of the true (ticked) state of the boolean. Thus a question note with an answer would set $HasAnswer to true.

I quite appreciate that for many users action code might as well be magic, until its syntax swims into focus (if it ever does!), but a take-away from the above is don’t be too literal about names of things being set in stone. If there are other attribute names that make sense for you, go within it (as long as it is an allowed format).

Sorry for two long posts but I hope that clarifies things a bit.

2 Likes

(Ah, this code looks familiar. )
Great work by @talazem @mwra

This is INVALUABLE!
I had never seen/studied loop variables before and could not figure out what they were. Till now! More learning on the way!
Many thanks.

Great Starter file example. So well documented.
What I like most is the autotracking (registry section) and the outline order based ZettelNumber.
Nice.

Tom

Thanks, I just helped with some code. All the credit goes to @talazem with extra props for the detailed instructions (I know it’s the most bothersome part of the task as the benefit is only to the reader not author!).

Hi Mark

I am trying to restict the scope to a certain folder, in this case, let’s say to the containter “/The Zettelkasten”
I am still fumbling with coding. More to learn. Would you mind giving me another example on where/what operator you would use? I generally use ‘descendedFrom’ or ‘include’ but I cannot get these to work.
For the sake of this example, let’s use ‘descendedFrom’

Thanks
Tom

Not enough context! Scope of what task or what piece of code?

Sorry Mark. I would like to use an Edict in the Keywords register to restict it to a particular folder, in this case “The Zettelkasten” container. I am not sure if this is even possible but I thought I would ask. My thought was to use the "descendedFrom(‘The Zettelkasten’) operator within this code below:

$MyString =;
$MyList = values(“Keywords”).isort();
$MyList.each(aValue){
$MyString = $MyString + “\n\n” + aValue;
$MyList1= find($Keywords.icontains(aValue));
$MyList1.each(aKeywords){
$MyString = $MyString + “\n” + $ZettelNumber(aKeywords) + " " + $Name(aKeywords);
$MyString = $MyString + “\n” + $NoteURL(aKeywords);
};
};
$Text = $MyString;
$MyString =;

Hope that helps my context and thanks again for the assistance.
Tom

Thanks, that helps. So you want to restrict the scope of the list of note paths of notes with a $Keywords match, as is set in this line:

We are using a find(query) here, where the query term is all note(s) that have the exact discrete $Keywords string value as supplied in the loop variable aValue.

We want to restrict that query further by only matching the above if the note is also descended from, i.e. descendedFrom(), a container with the title ‘The Zettelkasten’. If the that name is not unique in the document, you will need to insert the full path where the cold below uses just the container title. So, our extra query term is an AND join of:

descendedFrom("The Zettelkasten")

As the latter is decrease scope fastest, i.e. passes —at maximum—fewer possible successful matches than the existing query, we put the new term first with a & AND join thus:

descendedFrom("The Zettelkasten") & $Keywords.icontains(aValue)

In turn the overall code change becomes:

$MyList1= find(descendedFrom("The Zettelkasten") & $Keywords.icontains(aValue));

Note: too many other tasks on the go to check this live in the demo doc but it should work—if I’ve correctly understood the locus of the scope change!

1 Like

Thanks for a great contribution to us developing our own TB setups for scientific knowledge management! I got plenty of inspiration for my own (so far much simpler) system. I have a quick question though, when trying to figure out how you have built your system, and with the risk of sounding like a complete idiot, I cannot find the container “The Zettlekasten” in map view. When opening it in outline view and then going to map view, it looks like it is located directly under “ZKN Demo copy”, but I cannot find it anywhere. How is that possible? Where can I find it?

I’m glad to hear you are finding it helpful.

In this particular Tinderbox file, I largely work in Outline view, and do not use the Map view. As a result, my ‘The Zettelkasten’ is not a container, but a separator. Separators appear in Outline View but not in Map view, I believe.

This is easily remedied if you’d like, however. In Outline view, choose ‘The Zettelkasten’ note, and uncheck the ‘separator’ attribute in the inspector for that note. The note will become a regular container, and will now appear in the Map.

I’m away from my computer that runs Tinderbox at the moment, so I can’t be sure that this will not break any of the automation. From memory, though, I don’t expect it will!

1 Like

Oh, obviously! Thanks for your quick reply, so I can rest easy tonight :slight_smile:

This looks absolutely brilliant. I have started using it and very much like the simple way it creates these rich links. Also, because its in Tinderbox, and I will be able to add maps and other connections to my research, it feel very sustainable.
One question. I dont seem to be able to drag from “bookends” into the references list successfully. A mostly empty note gets created, with no attributes added, even though the Bookends item has all the information. Is there a particular way that bookends needs to be set up? (or perhaps because I am using a trial version of bookends its not working) . Many thanks for any help or advice, and thanks again for sharing this hard work!
Thomas