Archives    About    Etc    authors    etc

Creating and linking Zettelkasten notes in Vim

This is the third post in a series of sorts about note-taking in Vim. I have silently kept playing around with the system outlined in the previous posts (-1 ,-2 ). Some things I have abandoned, some are improved and some are changed. I have inserted several updates (marked as “UPDATE”) in the previous posts in case you are curious. If we have reached some sort of equilibrium at the end of this series I’ll make sure to create a place where people can easily download all relevant configuration and used scripts, but for now everything is a matter of “voortschrijdend inzicht,” a beautiful Dutch phrase that’s hard to translate and certainly hard to pronounce for most of my readers. Given the fact that my previous posts on Vim are well-received and several people are trying it out, it’s time to pick up writing again and start chipping away at the backlog.

To give you a taster of what’s to come:

  • various surprising uses of back-linking between files
  • a script for automatically updating them in-place in a dedicated section each note
  • exploring methods of visualizing relations between notes …
  • … and ideally using this visualizing for some free-roaming navigation as well.

In this post, I want to discuss a seemingly minor issue that will nevertheless potentially have a big impact on your workflow. It concerns the quick creation of new timestamped notes in your note directory or Zettelkasten, and then easily creating a correctly formatted Markdown link to it from another note. If you are impatient, you can have a look at the screencast below. If not, let me give a brief introduction to show you where the potential workflow issue is.

What’s the issue I’m trying to fix?

The authors of this nice Zettelkasten blog argue that you should give up trying to categorize your notes in hierarchical folders and instead should throw everything into one big flat Zettelkasten. This is scary, because notes that do not have many interconnections with other notes may be forgotten when the Zettelkasten gets big (it will be forgotten by you for sure, but also “forgotten” by the Zettelkasten itself if it lacks links). Nevertheless, I’m making the transition because I want to commit to the idea of my note collection being dynamic, organic, an entity of its own, rather than it being a static dump. In order to make this transition, you start to fully rely on your tools. Since I’m hacking together my own tools some issues came up, in this case with using timestamps in filenames.

The O.G. Zettelkasten of Luhmann had an extensive naming convention for organizing notes, but it was more of a necessary evil because computers were not in the picture yet. Given that we now have digital means of naming, searching, and linking notes, a strict naming convention for the notes is an unnecessary complication that blindly applies an “analogue” mindset to a digital solution. The authors from are however strong proponents of the more superficial organization of notes by their time of creation, which they do by inserting a unique timestamp at the beginning of the filename. For me, the best argument for this approach is that unique timestamps are a good way of recovering links through potential filename changes. The main reason I did not use them was however that I used Vim’s default filename/path completion (C-x C-f in insert mode) when making Markdown links. This worked fine for me as long as filenames are meaningful, but this just doesn’t cut it anymore when all filenames start with a timestamp, as you would have to manually start typing the timestamp. An early adapter of my Vim experiment, Boris , did however use long complex timestamps and noticed interlinking was getting in the way of his workflow. Since he now makes all his notes for his PhD in Vim, I certainly do not want to be responsible for trouble! So here it goes …

Create a timestamped Markdown note in your Zettelkasten

Before we solve the bigger issue, let’s add some convenience. When using timestamps, manually typing out the date and time is a pain in the ass. Each timestamp needs to be a unique identifier, so this means you at least also want to include the time of day, potentially up to the amount of seconds if you regularly make multiple notes within a minute. I don’t personally, but the code below is very easy to adjust to your own needs.

First, we declare a variable that holds the location of our Zettelkasten, so we may use it in multiple places without having to retype the whole path.

let g:zettelkasten = "/home/edwin/Notes/Zettelkasten/"

Second, we want to define our own custom command that 1) pre-fills all the stuff we don’t want to type, namely the timestamp and the extension (I always use markdown), and 2) that prompts you for the name of your note:

command! -nargs=1 NewZettel :execute ":e" zettelkasten . strftime("%Y%m%d%H%M") . "-<args>.md"

This will produce a filename like “”. Don’t bother with understanding this. Writing it certainly gave me a headache because I’m new to Vimscript. What is interesting for you is “%Y%m%d%H%M” because it indicates how you want to format your datetime. You can read about this by typing :help strftime and otherwise this is a good resource.

