org.nvim

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):
  and or not              combine queries
  todo [KW...]              a not-done TODO; with keywords, any of them
  done                      a done keyword
  tags [TAG...]             any of the tags, inherited or local
  tags-all [tags&]           all of the tags
  tags-local [ltags]         own tags only
  tags-inherited [itags]     inherited tags only
  tags-regexp [tags*]        a tag matches a regexp
  priority [CMP] [P...]      (priority "A"), (priority '>= "B")
  deadline scheduled        planning dates; a single number N is
                              :to N, (deadline auto) uses the warning
                              period
  closed                    closed; a single number N means the last N
                              days (:from -N)
  planning                  any of the three; a number is :to
  ts ts-active [ts-a]       timestamps in the entry, of either kind,
  ts-inactive [ts-i]          active or inactive; a number is :to
  clocked                   finished clocks (a running clock is
                              ignored); a number N is the last N days
  property KEY [VAL]         :inherit t to inherit the property
  heading [h]                every string is in the heading
  heading-regexp [h*]        every regexp matches the heading
  regexp [r]                 every regexp matches the entry
  rifle [smart]              every string is in the entry or its path
  level [CMP] N [N]          (level 2), (level 2 4), (level '> 1)
  effort [CMP] D [D]         like level, with durations ("1:30")
  category [CAT...]          path [RE...] matches the file name
  habit  blocked            habits; TODOs that cannot be done yet
  outline-path [olp]         every string is in some path segment
  outline-path-segment [olps] strings match consecutive segments
  parent ancestors children descendants [QUERY]
                              the relative exists (and matches QUERY)
  src :lang L :regexps (RE)  a source block
  link [TEXT] :description :target :regexp-p
  pred FN                   (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}         search files (prompts without a query)
:Org ql_search_buffer {query}  search the current buffer
:Org ql_view [name]            open a view of views (prompts for one)
:Org ql_save_view [name]       save the search of the agenda buffer as
                                a view (also save_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_agenda searches
                                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 of type (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.