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 ofquery, 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|NEXTtodo 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 followsuse_property_inheritancelevel 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) filterfunction(h)returning true to keep the handle api.headline_at([opts]) The headline containing a line:opts.bufnr(default current) andopts.lnum(default the cursor line), oropts.fileandopts.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 (givelnum, else nil and an error). api.find_by_id(id) The headline with this ID, found likeid: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; defaultagenda.span),align(default true: a week starts onagenda.start_on_weekday),include_empty(default true),log(clocked and closed entries, likel),files. api.agenda.todo([opts]) The TODO list;opts.keywordsa 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),kindthe agenda's finer type (past-scheduled, ...),daythe "YYYY-MM-DD" it's listed on,dateits timestamp,time/end_time"HH:MM",extrathe leader ("Scheduled: ", "In 3 d.: ") andheadlinea 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" },
})
Links
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 forapi.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 atopts.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 forapi.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 searchopts.bufnr(default current).headlineis set when the line is a headline. Not resolved:/regexp/searches, BibTeX keys and whatlinks.search_functionsor 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 firesUserautocmds with adatatable.api.on(name, fn, [opts])callsfn(data, ev)for one (opts.once,opts.group) and returns an id forapi.off(id);api.eventsmaps 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_triggersonce 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 (abortedwhen 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 whatorg.apidoesn'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 }.