Any Use of Spaced Repetition/Incremental Reading?

Hello everyone, first-time poster here who just discovered Org-Roam this week.

I was poking around for a way to cobble together a bunch of Learning/Knowledge Management ideas using Emacs, and I happened to stumble on Org-Roam, which seems like exactly what I didn’t even know I wanted. I’m still getting set up and comfortable with it, but I have a question about a possible extension:

Has anyone incorporated any kind of spaced repetition to their workflow with Org-Roam? I saw Org-FC mentioned in the appendix of the documentation, but I haven’t done anything with it yet.

I’m specifically interested in Incremental Reading, which I think the ecosystem ought to support with a little configuration.

Even if nobody else has given this a shot, I’ll share my efforts once I start working on getting it up and running, but I didn’t want to reinvent the wheel if anybody has more thoughts than you can find at the Emacs Wiki incremental reading page or the Org-Drill page.

1 Like

I think some people have used Anki for spaced repetition, I would look at this thread …


Thanks very much for the link, that package certainly looks great, and I plan to start using it with my basic flashcards. I’ve been hoping to find a way, though, to use an in-Emacs spaced repetition package, like org-drill (linked above) or org-fc in order to present information within emacs.

One example workflow might be to put my fleeting thoughts into spaced repetition in order to prompt me to refine them into permanent slips. If I wasn’t ready to work on it, I could “remember” it and have it pop up later.

Another might be to have a longer captured excerpt pop up based on the SRS algorithm and then I could capture some portion of it to create a new note with a backlink to the longer piece.

As I said in the OP, I’m still getting comfortable with the more basic functions of org-roam, but I look forward to trying to build out some workflows like the above.

I’m very interested in this - I’ve been using Anki/AnkiEditor, but so far it’s more to remember facts than for incremental reading. I’d like to use it for that too and would be interested to hear in any solution you come up with.

This post on How to build a spaced repetition system in Roam Research might be an interesting approach for a way to leverage SRS in org-roam that doesn’t require any additional package at all.

Andy Matuschak has some good notes on spaced repetition for application, synthesis and creation.


Thanks very much for those links! I wasn’t able to read the first without a membership, which was disappointing, but Andy Matuschak’s thoughts were really interesting and useful. I’ve vaguely tried some variations similar to what he talks about, like using some Anki cards to remind me to apply something, rather than just remember it, but his framework is much more complete. I could see that idea working nicely with org-roam by having a card that has links to others in an SRS set up, and the prompt is to read through the links or look for more connections and link them.

Hi there,
Your question triggered me to share my basic way of doing spaced repetition in org mode. I recently integrated org-roam into it by using custom Elisp links in my agenda.
Give a look to if you are curious!

I got wrong-number-of-arguments with both snippets, any update? I am using Emacs 27 /org-mode9.4 with Arch Linux.

Hey mistan,

Very weird. I just tried with a clean installation of Emacs 27 with Org
9.4.4 and the code (at the bottom of the post) worked fine.

