org.nvim

Lua API

require("org.api") is the public Lua API for plugins and configs: files, headlines and their changes, agenda queries, capture, links, the clock and events. Everything it returns is plain data (tables of strings, numbers and booleans) or a headline handle, a plain table with methods.

api.version is the version of the API, MAJOR.MINOR.PATCH, separate from the plugin's release. Within a major version functions, fields and event payloads are only added (a new MINOR), never removed or changed. api.has("1.2") is true when this API provides version 1.2: the same major version, not older. Other modules (org.parser, org.files, org.agenda, ...) are internal: they can change in any release.

local ok, api = pcall(require, "org.api")
if ok and api.has("1.0") then ... end

Functions that can fail return nil and an error message. They never prompt: a prompt the change would show (a log note, a date, a choice) answers as if cancelled, so notes are left out and only the time is logged unless you pass note. Org's messages are not shown either.

Files

api.agenda_files()          Absolute paths of the agenda files.
api.load([paths])           Read files: one path returns one file (nil and
                            an error when it can't be read), a list of
                            paths, globs or directories returns a list, and
                            no argument the agenda files. A file loaded in
                            a buffer is read from the buffer, unsaved
                            changes included.
api.current([bufnr])        The file of a buffer (default current), nil
                            when it isn't an org buffer.

A file is { file, bufnr, title, category, filetags, properties, todo_keywords = { todo, done }, headlines }: file is the path (nil for a buffer without one), properties the file-level property drawer and headlines every headline in document order.

Headlines

api.headlines([query])      Headlines matching every condition of query,
                            in file order (nil and an error for an invalid
                            match string):
    files     files, globs or directories (default the agenda files)
    match     a tags/property match as in agenda tag searches
              (org-match-syntax): +work-urgent, PRIORITY="A",
              +project/TODO|NEXT
    todo      a keyword or a list of them; true: any not-done keyword;
              false: no keyword
    done      in a done state (true) or not (false)
    tags      a tag or a list, all required, inherited ones included
    property  { NAME = "value" | true | false | function(value) }:
              true is set, false unset; inheritance follows
              use_property_inheritance
    level     a number, or { min = 2, max = 3 }
    title     text the title contains, ignoring case (été finds ÉTÉ)
    id        the ID property
    archived  false skips subtrees tagged ARCHIVE (default: included)
    filter    function(h) returning true to keep the handle
api.headline_at([opts])     The headline containing a line: opts.bufnr
                            (default current) and opts.lnum (default the
                            cursor line), or opts.file and opts.lnum.
                            A buffer's cursor is the current window's when
                            it shows the buffer, else that of the first
                            window showing it; a buffer no window shows
                            has none (give lnum, else nil and an error).
api.find_by_id(id)          The headline with this ID, found like id:
                            links find it.
api.date(value)             A date in the form below, from any value the
                            API accepts as a date.
A headline handle has these fields, a snapshot taken when it was made:
    title         text without TODO keyword, priority and tags
    plain_title   title with links shown as their description and
                  statistics cookies removed
    raw           the whole headline line
    level, todo, priority (nil without a cookie), category, id
    todo_type     "todo", "done" or nil;  done  a boolean
    tags          own tags;  all_tags  with inherited and file tags
    properties    own properties, names upper-cased
    scheduled, deadline, closed   dates (nil when not set)
    file          the path (nil for a buffer without one)
    line, end_line   the headline line and the last line of its subtree
    outline_path  titles of the ancestors
    archived, commented   booleans

A date is { year, month, day, hour, min, end_hour, end_min, active, repeater, warning, date = "2026-10-02", time = "09:30", timestamp (Unix time), text (as org writes it), raw (as written in the file) }; for a range <2026-10-02 Fri>--<2026-10-04 Sun> the fields are its start, and text and raw the whole range. Where the API takes a date it accepts such a table (or any table with year, month, day and optionally hour and min), a Unix time, or a string: a timestamp (<2026-10-02 Fri 09:30>, 2026-10-02) or what the date prompt reads (+2d, fri). Fields out of range roll over like os.time(): day 32 of October is November 1, hour 25 is 1:00 the next day, and so is a timestamp's day its month doesn't have (2026-02-30 is March 2). A field that isn't an integer, a year outside 1-9999, an end time outside the day or a repeater or warning that isn't { type, value, unit } is an error.

Methods find the headline again, run the same code as the keys, refresh
the fields and return the handle (or nil and an error). A handle with an
ID finds the entry with that ID wherever it went; when no entry has it, or
several do, that's an error, never another entry. While the file is loaded
in a buffer (a change loads it), an extmark follows the headline's line
through every edit, yours and the API's, so all the handles of a query
stay on their entries while you change them one by one (a line replaced
by another change, for its keyword, tags or a statistics cookie, stays
the handle's while no other headline has its title). A file changed
outside Neovim leaves only the text: the one headline with the handle's
text. When the headline can't be told apart (its line was deleted, its
subtree moved, several headlines have its text), a method returns nil and
"the headline moved; get a new handle". After h:archive() the handle no
longer points to an entry.
    h:set_todo(state, [opts])     C-c C-t; nil removes the keyword.
                                  opts.note is the note the logging asks
                                  for, opts.force ignores blocking
    h:set_tags(tags)              a list or ":a:b:". A tag is letters and
                                  digits (of any script), _, @, #
                                  and % (org-tag-re); another name
                                  (follow-up) is an error, as org
                                  wouldn't read it back as a tag
    h:add_tag(tag), h:remove_tag(tag)  add or remove one of the tags the
                                  headline has now (also those added
                                  after the handle was made), like
                                  org-toggle-tag
    h:set_priority(p)             "A", "10" or a number; nil removes it.
                                  An error (also for nil) when
                                  priority_enable_commands is false
    h:set_property(name, value)   org-entry-put; nil deletes the property
    h:get_property(name, [inherit])  special properties too (ALLTAGS)
    h:schedule(date)              C-c C-s, nil removes the date; also
                                  h:set_scheduled(date). The old repeater
                                  stays unless the date has its own
    h:set_deadline(date)          C-c C-d (the deadline field holds the
                                  date, so the method has another name)
    h:clock_in(), h:clock_out()   clock_out returns the minutes
    h:is_clocked_in()
    h:refile(dest, [opts])        dest: another handle, a file path, or
                                  { file = path, headline = "Title" }
                                  (or an outline path list). opts.copy
                                  keeps the original, opts.prepend makes
                                  it the first child. The handle then
                                  points to the moved entry
    h:archive()                   archive per archive_location; returns
                                  the archive file
    h:open([opts])                show it in a window, cursor on it;
                                  opts.split "split", "vsplit" or "tab".
                                  goto is a LuaJIT keyword: h:goto()
                                  can't be written, h["goto"](h) works
    h:add_id()                    the ID, created when missing (alias
                                  id_get_or_create); h:get_id() only
                                  reads it
    h:parent(), h:children()      handles of the parent and children
    h:reload()                    read the headline again

Changes work whether or not the file is loaded: it's loaded in a hidden buffer when needed and written with org's own save (write hooks such as encryption run, org-crypt). A change saves every file it touched, all of them or none: the headline's file and each other file it changed (a refile's destination, the archive file whatever archive_subtree_save_file says, the file whose clock h:clock_in() stopped), the headline's file last. By default none of them is saved when one had unsaved changes before the change: your own unsaved edits are never written behind your back, and an entry that moved to another file is never written out of its file while it's only in a buffer there. Every change method takes opts.save (true saves them all, false none). Like org-refile and org-archive, a refile still saves a destination file that isn't shown in any window, and archiving the archive file when archive_subtree_save_file says so, before the entry leaves its file.