Now all we have to do is declare a mapping to call our command. I use the “n” prefix for all note-related stuff I write myself, so I choose “nz”, which just also happens to be a mnemonic for new zettel.

nnoremap <leader>nz :NewZettel 

Done! Now let’s solve the real problem of effortlessly linking to this note. Warning: it gets pretty sexy ahead.

The main issue was that we never want to type timestamps in order to reap the benefits of path completion to get a Markdown link to the file we want. Now that we are at it, having to format a Markdown link like [description](link) also takes time, so let’s automatize that as well.

My new solution is to rely on my fuzzy file finder to find a file and automatically create a markdown link to it. I use CtrlP with ripgrep, but fzf is also a great choice, see fernando’s comment for a fzf solution. This is a great solution because the fuzzy nature of it allows you to ignore the timestamp altogether. But it also allows you to search on a partial fragment of the time and a part of the note title. I can imagine you for example remember making a note about Zettelkasten somewhere in 2020, but you don’t quite remember the exact date (unless you are Rain Man) and neither the precise name of the file. No problemo! Boot up CtrlP and search on “2020Zettelkasten”. We can extend CtrlP to then automatically create a markdown link to the matching file, with Ctrl-X. Have a look at the short screencast I made .

I started with code provided in this StackExchange post and adjusted it to create valid Markdown links:

" CtrlP function for inserting a markdown link with Ctrl-X
function! CtrlPOpenFunc(action, line)
   if a:action =~ '^h$'    
      " Get the filename
      let filename = fnameescape(fnamemodify(a:line, ':t'))
	  let filename_wo_timestamp = fnameescape(fnamemodify(a:line, ':t:s/\d+-//'))

      " Close CtrlP
      call ctrlp#exit()
      call ctrlp#mrufiles#add(filename)

      " Insert the markdown link to the file in the current buffer
	  let mdlink = "[ ".filename_wo_timestamp." ]( ".filename." )"
      " Use CtrlP's default file opening function
      call call('ctrlp#acceptfile', [a:action, a:line])

let g:ctrlp_open_func = { 
         \ 'files': 'CtrlPOpenFunc',
         \ 'mru files': 'CtrlPOpenFunc' 
         \ }

I just love it. Irregardless of whether I will use timestamps in my filenames, this will greatly speed up interlinking notes in my Zettelkasten.


Secondary sorting in Python <-- Latest

Dynamic BibTeX bibliography paths with spaces <-- Random


Do you want to link a webmention to this page?
Provide the URL of your response for it to show up here.


Fernando on Thursday, Apr 23, 2020:

For those using fzf instead of CtrlP, with help from Zorzi (

you can do:

function! HandleFZF(file)
    "let filename = fnameescape(fnamemodify(a:file, ":t"))
    "why only the tail ?  I believe the whole filename must be linked unless everything is flat ...
    let filename = fnameescape(a:file)
    let filename_wo_timestamp = fnameescape(fnamemodify(a:file, ":t:s/^[0-9]*-//"))
     " Insert the markdown link to the file in the current buffer
    let mdlink = "[ ".filename_wo_timestamp." ]( ".filename." )"

command! -nargs=1 HandleFZF :call HandleFZF(<f-args>)

Edwin on Friday, Apr 24, 2020
In reply to Fernando

That’s very useful! That’s my official debute on stack exchange, by the way, so thanks for that. I’ve actually meant to mention in the post that this should be easy in fzf as well. This is a great addition, because I know most people prefer fzf. The only reason I do not use fzf for this notetaking system is that I require it to be cross-platform and fzf doesn’t work great on Windows. I’ll refer to your comment in the post 😊.

Just as a sidenote, one fzf-related thing I’d like to try out (but I didn’t yet) is fzf notational velocity.

Edwin on Friday, Apr 24, 2020
In reply to Fernando

Oh and I see I overlooked a question hidden in the code. “Why only the tail?". That’s actually a better question than you may initially suspect. I’ve struggled a bit with this and there’s multiple layers to my answer.

First the practical answer: for Vim it’s not necessary to include the whole (relative) path if you set Vim up this way:

" Include subfolders
set path+=**

This way, Vim will find your file anyways with gf, even if the note is in a different folder, assuming you follow the convention I use that your working directory is the root directory of all your notes.

The real answer is a bit more intricate, because I additionally want all Markdown links to work when I export all my files as a website, or preview them on GitHub. With your current solution, the link would look like this if I’m not mistaken “Zettelkasten/”. This will work for Vim because your working directory is the root directory of your notes, but it won’t for example on GitHub. It would look for Zettelkasten/Zettelkasten/ . You could fix this by instead using “../Zettelkasten/”, but this will break your linking in Vim!

TLDR; unless my reasoning is flawed, this will force you into some hybrid linking convention where you’ll maintain references relative to your Vim working directory and links that function anywhere, relative to your specific note. You could use the Vim link (just the filename with the path setting from above) as the link description, and the other as the actual link.

This all made me commit to the Zettelkasten method because they work with a flat directory anyways, which solved everything. Additionally, having a flat directory makes writing my own tools so much easier.

Robert on Thursday, May 7, 2020
In reply to Fernando

Thanks for creating the code for fzf. But I can’t figure out how to use it.

I added to my vimrc and when I use the :HandleFZF I see it needs an argument after the command. I can type something after and a link is created with the text I typed but I am not sure how to to use FZF with it.

Could you provide a little explanation on how to use it?


Edwin on Thursday, May 7, 2020
In reply to Fernando

Hi Robert, I haven’t tried this myself but what you want is to let fzf autofill the argument of the HandleFZF function and call it. So you are right that you are not supposed to call it manually. Fernando left out the following line:

call fzf#run({'sink': 'HandleFZF'})

This should dump the currently selected file into the HandleFZF function. As I don’t use FZF myself I’m not sure if there is a default keychord that triggers this hook. What should work in any case is defining a key mapping with nnoremap to call fzf#run({'sink': 'HandleFZF'}). Please let us know if this works so that others won’t run into the same issue! :-)

Robert on Thursday, May 7, 2020
In reply to Fernando

Edwin, Your addition was perfect the script now runs. Thanks for the help.

I am new to vimscript but I hope to take your link idea along with Fernando’s and add Silver Searcher to it.

fzf.vim along with Ag (silver sercher) allows you to search for a term(s) in documents and gives you a preview of the document(s) before you open it(them). I open to take that and add the capability to create a link from the selected documents into the current buffer.

Edwin on Thursday, May 7, 2020
In reply to Fernando

Great! That’s cool! It sounds like you are looking for notational-fzf-vim. Vimscript can be a hassle by the way. For a less trivial things I tend to switch to Python (you can also use Python from within Vimscript, just so you know).

If you have a nice workflow going at some point, I would be glad to hear it!

hbenevides on Sunday, May 10, 2020
In reply to Fernando

Hey guys,

Based on your comments I wrote a script that inserts links using a custom fuzzy completion (from fzf.vim, so don’t need to exit vim insert mode to create links. First, I mapped <c-l>z in insert mode to call fzf#vim#complete using rg to match any line starting with a # (title of notes). The result, then, is send to another function which creates the link.

P.S: I use only timestamps in my filenames.


" make_note_link: List -> Str
" returned string: [Title](
function! s:make_note_link(l)
        " fzf#vim#complete returns a list with all info in index 0
        let line = split(a:l[0], ':')
        let ztk_id = l:line[0]
        let ztk_title = substitute(l:line[1], '\#\s\+', '', 'g')
        let mdlink = "[" . ztk_title ."](". ztk_id .")"
        return mdlink

" mnemonic link zettel
inoremap <expr> <c-l>z fzf#vim#complete({
  \ 'source':  'rg --no-heading --smart-case  ^\#',
  \ 'reducer': function('<sid>make_note_link'),
  \ 'options': '--multi --reverse --margin 15%,0',
  \ 'up':    5})

Edwin on Sunday, May 10, 2020
In reply to Fernando

@hbenevides thanks for sharing! This would be a great guest post, if you care to explain your rationale :-) Feel free to email me if you are interested.

