org-ql
Stability: stable (org-extensions-stability)
Modelled on the Emacs package org-ql: a query language for Org entries, search views and a dynamic block. Enable it with:
extensions = {
ql = {
files = "agenda", -- or "buffer", "all", a list of globs
default_predicate = "rifle", -- predicate of bare words
sort = nil, -- default sort of searches
views = { -- for :Org ql_view
["Work"] = { query = "todo: tags:work", sort = { "priority", "date" } },
},
views_file = vim.fn.stdpath("data") .. "/org/ql-views.json",
save_view_key = "<C-x><C-s>", -- in search buffers
include_hidden = false, -- true: also COMMENT/ARCHIVE subtrees
expand_repeaters = true, -- repeats count in date ranges
cache = true, -- remember results until files change
},
}
Queries
A query is a sexp, as in Emacs, or the plain ("non-sexp") syntax:
(and (todo "NEXT" "WAITING") (tags "work") (not (done)))
todo:NEXT,WAITING tags:work !done
In plain queries, space-separated terms are ANDed, ! negates a term, commas separate arguments (ORed), key=value is a keyword argument (ts:from=-7,to=today), "quoted text" keeps spaces and a bare word is (rifle word). A word: that is no predicate name (http://..., 10:30) is a plain word too. In a sexp query a bare string is (regexp STRING). From Lua a query can also be a table: { "and", { "todo", "NEXT" }, { "pred", function(hl) ... end } }.
Predicates (aliases in brackets):andornotcombine queriestodo[KW...] a not-done TODO; with keywords, any of themdonea done keywordtags[TAG...] any of the tags, inherited or localtags-all[tags&] all of the tagstags-local[ltags] own tags onlytags-inherited[itags] inherited tags onlytags-regexp[tags*] a tag matches a regexppriority[CMP] [P...](priority "A"),(priority '>= "B")deadlinescheduledplanning dates; a single number N is:to N,(deadline auto)uses the warning periodclosedclosed; a single number N means the last N days (:from -N)planningany of the three; a number is:totsts-active[ts-a] timestamps in the entry, of either kind,ts-inactive[ts-i] active or inactive; a number is:toclockedfinished clocks (a running clock is ignored); a number N is the last N dayspropertyKEY [VAL]:inherit tto inherit the propertyheading[h] every string is in the headingheading-regexp[h*] every regexp matches the headingregexp[r] every regexp matches the entryrifle[smart] every string is in the entry or its pathlevel[CMP] N [N](level 2),(level 2 4),(level '> 1)effort[CMP] D [D] likelevel, with durations ("1:30")category[CAT...]path[RE...] matches the file namehabitblockedhabits; TODOs that cannot be done yetoutline-path[olp] every string is in some path segmentoutline-path-segment[olps] strings match consecutive segmentsparentancestorschildrendescendants[QUERY] the relative exists (and matches QUERY)src:lang L :regexps (RE) a source blocklink[TEXT] :description :target :regexp-ppredFN (Lua)FN(headline)is true
Date arguments (:from, :to, :on) are a number of days from today, today, yesterday, tomorrow, now, or a date such as 2026-10-01 or 2026-10-01 14:00. :to without a time includes that whole day. :with-time t (or nil) keeps only timestamps with (without) a time. A repeating timestamp matches a range when one of its occurrences falls in it (expand_repeaters). Regexps are Emacs regexps and match case-insensitively. Results are cached per entry until its file changes (cache), except for queries with Lua functions or now.
Sorting
sort is one of date (the deadline, else the scheduled date), deadline, scheduled, closed, priority, todo (keyword order), random, reverse, a Lua comparator function(a, b) on headlines, or a list of those. As in org-ql, a list is applied in order, so the last one is the primary key: { "priority", "date" } sorts by date, then priority, and { "date", "reverse" } is newest first. Entries without the key go last.
Commands
:Org ql_search {query}searchfiles(prompts without a query):Org ql_search_buffer {query}search the current buffer:Org ql_view [name]open a view ofviews(prompts for one):Org ql_save_view [name]save the search of the agenda buffer as a view (alsosave_view_key, <C-x><C-s>, in search buffers):Org ql_find {query}jump to a matching entry of the buffer (org-ql-find);ql_find_agendasearches the agenda files:Org ql_refile {query}refile the subtree at the cursor under a matching entry (org-ql-refile):Org ql_sparse_tree {query}sparse tree of the matches in the buffer:Org ql_recent_items [N] [type]entries with timestamps oftype(ts, clocked, closed, ...) in the last N days, newest first (org-ql-view-recent-items)
All of them are also actions, for mappings. Results open in the agenda buffer, so the agenda keys work on them. Saved views are kept in views_file and listed with the configured ones (which win on a name clash). The same view can be an agenda custom command:
agenda = { custom_commands = {
q = { type = "org-ql", query = "(and (todo) (scheduled :to today))",
sort = "date", description = "Due" },
} }
Dynamic block
#+BEGIN: org-ql :query "todo: tags:work" :columns (heading todo (priority "P") deadline) :sort priority :take 10
#+END:
C-c C-c on the block writes a table of the buffer's matching entries. :columns takes heading (a link), todo, priority, deadline, scheduled, closed and (property "NAME"), each optionally with a header: ((property "OWNER") "Who"). :take N keeps the first N results (the last N when negative), :ts-format formats dates (default "%Y-%m-%d") and :files searches other files.
Lua API
local ql = require("org.extensions.ql")
ql.select("agenda", "(todo)", { sort = "priority" }) -- headlines
ql.search("tags:work", { files = "buffer", title = "Work" })
ql.query.compile("(tags \"x\")") -- headline predicate
Differences from Emacs org-ql: COMMENT and ARCHIVE subtrees are skipped, as in the agenda, unless include_hidden is set; repeaters are expanded in date ranges (org-ql reads the timestamp only; expand_repeaters = false does the same); a timestamp range matches when it overlaps the period, where org-ql tests each end; (pred ...) needs a Lua function, as Lisp forms in queries are not evaluated; (level 2 4) means levels 2 to 4; the plain syntax also accepts comparators (priority:>=,B) and numbers (closed:7); there is no sidebar of views and no link to a search.