Any chance you can set (setq debug-on-error 't) and share the stack

Hi Andrea,

Thanks for your effort and prompt response. The following is what I am getting.

funcall-interactively: Wrong number of arguments: (lambda (e) "Resets the header on the TODO states and increases the date
according to a suggested spaced repetition interval." (interactive) (edebug-enter 'my/space-repeat-if-tag-spaced (list e) #'(lambda nil (edebug-after (edebug-before 0) 73 (let* ((spaced-rep-map '(... ... ... ... ... ...)) (spaced-key "spaced") (tags (edebug-after (edebug-before 1) 2 (org-get-tags))) (spaced-todo-p (edebug-after (edebug-before 3) 6 (member ... ...))) (repetition-n (edebug-after (edebug-before 7) 11 (car ...))) (n+1 (edebug-after (edebug-before 12) 29 (if ... ... 0))) (spaced-repetition-p (edebug-after (edebug-before 30) 33 (alist-get ... ...))) (new-repetition-tag (edebug-after (edebug-before 34) 38 (concat "repetition" ...))) (new-tags (edebug-after (edebug-before 39) 49 (reverse ...)))) (edebug-after (edebug-before 50) 72 (if (edebug-after (edebug-before 51) 54 (and ... ...)) (edebug-after (edebug-before 55) 71 (progn ... ... ... ... ...))))))))), 0

UPDATE: After I restarted my system, it seems working now. This is really cool! Is there a way to skip adding DONE tag in headline?

Hey mistan,

I guess you are using M-x my/space-repeat-if-tag-spaced, but I designed
the function to run automatically when you use M-x org-todo if you
have a tag :spaced: on the heading.

An example of heading:

  • TODO something I really want to learn :spaced:

If you call M-x org-todo on it, it will be rescheduled in a spaced
repetition fashion.

In Elisp you can call a function with M-x only if it starts like:

(defun fun () (interactive) …)

I prefer Emacs doing things for me when possible, so I hid the thing
behind a hook (which runs the function after another action I do):

(add-hook 'org-trigger-hook 'my/space-repeat-if-tag-spaced)

Hope it helps!

Hi Andrew,

Thanks for your reply. You guessed right. I did try to call M-x my/space-repeat-if-tag-spaced after adding (interactive).

Just to clarify. It will insert DONE in the headline after clicking done in each repetition. Any way to prevent the headline from modified?

sorry for the late reply. My function should just mark it done and then set it to todo again. Also it should not be used interactively, but it silently works when you mark a headline as done. Instead of setting it as DONE, it will increase the repetition counter tag.

Hi Andrea,

I like your way of doing spaced repetition using just the agenda. I feel that it is more suitable for relatively large and complicated notes that simple flashcards are too cumbersome to handle.

I tried your snippet and noticed something and I wonder whether you could offer some help.

I’ve added the :spaced: tag to a heading and called org-todo on it, then a new date was scheduled. When I open the agenda (M-x org-agenda), I would like to filter by the spaced tag so I get to see all the drill items separately from my normal TODO items. I tried to use the Match a TAGS/PROP/TODO query option, and after typing spaced to the prompt, the bottom bar shows Wrong type argument: arrayp, nil, with nothing showing up in the filtered results.

Any idea how I could achieve the tag filtering? Thanks

UPDATE: here is the messages I gathered after (setq debug-on-error 't) (:

Debugger entered--Lisp error: (wrong-type-argument arrayp nil)
  substring(nil 0 15)
  (org-eval (substring (org-entry-get nil "CLOSED") 0 15))
  (format "%s" (org-eval (substring (org-entry-get nil "CLOSED") 0 15)))
  (format "%s] " (format "%s" (org-eval (substring (org-entry-get nil "CLOSED") 0 15))))
  eval((format "%s] " (format "%s" (org-eval (substring (org-entry-get nil "CLOSED") 0 15)))))
  org-agenda-format-item("" #("TODO Move objects :drill:spaced:repetition0:" 0 4 (face org-todo org-category "blender" org-todo-head #("TODO" 0 4 (face org-todo)) wrap-prefix #("*** " 0 4 (face org-indent)) line-prefix #("*" 0 1 (face org-indent)) fontified t) 5 17 (face org-level-2 org-category "blender" org-todo-head #("TODO" 0 4 (face org-todo)) wrap-prefix #("*** " 0 4 (face org-indent)) line-prefix #("*" 0 1 (face org-indent)) fontified t) 18 43 (keymap (keymap (follow-link . mouse-face) (mouse-3 . org-find-file-at-mouse) (mouse-2 . org-open-at-mouse)) mouse-face highlight face (org-tag org-level-2) org-category "blender" wrap-prefix #("*** " 0 4 (face org-indent)) line-prefix #("*" 0 1 (face org-indent)) fontified t) 43 44 (keymap (keymap (follow-link . mouse-face) (mouse-3 . org-find-file-at-mouse) (mouse-2 . org-open-at-mouse)) mouse-face highlight face (org-tag org-level-2) org-category "blender" rear-nonsticky (mouse-face highlight keymap invisible intangible help-echo org-linked-text htmlize-link) wrap-prefix #("*** " 0 4 (face org-indent)) line-prefix #("*" 0 1 (face org-indent)) fontified t)) "  " "blender" ("drill" "spaced" "repetition0"))
  org-scan-tags(agenda (lambda (todo tags-list level) (progn (setq org-cached-props nil) (or (and (member "spaced" tags-list))))) nil)
  funcall-interactively(org-tags-view nil)
  funcall-interactively(org-agenda nil)
  call-interactively(org-agenda nil nil)

Hello Jason,
the debugging trace you showed suggests that your entry is malformed

  eval((format "%s] " (format "%s" (org-eval (substring (org-entry-get nil "CLOSED") 0 15)))))

seems to say that the CLOSED property (I guess in the logbook properties, not sure) has a problem.
The heading seems to be “TODO Move objects :drill:spaced:repetition0”

Maybe you need to fix something in there?

Thanks for the reply.

It is indeed something on my side. That CLOSED thing came from another snippet I copied from somewhere else (dont even remeber now). And there are other issues in my agenda settings, which turn out to be quite a mess. For instance, I found that (setq org-agenda-files (list "~/Notebooks/org")) somehow does not include all the org files in it, and that is causing the spaced-tagged headings not showing up. I’m still struggling to set things straight.

But I’m certainly going to use your invention.

Hey Jason,

Cool, hope it serves you well! Indeed org-agenda-files requires a list
of files: you can do

(setq org-agenda-files (seq-filter
(lambda (file) (string-suffix-p
“.org” file))
(directory-files “~/Notebooks/org”)))

That takes all the files in your directory (directory-files) and filters only for .org
ones (seq-filter + the suffix predicate).

Have fun!

Hi Andrea,

The seq-filter trick worked, I can also filter out some org files that I’d like to ignore now. Very helpful!

But there is no end to man’s laziness: could you help me create a function that i) sets the :spaced: tag to a heading and ii) makes a schedule to it? Then I could bind that to a key-binding and quickly set up a heading for SR.

Based on your code I guess it would somehow involve the org-set-tags-to and org-todo functions? My elisp is too poor to work that out.


Mmm, why don’t you use Org Capture for that? You just need a template
and it is easy to setup: Capture (The Org Manual)
So you can avoid most of the Elisp complexity

I guess Captures will do better for creating new headings. But I have already a good number of existing ones that I’d like to include into SR.

I think I’ve managed to craft up one which seems to function, at least for now:

(defun my-set-spaced-repetition ()
  "When called on a heading: 1) append the :SR: tag if not already there, 2) set a schedule
   to the next day (++1d)"
    (if (org-current-level) (progn 
	(let* ((tags (org-get-tags))
	       (sr-tag "SR"))
	(if (member sr-tag tags) (message "Already has SR tag.")
	    (progn (add-to-list 'tags sr-tag t) ; append SR to tag list
                  (add-to-list 'tags "repetition0" t)
		  (org-set-tags tags)
		  (message "Added SR tag to heading.")
	(org-schedule nil "++1d")

(global-set-key (kbd "C-c r") 'my-set-spaced-repetition)

Oh, well done Jason!! I have been amazing at Elisp :slight_smile:
For curiosity (your code looks great) there is an append function in
Elisp to join lists, so you can do things like

(append (org-get-tags) (list “SR” "repetition0))

Again great job! Also, if you look for support with Elisp or Emacs in
general, you may be interested into the Emacs Buddy initiative: GitHub - ag91/emacs-buddy: Emacs Buddy initiative to help new Emacs users with their struggles