hbenevides on Sunday, May 10, 2020
In reply to Fernando

@Edwin, that would be cool. I’m not sure if I have a rationale. Perhaps with this it is possible to reduce friction and also have good-looking notes =)

BTW, great job in this series of posts! Useful content expressed in good written.

(Apologies for the typos in the previous comment =|)

Edwin on Monday, May 11, 2020
In reply to Fernando

Thanks for the compliment! It is perfectly fine to keep a post very simple and minimal, even better I would say. You could write it as a response/addition. I think it would already be valuable to:

I’m still struggling with the best naming convention, so I’m really interested in the whole timestamp-for-filename thing. If I would switch, I would also need a method for finding the correct note based on its title. (So assuming you write a guest post, I may end up responding to it with a CtrlP version!).

In short, I strongly encourage you to give it a try! I’m sure other people would be interested as well and above all, it’s fun!

Robert on Monday, May 11, 2020
In reply to Fernando

Edwin and hbenevides,

I took hbenevides script and modified it so it works with silver searcher and presents a preview box. Rather than throw the script in the comments I posted it on my blog. I hope it helps.

Edwin on Monday, May 11, 2020
In reply to Fernando

@Robert That’s great! I have to step up my game now to match the combined effort of all of you! I’m curious where CtrlP will fall short of fzf when it comes to customization. Great minimalistic website, I subscribed to your RSS so I’ll be following you (many years ago since I translated some Greek, hey, maybe I pick some up again).

I am still interested in having this information on my website in a place that is more accessible than the comments, because I think it’s a great addition to this post. It would be easier to find for people. It could be a cross-post of your blog @Robert, or a new write-up by @hbenevides. I could try out fzf myself and do it, but since it is a combined effort of all of you, you should take the credit :-)

nv2lt on Saturday, May 23, 2020
In reply to Fernando

This is an excellent post Edwin. Well done! In reply to your comment in 24 Apr 2020:

The real answer is a bit more intricate, because I additionally want all Markdown links to work when I export all my files as a website, or preview them on GitHub. With your current solution, the link would look like this if I’m not mistaken “Zettelkasten/”. This will work for Vim because your working directory is the ...

In order to work in Gitlab (I assume that the same applies for Github) I made the following modification to Fernardo’s script: let mdlink = "[".filename_wo_timestamp[:-4]."](./".filename[:-4].")" This removes .md suffix and also adds the ./ in order to make it work for Gitlab wiki pages. By doing this you can have nested folders and all links in Gitlab wiki will work. The problem now is that by removing .md suffix the vim’s gf command no longer works. In order to overcome this I use the plasticboy/vim-markdown plug which allows to use ge instead of gf to follow markdown link inside vim. It also allows to have link to Headings inside a file like file. In this scenario you can have the same pattern work both inside vim and in gitlab (probably github also).

Congrats again!

Edwin on Saturday, May 23, 2020
In reply to Fernando

@nv2lt thanks for your reply and your addition! I personally prefer to not rely on plasticboy/vim-markdown (I like vim-pandoc), but this is a great suggestion for those that prefer to keep nested directories! This again shows the beauty of the Vim-route: everyone can find a way to adjust it to their preferences.

Thanks a lot for dropping by!

Jared on Friday, Jul 31, 2020
In reply to Fernando

A little bit that I added to the fzf function that maybe someone will find useful. (everything before put, the first five lines, is exactly the same)

fu! HandleFZF(file)
    let filename = fnameescape(fnamemodify(a:file, ":t"))
    let filename_wo_timestamp = fnameescape(fnamemodify(a:file, ":t:s/^[0-9]*-//"))
    let mdlink = "[ ".filename_wo_timestamp." ]( ".filename." )"
    let curfilename = fnameescape(expand("%:t"))
    let curfilename_wo_timestamp = fnameescape(fnamemodify(expand("%"), ":t:s/^[0-9]*-//"))
    let curmdlink = "[ ".curfilename_wo_timestamp." ]( ".curfilename." )"
    call writefile(add(readfile(filename), curmdlink), filename)

In short this is two way linking. So if file A is open and we insert a link to file B with the script B my addition would also read B and append to the file a link to file A A