local api = require("org.api")
for _, h in ipairs(api.headlines({ match = "+work", todo = "WAITING" })) do
  h:set_todo("TODO")
  h:schedule("+1d")
end

Agenda

The entries of the agenda views as data, without opening the agenda.
api.agenda.agenda([opts])   Days of an agenda span: { { date =
                            "2026-10-05", day = <date>, items = {...} } }.
                            opts.from (a date, default today), span
                            ("day", "week", "fortnight", "month", "year"
                            or a number of days; default agenda.span),
                            align (default true: a week starts on
                            agenda.start_on_weekday), include_empty
                            (default true), log (clocked and closed
                            entries, like l), files.
api.agenda.todo([opts])     The TODO list; opts.keywords a keyword or a
                            list ("*": any keyword), opts.files.
api.agenda.tags(match, [opts])  A tags/property match; opts.todo_only.
api.agenda.search(query, [opts])  A search view query (org-agenda-search);
                            opts.todo_only (like !, also with a
                            leading *).
Items are sorted like the views (agenda.sorting) and are `{ type, kind,
title, todo, priority, category, tags, done, day, date, time, end_time,
extra, file, line, headline }: type` is where the entry comes from
(scheduled, deadline, timestamp, range, sexp, closed, clock, todo, tags,
search), kind the agenda's finer type (past-scheduled, ...), day the
"YYYY-MM-DD" it's listed on, date its timestamp, time / end_time
"HH:MM", extra the leader ("Scheduled: ", "In   3 d.: ") and headline
a handle.
for _, day in ipairs(require("org.api").agenda.agenda({ span = "day" })) do
  for _, item in ipairs(day.items) do
    print(item.time or "", item.todo or "", item.title)
  end
end

Capture

api.capture(opts)           Capture without a capture window: the template
                            is filled in, stored and saved at once.
    key       a template key of capture.templates, or
    template  template text (with target, headline, olp, type,
              datetree as in org-capture-templates) or a whole
              template table
    values    answers to the %^ prompts, by label ({ Title = "x" }
              for %^{Title}, "Tags" for %^g, "Date" for %^t) or
              by position ({ "x", "y" }); others take their default.
              A date prompt takes a date in any form the API takes
              (org-api-headlines); a Unix time keeps its time of day
    initial   the text of %i
    date      the capture date (%t, date trees, time_prompt)
