README.org
July 10, 2026 ยท View on GitHub
#+title: vulpea-para
PARA for your notes, without the filing.
#+begin_html
#+end_htmlvulpea-para brings the PARA method (Projects, Areas, Resources, Archives) to an Org-mode note collection, on top of [[https://github.com/d12frosted/vulpea][vulpea]]. The idea: you do not file notes into four folders. A note lives wherever it lives, and its PARA role is read back from its tags with a fast database query. You reclassify by flipping a tag, not by moving files around and repairing links.
This is early software, pulled out of a personal PARA setup I have run daily since ~2024-2025 and rebuilt here to be tested, explained, and shareable. Expect things to move.
- The shape of it
Four buckets, recognized rather than filed:
- Areas are the pillars: one note per ongoing responsibility (the blog, finances, a garden), tagged =:area:=.
- Projects are headings with a finish line, tagged =:project:=, living inside the area they belong to.
- Resources are your reference notes; most of your notes are these.
- Archives are anything that has gone quiet, moved aside but still searchable.
And the nice part: these are facets, not walls. A note can be more than one at once (a person is a resource you read, and an area you keep up).
- What you can do
- Capture into the right place without filing: a project under an area (=M-x vulpea-para-capture-project=), a new area (=M-x vulpea-para-capture-area=), and, through the Org capture building blocks (see below), tasks and meetings filed under the person they are about. Projects get a readable =Area > Project= category.
- Lean on an agenda that builds itself from the notes that actually hold open work, in a few milliseconds even across thousands of notes: turn on =vulpea-para-agenda-mode=. Ready-made views come with it, =vulpea-para-agenda-main=, =-person=, and =-area=, plus command building blocks (=vulpea-para-agenda-cmd-*=) you splice into your own dispatcher.
- Refile anywhere in the vault, right from the agenda: =vulpea-para-refile-files= feeds =org-refile-targets= every live note (areas first) instead of just the agenda files, and =vulpea-para-refile-mode= answers the target list from the database, so it is instant even at thousands of notes where org's own file scan would take minutes.
- Find an area, a project, or a resource: =vulpea-para-find-area= and friends.
- Archive a finished project into its area: =M-x vulpea-para-archive-project=.
- Keep the whole thing honest with =M-x vulpea-para-doctor=.
There is also a small, fast read API (=vulpea-para-areas=, =vulpea-para-projects=, =vulpea-para-resources=, =vulpea-para-area-of=, and so on) for building your own views on top, including reactive ones with vui.el.
The [[file:docs/getting-started.org][getting started guide]] walks through setting this up from scratch, and the design lives as short notes under [[file:docs/adr/][docs/adr/]].
- Wiring it into your config
The fast path: call =(vulpea-para-setup-defaults)= and you get a working setup out of the box, a default agenda dispatcher in its own buffer (=vulpea-para-agenda-main-buffer-name=), the =Area > Project= prefix, capture templates, and the self-updating agenda turned on.
It is opt-in, though: vulpea-para never sets an org variable on its own. Skip =setup-defaults= and assemble the pieces yourself, so your agenda and capture layout stays exactly yours. Here is the full version.
The self-updating agenda:
#+begin_src emacs-lisp (require 'vulpea-para) (vulpea-para-agenda-mode)
;; keep a cemetery (or anything) out of the agenda file list (setq vulpea-para-agenda-files-filter (lambda (note) (not (vulpea-note-tagged-any-p note "cemetery")))) #+end_src
By default the mode only tags files in your vault (under =vulpea-db-sync-directories=), so the Org files you edit elsewhere never pick up an =agenda= tag. Point =vulpea-para-agenda-tag-scope= at a different predicate to widen or narrow that.
A file only earns the =agenda= tag while it holds open work, so an empty or all-done file drops off the agenda. To keep one on regardless, list it in =vulpea-para-open-work-files=; such a file always counts as open work. Entries are bare file names (matched by base name, so ="inbox.org"= matches wherever it lives), absolute file names, or predicates of the buffer's file name. Handy for an inbox you always want in view:
#+begin_src emacs-lisp (setq vulpea-para-open-work-files '("inbox.org" "inbox-dor.org")) #+end_src
One catch when you bring an existing vault: the tag is maintained on save, so a file whose open work predates the mode is invisible to the agenda until it happens to be saved again. The symptom is TODOs that show up in =vulpea-para-agenda-area= (which reads the file directly) but not in the main agenda or the global todo list. The first agenda build of a session checks the database for this drift and warns when it finds any; run =M-x vulpea-para-agenda-backfill= once to tag every such file and the agenda catches up (=vulpea-para-agenda-warn-missing= turns the check off). If TODOs are still missing after a backfill, check that the files live under =vulpea-db-sync-directories=: that is both the tagging scope and what the database sees.
Your agenda dispatcher, assembled from the building blocks (this stays in your config; vulpea-para only provides the pieces):
#+begin_src emacs-lisp (setq org-agenda-custom-commands `((" " "Agenda" (,vulpea-para-agenda-cmd-refile ,vulpea-para-agenda-cmd-today ,vulpea-para-agenda-cmd-focus ,vulpea-para-agenda-cmd-stuck-projects ,vulpea-para-agenda-cmd-waiting ,(vulpea-para-agenda-cmd-current-quarter)))))
;; the Area > Project prefix in the agenda (setq org-agenda-prefix-format '((agenda . " %(vulpea-para-agenda-category 36) %?-12t %12s") (todo . " %(vulpea-para-agenda-category 36) "))) #+end_src
Capture, again assembled by you:
#+begin_src emacs-lisp (setq org-capture-templates '(("t" "task" entry (file+headline "inbox.org" "Tasks") (function vulpea-para-capture-task-template)) ("p" "project" entry (function vulpea-para-capture-project-target) (function vulpea-para-capture-project-template)))) #+end_src
Refile, so a captured task can reach any note in the vault, not only the files that already carry open work (with the self-updating agenda, =org-agenda-files= is only the notes that hold work, so building refile targets from it would be backwards; refiling is exactly how work reaches a note that had none):
#+begin_src emacs-lisp (vulpea-para-refile-mode) (setq org-refile-targets '((vulpea-para-refile-files :maxlevel . 3)) org-refile-use-outline-path 'title org-outline-path-complete-in-steps nil org-refile-allow-creating-parent-nodes 'confirm)
;; and, like the agenda, the target list takes a filter (setq vulpea-para-refile-files-filter (lambda (note) (not (vulpea-note-tagged-any-p note "cemetery")))) #+end_src
Targets are every live note, area files first, and they complete by title: a target reads as =Barberry Garden/Tasks=, not =20240807084021-barberry_garden.org/Tasks=, because file names drift away from what a note is called while titles keep up (prefer file names anyway? set =org-refile-use-outline-path= to ='file=). The file itself is a valid target too, so a task can land at the top of an area or resource that has no headings yet.
The mode is what makes this instant. Org normally builds the target list by visiting every candidate file, which takes minutes across a real vault; the database already knows every heading, its level, and its position, so =vulpea-para-refile-mode= answers =org-refile-get-targets= with one query (=vulpea-para-refile-target-table=) and touches no file. It only steps in when =org-refile-targets= is exactly the spec above; anything else (org-goto, a manually restricted refile) falls through to org's own scan. The flip side of reading from the database: targets are the vault's notes, so a heading vulpea does not index is not offered, and a file edited but not yet saved offers its last-synced positions (org detects the drift and asks you to retry). Without the mode the same spec still works on a small vault, scanned by org the slow way, with =vulpea-para-refile-verify-target= available for =org-refile-target-verify-function= to keep archived subtrees out.
Then bind the commands you like: =vulpea-para-agenda-main=, =vulpea-para-agenda-person=, =vulpea-para-agenda-area=, =vulpea-para-capture-area=, =vulpea-para-capture-project=, =vulpea-para-find-area=, and the rest.
- Architecture and roadmap
vulpea-para is a library first: predicates, queries, and the capture, archive, and agenda flows, all reading from vulpea's database so they stay fast at ten thousand notes. The one place that touches a file at write time is the open-work check on save. It drives org's own agenda and capture rather than reinventing them, and its own surface stays small (the interactive commands and the doctor's report).
The richer reactive views (active projects, areas gone quiet, the resources feeding a project, a browsable archive) are planned, and they will live here too, built on vui.el and vulpea-ui over this same read API. Also planned: schema validation of PARA structure.
- Status
Early, but real and tested. Following along is welcome; relying on it in anger is not advised yet.
- Requirements
- Emacs 27.2 or newer
- [[https://github.com/d12frosted/vulpea][vulpea]] 2.2.0 or newer
- Development
#+begin_src sh make prepare # install dependencies make test # run the tests make lint # check style #+end_src
- Support
If you enjoy this project, you can support its development via [[https://github.com/sponsors/d12frosted][GitHub Sponsors]] or [[https://www.patreon.com/d12frosted][Patreon]].
- License
GPLv3. See [[file:LICENSE][LICENSE]].