For the uninitiated the % in vim represents the current file, but when using it in a function it is treated differently so you have to use the expand function to expand the variable to the filename. The final line is the one that does the writing to the other file. readfile grabs the contents of a file and turns it into a list object. Add appends to a list, and writefile takes a list and writes it to a file.

Final bit you could also use the file completion mapping to call the function (though I would recommend moving your things to a ftplugin file then so it doesn’t map it that way everywhere). Basically for our fzf function that would look like this. (the only works inside ftplugin so if you put this in your general vimrc remove the )

inoremap <buffer> <C-X><C-F> <esc>:call fzf#run({'sink': 'HandleFZF'})<CR>A

Also an alternate way of doing your new zettel command could be to do this instead.

 cnoremap :nz e /home/edwin/Notes/Zettelkasten/<c-r>=strftime("%Y%m%d%H%M")<CR>-

I prefer this way because I am used to hitting : to deal with files, rather than my leader key which I use to do other things. There are some slight drawbacks in comparison. For one if you type nz anywhere in the command line it will replace the nz with the whole command. Could be annoying if you have notes on New Zealand. Second you would still have to type .md but I know I would do that anyway if I used your command and I would end up with a lot of files ending with

Just to break one thing down for those that don’t know it Ctrl-r is how you access your registers (where you copy paste from) when you aren’t in normal mode. and the “=” register just takes a function and pastes the output in place.

Personally I write my notes before I am ready to save them and I am already in the directory so mine looks like this.

cnoremap :wt w <c-r>=strftime("%Y%m%d%H%M")<CR>-

Edwin on Sunday, Aug 2, 2020
In reply to Fernando

@Jared, great stuff! Thanks for putting effort into the thorough explanation! It’s time I’ll look into fzf myself, now that I have some free time on my hand. I’ll definitely have a closer look at your contribution!

P.S. sorry for the late acceptation of your comment; I do moderation by hand and I’ve been travelling.

Jared on Sunday, Aug 2, 2020
In reply to Fernando

Okay so on my above comment I screwed up some sentences because I didn’t account for the markdown syntax.

That first paragraph after the first codeblock the links in there were me trying to show an example of the markdown link. Of course those got translated into html links so that doesn’t show. Also in the paragraph before the second codeblock I was trying to warn people not to include the buffer statement if it is in your general vimrc rather than ftplugin. the buffer is between two triangle brackets so the markdown disappeared the word which is why the sentence trails off.

@edwin thanks for saying I gave a through explanation after looking at my comments I am beginning to think I might be long winded.

alex on Friday, Apr 24, 2020:

Strong post! A technical post that is practical, yet unheard of. Unheard of in the way of introducing a new idea that might be valuable to you. 👏

Edwin on Friday, Apr 24, 2020
In reply to alex

That’s a great compliment, thanks!

alex on Friday, Apr 24, 2020:

Now make a graphly-linked note taking plugin for vim :p

Edwin on Friday, Apr 24, 2020
In reply to alex

Well that will take me a while :-D. However, I am experimenting with different ways of visualizing and graphically browsing notes, so you can expect a post about that (at some point in the not so near future). If you have time for something like that, it would be a great guest contribution to this project! ;-)

Robert on Monday, May 11, 2020:

@Edwin In terms of controlP vs fzf with ripgrep or silver searcher, fzf, ripgrep, and silver searchers are command line programs that can be used in vim via a wrapper or plugin. I was already using fzf and silver searcher on the command line before I started using it in Vim. FZF is a Swiss Army Knife type of application, I can use it in Vim, to select music for mpd, or many other tasks. I use it because that is what I heard of first and it is so versatile.

Edwin on Monday, May 11, 2020
In reply to Robert

Oh yeah fzf is great! My comment about customization above wasn’t that clear. The only reason I don’t use rely on fzf.vim is because I want my setup to work effortlessly on any OS. fzf works amazing on Linux, but I had trouble with it on Windows (I don’t necessarily blame fzf for that, by the way). The mileage of command-line tools is just less on Windows because the command line works different, although there is improvement in that area. Anyways: that’s what I meant with customization. CtrlP actually doesn’t have external dependencies by default and works in any Vim install. So I was wondering if a setup that’s very dependent on external tools would work that well on an OS like Windows. Additionally, I suspect some nice functionality of fzf.vim may not be in CtrlP, but I should explore that a bit more.