Returns { file, bufnr, line, headline } (headline for entry
templates; bufnr is nil when the template's kill_buffer closed the
buffer). OrgCaptureAfterFinalize fires as for any capture.
require("org.api").capture({
  template = "* TODO %^{Task}\n  %U",
  target = "~/org/inbox.org",
  values = { Task = "Call the bank" },
})
api.links.store(link, [desc])   Add to the stored links (<prefix>li).
api.links.store_location([where], [opts])  Store a link to a handle or
                            { bufnr, lnum } (default the cursor, as for
                            api.headline_at), like <prefix>ls. An ID it
                            creates (links.use_id) is saved like other
                            changes (org-api-saving, opts.save).
api.links.stored()          The stored links { link, desc }, newest first.
api.links.format(link, [desc])  [[link][desc]], escaped.
api.links.insert(link, [desc], [opts])  Insert at the cursor, or at
                            opts.bufnr, opts.row (1-based), opts.col
                            (0-based byte), written for that buffer like
                            <prefix>li. What isn't given comes from the
                            buffer's cursor (as for api.headline_at: nil
                            and an error when no window shows the buffer).
                            Returns the inserted text.
api.links.resolve(link, [opts])  What a link (a target or [[...]]) points
                            to, without following it: { type, path,
                            target, search, file, line, headline, url }.
                            It finds what following the link finds (the
                            searches of org-links, ignoring case and
                            blanks: *heading, #custom-id, (coderef),
                            a line number, <<target>>, #+NAME:, a
                            headline title, text). Internal links search
                            opts.bufnr (default current). headline is
                            set when the line is a headline. Not resolved:
                            /regexp/ searches, BibTeX keys and what
                            links.search_functions or org-ctags answer.

Clock

api.clock.status()          The running clock, or nil: `{ title, file,
                            start (a date), minutes, clocked, effort,
                            overrun, headline }` (see org-clock).
api.clock.is_running()
api.clock.clock_out([opts]) Stop it wherever it runs; returns the minutes.
api.clock.cancel([opts])    Cancel it (<prefix>xq): its CLOCK line is
                            removed.
Both save the file of the CLOCK line (org-api-saving, opts.save).

Events

Org fires User autocmds with a data table. api.on(name, fn, [opts])
calls fn(data, ev) for one (opts.once, opts.group) and returns an id
for api.off(id); api.events maps each name to a short description.
    OrgTodoStateChange   bufnr, lnum, from, to, state, done, repeated
                         (org-todo-events)
    OrgTodoRepeat        same data, a repeating task moved on
    OrgPriorityChanged   bufnr, lnum, file, from, to (nil: no cookie)
    OrgTagsChanged       bufnr, lnum, file, from, to: the own tags of a
                         headline changed, whatever changed them (C-c C-q,
                         the region command, todo_state_tags_triggers
                         once per tag, the ARCHIVE, ORDERED and ATTACH
                         tags, the agenda, the column views, the API, ...;
                         org-after-tags-change-hook)
    OrgPropertyChanged   bufnr, lnum, name, value
    OrgClockIn           bufnr, lnum, title
    OrgClockOut          path, title, minutes, removed
    OrgClockCancel       path, title
    OrgCaptureAfterFinalize  bufnr, line (aborted when cancelled)
    OrgRefile            bufnr, lnum, file (where the entry now is), title,
                         source_bufnr, source_file, copy
                         (org-after-refile-insert-hook). As in Emacs, it
                         fires after the last-refile bookmark is set; a
                         destination not shown in a window is saved after
                         it too, with the handler's edits. Unlike in
                         Emacs, the entry has already left its old place.
    OrgArchive           bufnr, lnum, title, archive_file (before the
                         subtree leaves its file); OrgArchiveFinalize: the
                         same in the archive buffer
    OrgFileLoaded        file, bufnr, source ("disk" or "buffer"): a file
                         was read from disk, or a buffer parsed for the
                         first time. Fires on the next event loop turn.
Handlers run during the command, like Emacs hooks, and may edit buffers:
the command follows its entries where a handler moves them (the region
command its headlines; a refile the line it returns, its bookmark and the
API handle).
Other features fire their own events (clock, capture, agenda, babel, ...;
see their sections).
local api = require("org.api")
api.on("OrgTodoStateChange", function(data)
  if data.done then
    local h = api.headline_at({ bufnr = data.bufnr, lnum = data.lnum })
    vim.notify("Done: " .. h.title)
  end
end)

Other Lua entry points

require("org").setup({opts})        configure (see org-config)
require("org").agenda([key])        open the dispatcher or a view
require("org").capture([key])       capture interactively
require("org").statusline()         clock and timer string for statuslines
require("org").action(name)         run an action (org-keymaps)
The modules below the API are internal and may change between releases;
use them only for what org.api doesn't cover yet:
  org.parser    parse(lines, filename) → file with headlines, planning,
                properties, clocks and timestamps
  org.files     get(path), get_buffer(bufnr), agenda_files()
  org.date      parse(), read_date(), Date:add(), Date:to_string()
  org.todo      change_state(target, keyword)
  org.clock     clock_in(target), clock_out(), active()
  org.agenda.search  compile(match) → function(headline) → boolean
  org.export    export(format, opts) → output path; to_string(format)
  org.dblock    register(name, fn)
Their targets are { bufnr = n, lnum = l }.