This reference covers the programmatic API for building tools with Vulpea.
All query functions return vulpea-note structs:
(vulpea-note-id note) ; UUID string
(vulpea-note-path note) ; Absolute file path
(vulpea-note-level note) ; 0 = file-level, 1+ = heading
(vulpea-note-title note) ; Note title
(vulpea-note-primary-title note) ; Primary title (without aliases)
(vulpea-note-aliases note) ; List of alias strings
(vulpea-note-tags note) ; List of tag strings
(vulpea-note-links note) ; List of (type . target) pairs
(vulpea-note-meta note) ; Alist of metadata
(vulpea-note-properties note) ; Alist of org properties
(vulpea-note-todo note) ; TODO state string or nil
(vulpea-note-priority note) ; Priority character or nil
(vulpea-note-scheduled note) ; Scheduled timestamp or nil
(vulpea-note-deadline note) ; Deadline timestamp or nil
(vulpea-note-closed note) ; Closed timestamp or nil
(vulpea-note-outline-path note) ; List of parent headings
(vulpea-note-attach-dir note) ; Attachment directory or nil
(vulpea-note-file-title note) ; Title of file containing noteThe fastest way to get a single note:
(vulpea-db-get-by-id "5093fc4e-8c63-4e60-a1da-83fc7ecd5db7")
;; => #s(vulpea-note :id "5093fc4e..." :title "My Note" ...)
;; => nil if not found(vulpea-db-query-by-ids '("id-1" "id-2" "id-3"))
;; => list of vulpea-note structs (missing IDs are omitted)(vulpea-db-get-file-by-id "note-id")
;; => "/path/to/note.org" or nilFind notes matching a title substring:
(vulpea-db-search-by-title "meeting")
;; => list of notes with "meeting" in titleAll query functions return lists of vulpea-note structs.
;; Notes with ANY of these tags
(vulpea-db-query-by-tags-some '("project" "task"))
;; Notes with ALL of these tags
(vulpea-db-query-by-tags-every '("project" "active"))
;; Notes WITHOUT any of these tags
(vulpea-db-query-by-tags-none '("archive" "deprecated"));; Notes linking to ANY of these IDs (backlinks)
(vulpea-db-query-by-links-some '("target-note-id"))
;; Filter by link type
(vulpea-db-query-by-links-some '("note-id") "id")
;; Notes linking to ALL of these IDs
(vulpea-db-query-by-links-every '("id-1" "id-2"));; Get all attachment destinations with their attach-dirs for notes in a file
(vulpea-db-query-attachments-by-path "/path/to/file.org")
;; => (("image.png" . "/data/note1/") ("document.pdf" . "/data/note2/") ...)This is an optimized query that retrieves all attachment link destinations along with their attachment directories in a single database query, avoiding N+1 queries when processing multiple notes in a file. Each result is a cons cell (DEST . ATTACH-DIR) where DEST is the attachment file name and ATTACH-DIR is the attachment directory of the note containing the link.
;; Notes with a specific metadata key
(vulpea-db-query-by-meta-key "status")
;; Notes where key equals value
(vulpea-db-query-by-meta "status" "active")
;; With type specification
(vulpea-db-query-by-meta "region" "region-id" "note");; Notes from specific files
(vulpea-db-query-by-file-paths '("/path/to/a.org" "/path/to/b.org"))
;; With level filter
(vulpea-db-query-by-file-paths '("/path/to/a.org") 0);; File-level notes only
(vulpea-db-query-by-level 0)
;; First-level headings only
(vulpea-db-query-by-level 1)For complex queries:
(vulpea-db-query
(lambda (note)
(and (member "project" (vulpea-note-tags note))
(string-prefix-p "2025" (vulpea-note-title note)))))(vulpea-db-query-tags)
;; => ("project" "task" "meeting" ...)Find broken ID links that point to non-existent notes:
(vulpea-db-query-dead-links)
;; => ((#s(vulpea-note ...) . "nonexistent-id") ...)Returns list of cons cells (SOURCE-NOTE . BROKEN-TARGET-ID). Only checks links of type “id”.
Find notes with no incoming ID links (nothing links to them):
(vulpea-db-query-orphan-notes)
;; => (#s(vulpea-note ...) ...)Find notes with no incoming or outgoing ID links (completely disconnected):
(vulpea-db-query-isolated-notes)
;; => (#s(vulpea-note ...) ...)This is stricter than vulpea-db-query-orphan-notes — a note that links out but has no incoming links is an orphan but not isolated.
Find notes that share the same title:
;; All notes
(vulpea-db-query-title-collisions)
;; => (("Wine" . (#s(vulpea-note ...) #s(vulpea-note ...)))
;; ("Beer" . (#s(vulpea-note ...) #s(vulpea-note ...))))
;; File-level notes only
(vulpea-db-query-title-collisions 0)Returns list of (TITLE . NOTES) groups where each group has two or more notes with the same title. Optional LEVEL argument restricts to notes at a specific heading level (0 for file-level).
Query links directly from the database. All functions return plists with :source, :dest, :type, and :pos keys.
;; All links
(vulpea-db-query-links)
;; => ((:source "id1" :dest "id2" :type "id" :pos 100) ...)
;; Links of a specific type
(vulpea-db-query-links-by-type "id")
(vulpea-db-query-links-by-type "https")
;; Outgoing links from a note
(vulpea-db-query-links-from "note-id")
;; Incoming links (backlinks) to a note
(vulpea-db-query-links-to "note-id")(vulpea-db-count-notes) ; Total notes
(vulpea-db-count-file-level-notes) ; File-level only
(vulpea-db-count-heading-level-notes) ; Heading-level only;; Default: returns string
(vulpea-note-meta-get note "country")
;; => "France"
;; As number
(vulpea-note-meta-get note "rating" 'number)
;; => 95
;; As symbol
(vulpea-note-meta-get note "status" 'symbol)
;; => 'active
;; As link (extracts ID or URL)
(vulpea-note-meta-get note "url" 'link)
;; => "https://example.com"
;; As note (resolves id: link to vulpea-note)
(vulpea-note-meta-get note "region" 'note)
;; => #s(vulpea-note :id "region-id" :title "Bordeaux" ...)When a key has multiple entries:
- grape :: Cabernet Sauvignon
- grape :: Merlot
(vulpea-note-meta-get-list note "grape")
;; => ("Cabernet Sauvignon" "Merlot")
(vulpea-note-meta-get-list note "ratings" 'number)
;; => (95 92 88)There are multiple ways to read metadata, each with different trade-offs:
| Function | Source | Speed | Always Fresh? |
|---|---|---|---|
vulpea-note-meta-get | Note struct (from DB) | Fast | No (needs sync) |
vulpea-meta-get | File on disk | Slower | Yes |
vulpea-buffer-meta-get! | Pre-parsed metadata | Fastest | Depends |
Prefer =vulpea-note-meta-get= for most cases - it reads from the note struct which is populated from the database:
(vulpea-note-meta-get note "status")Use =vulpea-meta-get= when you need to read directly from the file (always fresh, but slower):
(vulpea-meta-get note-or-id "status")Use bang functions when reading multiple values from the same note via file access. Parse once and reuse:
;; Inefficient: parses file 3 times
(vulpea-meta-get note "a")
(vulpea-meta-get note "b")
(vulpea-meta-get note "c")
;; Efficient: parses file once
(let ((meta (vulpea-meta note)))
(vulpea-buffer-meta-get! meta "a")
(vulpea-buffer-meta-get! meta "b")
(vulpea-buffer-meta-get! meta "c"));; Simple
(vulpea-create "My Note")
;; => #s(vulpea-note ...)
;; With options
(vulpea-create "Project Note"
nil ; file-name (nil = use template)
:tags '("project" "work")
:properties '(("STATUS" . "active"))
:meta '(("client" . "Acme Corp"))
:head "#+created: %<[%Y-%m-%d]>"
:body "* Tasks\n\n* Notes\n")Returns the created vulpea-note. Signals an error if a file already exists at the target path (to prevent accidental overwrites).
Use :parent to create a heading inside an existing note’s file:
;; Create a heading under an existing note
(let ((parent (vulpea-db-get-by-id "parent-id")))
(vulpea-create "Sub-heading"
nil
:parent parent
:tags '("journal")
:properties '(("CREATED" . "[2025-01-15]"))))
;; Insert as first child
(vulpea-create "First Item" nil
:parent parent-note
:after nil)
;; Insert after a specific sibling
(vulpea-create "After Sibling" nil
:parent parent-note
:after "sibling-note-id")The heading level is computed automatically as parent-level + 1. The parent’s file must exist. Returns the created vulpea-note.
| Parameter | Description | Default |
|---|---|---|
title | Note title (required) | |
file-name | File path relative to default directory | from template |
:id | UUID for the note | auto-generated |
:tags | List of tags | |
:head | Content after #+title: | |
:body | Note body content | |
:properties | Alist for property drawer | |
:meta | Alist for metadata | |
:context | Custom variables for template expansion | |
:parent | vulpea-note to create heading under | nil (file-level) |
:after | Insertion position among siblings | 'last |
:after accepts:
'last— append as last child (default)nil— insert as first child- string — insert after the sibling with that note ID
;; By ID
(vulpea-visit "note-id")
;; By note struct
(vulpea-visit note)
;; In other window
(vulpea-visit note t)These operate on the current buffer (must be an org file).
(vulpea-buffer-title-get) ; Get title
(vulpea-buffer-title-set "New Title") ; Set title(vulpea-buffer-tags-get) ; Get tags
(vulpea-buffer-tags-set '("a" "b")) ; Set tags
(vulpea-buffer-tags-add "new-tag") ; Add tag (interactive)
(vulpea-buffer-tags-remove "old-tag") ; Remove tag (interactive)(vulpea-buffer-alias-get) ; Get aliases
(vulpea-buffer-alias-set "A" "B") ; Set aliases (replaces all)
(vulpea-buffer-alias-add "Alias") ; Add alias
(vulpea-buffer-alias-remove "Alias") ; Remove alias;; Set metadata (in current buffer)
(vulpea-buffer-meta-set "key" "value")
(vulpea-buffer-meta-set "rating" "95")
;; Set multiple properties efficiently (parses buffer once)
(vulpea-buffer-meta-set-batch
'(("status" . "active")
("priority" . 1)
("tags" . ("a" "b" "c"))))
;; Sort metadata keys in a specific order
(vulpea-buffer-meta-sort '("status" "priority" "tags"))
;; Keys not in the list are appended at the end
;; File-level version (by note or ID)
(vulpea-meta-set-batch note
'(("status" . "done")
("reviewed" . t)))When setting multiple metadata properties, prefer vulpea-meta-set-batch over multiple vulpea-meta-set calls - it’s significantly faster (up to 10x for 20 properties) since it only parses the file once.
Both file-level and heading-level notes can have their own metadata. The metadata API automatically scopes operations to the correct level based on the note’s level attribute.
For file-level notes (level = 0), metadata is the first description list before any headings:
:PROPERTIES:
:ID: file-id
:END:
#+title: My Note
- status :: active
- count :: 42
Content here...
* Some Heading
:PROPERTIES:
:ID: heading-id
:END:
- status :: done
Heading content...
For heading-level notes (level > 0), metadata is the first description list within that heading’s subtree:
;; Get the file-level note
(let ((file-note (vulpea-db-get-by-id "file-id")))
(vulpea-meta-get file-note "status"))
;; => "active"
;; Get the heading-level note
(let ((heading-note (vulpea-db-get-by-id "heading-id")))
(vulpea-meta-get heading-note "status"))
;; => "done"
;; Each note has its own metadata scope
(vulpea-meta-set file-note "status" "review") ; Only affects file-level
(vulpea-meta-set heading-note "status" "pending") ; Only affects headingThis scoping applies to all vulpea-meta-* and vulpea-buffer-meta-* functions.
Get or modify tags for a specific note (works with both file-level and heading-level notes):
;; Get tags for a note
(vulpea-tags note-or-id)
;; => ("project" "active")
;; Add tags
(vulpea-tags-add note-or-id "new-tag")
(vulpea-tags-add note-or-id "tag1" "tag2" "tag3")
;; Remove tags
(vulpea-tags-remove note-or-id "old-tag")
(vulpea-tags-remove note-or-id "tag1" "tag2")
;; Set tags (replaces all existing tags)
(vulpea-tags-set note-or-id "only" "these" "tags")For file-level notes (level = 0), these modify #+filetags. For heading-level notes (level > 0), they modify org heading tags.
Operate on multiple notes efficiently using vulpea-utils-process-notes:
;; Add a tag to multiple notes
(vulpea-tags-batch-add notes "archived")
;; => count of notes processed
;; Remove a tag from multiple notes
(vulpea-tags-batch-remove notes "active")
;; => count of notes processed
;; Rename a tag across all notes (implements #120)
(vulpea-tags-batch-rename "old-tag" "new-tag")
;; => count of notes modified
;; Interactive: M-x vulpea-tags-batch-rename
;; - Prompts for old tag (with completion from existing tags)
;; - Prompts for new tag name
;; - Shows count of modified notesThe rename function queries for all notes with the old tag, removes it, and adds the new tag. When called interactively via M-x vulpea-tags-batch-rename, it provides completion for existing tags.
Operate on metadata across multiple notes:
;; Set a metadata property on multiple notes
(vulpea-meta-batch-set notes "status" "archived")
;; => count of notes processed
;; Set a list value
(vulpea-meta-batch-set notes "tags" '("a" "b" "c"))
;; Remove a metadata property from multiple notes
(vulpea-meta-batch-remove notes "deprecated-key")
;; => count of notes processedWhen you rename a note’s title, incoming links still show the old title as their description. Vulpea provides tools to propagate title changes to link descriptions and optionally rename the file.
Interactive command to propagate a title change across the knowledge base:
;; Interactive usage
M-x vulpea-propagate-title-change
;; With prefix argument for dry-run preview
C-u M-x vulpea-propagate-title-change
;; Programmatic usage
(vulpea-propagate-title-change note-or-id)The command:
- Identifies the note (from current buffer or prompts)
- Prompts for the old title if not detected by
vulpea-title-change-detection-mode - Offers to rename the file based on the new title slug
- Categorizes incoming links:
- Exact matches: description equals old title or alias (case-insensitive) → can auto-update
- Partial matches: description contains old title → manual review needed
- For exact matches, offers: [!] Update all, [r] Review individually, [s] Skip, [q] Quit
- For partial matches, offers to open files for manual editing
With C-u prefix (dry-run), shows a preview buffer without making changes.
Rename a note’s file based on a new title:
(vulpea-rename-file note-or-id "New Title")
;; => new file path
;; Renames: old_title.org → new_title.org (using slug)
;; Updates database
;; Signals error if target file existsOnly works for file-level notes (level = 0).
Minor mode that detects title changes on save:
;; Enable for current buffer
(vulpea-title-change-detection-mode +1)
;; Enable for all org files
(add-hook 'org-mode-hook #'vulpea-title-change-detection-mode)When enabled:
- Captures the title before each save
- After save, if title changed, shows a message: “Title changed. Run M-x vulpea-propagate-title-change to update references.”
This mode only notifies - it doesn’t automatically update anything.
For modifications where immediate database update isn’t needed:
(vulpea-utils-with-note note
(vulpea-buffer-meta-set "status" "reviewed")
(save-buffer))
;; Database updates asynchronously via file watcherWhen you need to query updated data right away:
(vulpea-utils-with-note-sync note
(vulpea-buffer-meta-set "status" "done"))
;; Database is already updated here
(vulpea-note-meta-get (vulpea-db-get-by-id id) "status")
;; => "done";; Basic selection
(vulpea-select "Select note")
;; => selected vulpea-note or nil
;; With filter
(vulpea-select "Select project"
:filter-fn (lambda (note)
(member "project" (vulpea-note-tags note))))
;; With initial input
(vulpea-select "Select" :initial-prompt "meeting")
;; With alias expansion (allows selecting by alias)
(vulpea-select "Select" :expand-aliases t)
;; When alias is selected, returned note has:
;; - title = the selected alias
;; - primary-title = the original title(vulpea-select-from "Choose" notes)
;; => selected note from provided list
;; With alias expansion
(vulpea-select-from "Choose" notes :expand-aliases t)
;; Each note with aliases appears multiple times in completion(vulpea-select-multiple-from "Choose" notes)
;; => list of selected notes (each note can only be selected once)
;; With options
(vulpea-select-multiple-from "Choose" notes
:require-match t
:expand-aliases t)Control how notes are displayed in completion with vulpea-select-describe-fn:
;; Default: just the title
(setq vulpea-select-describe-fn #'vulpea-note-title)
;; Show outline path (parent headings) before title
;; Example: "Projects → Work → Task"
(setq vulpea-select-describe-fn #'vulpea-select-describe-outline)
;; Show file title and outline path before title
;; Example: "My Notes → Projects → Task"
(setq vulpea-select-describe-fn #'vulpea-select-describe-outline-full)The outline variants are useful when using headings as notes (vulpea-db-index-heading-level) to disambiguate notes with identical titles.
Interactive command for note selection and navigation:
;; Basic usage (interactive)
(vulpea-find)
;; With custom candidates source
(vulpea-find :candidates-fn (lambda (filter-fn)
(vulpea-db-query-by-tags-some '("project"))))
;; With filter
(vulpea-find :filter-fn (lambda (note)
(member "active" (vulpea-note-tags note))))
;; Open in other window
(vulpea-find :other-window t)
;; Require exact match (no creating new notes)
(vulpea-find :require-match t)Interactive command for inserting a link to a note:
;; Basic usage (interactive)
(vulpea-insert)
;; With custom candidates source
(vulpea-insert :candidates-fn (lambda (filter-fn)
(vulpea-db-query-by-tags-some '("person"))))
;; With filter
(vulpea-insert :filter-fn (lambda (note)
(= 0 (vulpea-note-level note))))
;; With custom note creation
(vulpea-insert :create-fn (lambda (title props)
(vulpea-create title nil :tags '("new"))))Both functions support :candidates-fn to customize the source of candidates, and :filter-fn to filter candidates. Global defaults can be configured via vulpea-find-default-candidates-source / vulpea-insert-default-candidates-source and vulpea-find-default-filter / vulpea-insert-default-filter.
;; Update specific file (synchronous)
(vulpea-db-update-file "/path/to/note.org")
;; Update via sync system (async if autosync enabled)
(vulpea-db-sync-update-file "/path/to/note.org");; Update directory
(vulpea-db-sync-update-directory "~/org/")
;; Full scan
(vulpea-db-sync-full-scan)
;; Force re-index
(vulpea-db-sync-full-scan 'force);; Convert title to URL-friendly slug
(vulpea-title-to-slug "My Note Title")
;; => "my-note-title"
;; Make org link to note
(vulpea-utils-link-make-string note)
;; => "[[id:abc123][Note Title]]"
(vulpea-utils-link-make-string note "Custom Description")
;; => "[[id:abc123][Custom Description]]"
;; Expand note into multiple notes based on aliases
(vulpea-note-expand-aliases note)
;; Returns a list of notes:
;; - First element: original note (unchanged)
;; - Additional elements: copies with each alias as title
;; and original title as primary-title
;;
;; Example with note having title "Full Name" and aliases ("Alias1" "Alias2"):
;; => (note-with-title="Full Name"
;; note-with-title="Alias1" primary-title="Full Name"
;; note-with-title="Alias2" primary-title="Full Name")For advanced use cases, direct database access:
;; Get database connection
(vulpea-db)
;; Run raw SQL
(emacsql (vulpea-db)
[:select * :from notes :where (= id $s1)]
"note-id")→ See Plugin Guide for custom tables and extractors.
| Function | Description |
|---|---|
vulpea-find | Interactive note selection and navigation |
vulpea-find-backlink | Find notes linking to current note |
vulpea-insert | Insert link to a note |
vulpea-create | Create file-level or heading-level note |
vulpea-visit | Visit note by ID or struct |
| Function | Description |
|---|---|
vulpea-db-query | Query with predicate |
vulpea-db-get-by-id | Get single note by ID |
vulpea-db-query-by-ids | Get multiple notes by IDs |
vulpea-db-get-file-by-id | Get file path for note ID |
vulpea-db-search-by-title | Search by title substring |
vulpea-db-query-by-tags-some | Notes with ANY tags |
vulpea-db-query-by-tags-every | Notes with ALL tags |
vulpea-db-query-by-tags-none | Notes WITHOUT tags |
vulpea-db-query-by-links-some | Notes linking to ANY IDs |
vulpea-db-query-by-links-every | Notes linking to ALL IDs |
vulpea-db-query-attachments-by-path | Attachment (dest . attach-dir) pairs |
vulpea-db-query-by-meta | Notes with metadata key=value |
vulpea-db-query-by-meta-key | Notes having metadata key |
vulpea-db-query-by-file-paths | Notes from specific files |
vulpea-db-query-by-level | Notes at specific level |
vulpea-db-query-tags | Get all unique tags |
vulpea-db-query-dead-links | Find broken ID links |
vulpea-db-query-orphan-notes | Notes with no incoming ID links |
vulpea-db-query-isolated-notes | Notes with no connections at all |
vulpea-db-query-title-collisions | Notes sharing the same title |
vulpea-db-query-links | All links as plists |
vulpea-db-query-links-by-type | Links filtered by type |
vulpea-db-query-links-from | Outgoing links from a note |
vulpea-db-query-links-to | Incoming links (backlinks) to a note |
| Function | Description |
|---|---|
vulpea-tags | Get tags for a note |
vulpea-tags-add | Add tags to a note |
vulpea-tags-remove | Remove tags from a note |
vulpea-tags-set | Set tags for a note (replace all) |
vulpea-tags-batch-add | Add a tag to multiple notes |
vulpea-tags-batch-remove | Remove a tag from multiple notes |
vulpea-tags-batch-rename | Rename a tag across all notes (interactive) |
vulpea-meta-batch-set | Set metadata property on notes |
vulpea-meta-batch-remove | Remove metadata property from notes |
| Function | Description |
|---|---|
vulpea-buffer-title-get | Get buffer title |
vulpea-buffer-title-set | Set buffer title |
vulpea-buffer-tags-get | Get buffer tags |
vulpea-buffer-tags-set | Set buffer tags |
vulpea-buffer-tags-add | Add tag interactively |
vulpea-buffer-tags-remove | Remove tag interactively |
vulpea-buffer-alias-get | Get aliases |
vulpea-buffer-alias-set | Set aliases (replace all) |
vulpea-buffer-alias-add | Add alias |
vulpea-buffer-alias-remove | Remove alias |
vulpea-buffer-meta-set | Set metadata value |
vulpea-buffer-meta-set-batch | Set multiple meta values |
vulpea-buffer-meta-sort | Sort metadata keys |
| Function | Description |
|---|---|
vulpea-db-autosync-mode | Toggle auto-sync |
vulpea-db-update-file | Immediate sync update |
vulpea-db-sync-update-file | Update file (async if enabled) |
vulpea-db-sync-update-directory | Update directory |
vulpea-db-sync-full-scan | Scan all directories |
- Plugin Guide - Custom extractors and tables
- Architecture - Internal design decisions