Having said that, only one way to find out. I should give the fzf + ag setup a go on Windows.

Jared on Saturday, Aug 1, 2020:

Its me again. A couple other things I thought could be helpful. I grabbed this bit of code from a commit to the markdown syntax file that never made it in. You would place this in .vim/after/syntax/markdown.vim. It is a syntax file that turns concealing on for markdown links. So your link would go from this to looking like this []. The stuff inside () is still there of course it is just “concealed”. When the cursor is in the same line it it would expand out to the full set again, or be revealed. Although if you don’t want it displayed at all you could also add, to the code below, setlocal concealcursor=nvic.

syntax spell toplevel
setlocal conceallevel=2

runtime! syntax/html.vim

syn region markdownLinkText matchgroup=markdownLinkTextDelimiter start="!\=\[\%(\_[^]]*]\%( \=[[(]\)\)\@=" end="\]\%( \=[[(]\)\@=" keepend nextgroup=markdownLink,markdownId skipwhite contains=@markdownInline,markdownLineStart concealends
syn region markdownLink matchgroup=markdownLinkDelimiter start="(" end=")" contains=markdownUrl keepend contained conceal
syn region markdownId matchgroup=markdownIdDelimiter start="\s*\[" end="\]" keepend contained conceal
syn region markdownAutomaticLink matchgroup=markdownUrlDelimiter start="<\%(\w\+:\|[[:alnum:]_+-]\+@\)\@=" end=">" keepend oneline concealends

Final bit you can add this into an ftplugin to change this mapping only for markdown files, or only for you notes directory if you set up an ftdetect. Basically the mapping takes the follow file commands and alters them to move to the next ( and then do the command. This way if your cursor is on a link inside [] it will move to the file inside () and follow that file. This even works if you use the conceal cursor from above and are unable to see the ().

nnoremap <buffer> gf f(gf
nnoremap <buffer> <c-w>f f(<c-w>f

Edwin on Sunday, Aug 2, 2020
In reply to Jared

Welcome back! A while back I also wrote code for concealing markdown syntax, but borrowing it from a commit like you did would have been more efficient :-). Before that I tried the vim-markdown plugin, which also has a conceal function, but I really disliked that it forced some stylistic choices on me. However, ultimately I found out that the pandoc plugin I use also already offers proper concealing of markdown links (duh!). I use that now.

If I remember correctly, this should already do the trick (with set conceallevel=1):

let g:pandoc#syntax#conceal#urls = 1

With regards to your second tip: awesome! Honestly, it’s so straightforward and yet I didn’t think of doing it myself like that. There are plugins for just following markdown links, but I also didn’t like it (although I don’t remember my particular issues with it). So thanks!

Small remark, for the future: in your comments you used four backticks for code blocks instead of three, which broke the conversion to html :-)

Jared on Sunday, Aug 2, 2020
In reply to Jared

Ha so I was talking about markdown syntax for links on a forum that takes markdown syntax so a couple of my comments are confusing because the syntax got converted into links. I assume code block should fix that So this sentence.

It is a syntax file that turns concealing on for markdown links. So your link would go from this to looking like this [].

Is supposed to read like this. (I only used three ticks this time.) :)

It is a syntax file that turns concealing on for markdown links. So your link would go from this []( to looking like this 

Yeah I felt clever getting it from the commit. Yeah I used that pandoc plugin for awhile but then I realized I only used like two of its features so I got rid of it,

You are right conceallevel 1 is enough.

I tried to take the link following function out of the vim-markdown plugin but my head kind of exploded trying to take his function for that and get it to work independently. So I figured I should just come up with an easy solution. I like tinkering and pretending to be an elitist so I try to take code snippets, or do things myself instead of installing plugins.

Leave a comment

Thank you

Your post has been submitted and will be published once it has been approved.


Something went wrong!

Your comment has not been submitted. Return to the page by clicking OK.