Command line (org)
Stability: experimental (org-extensions-stability)
bin/org is a shell command that runs org.nvim in a headless Neovim (nvim --headless -l), to print the agenda, capture, clock in and out, query, change and export headings from a terminal, a status bar, cron, a launcher such as Raycast, a script or an AI agent (every command has JSON output, org-extensions-cli-json). Link it onto your $PATH:
ln -s /path/to/org.nvim/bin/org ~/.local/bin/org
or enable the extension and run :Org cli_install (or :Org cli_install DIR, directories complete), which asks before linking bin/org into install_dir:
require("org").setup({ extensions = { cli = { install_dir = "~/bin" } } })
The command itself works without the extension: enabling it only adds cli_install, the options below and a :checkhealth org section (is bin/org executable, is it the org on $PATH, which config it uses). $ORG_NVIM_BIN picks the Neovim binary (default nvim).
Configuration
nvim -lskips your init.lua, soorgloads its own configuration, the first of: 1.--config FILE2.$ORG_NVIM_CONFIG3.stdpath("config")/org-cli.lua(~/.config/nvim/org-cli.lua) With none of them the defaults are used. The file is Lua: it either returns the table you pass tosetup(), or callsrequire("org").setup()itself. Returning a table keeps it independent of your plugin manager:
-- ~/.config/nvim/org-cli.lua
return {
org_directory = "~/org",
agenda_files = { "~/org/*.org" },
capture = { templates = { t = { description = "Task",
template = "* TODO %?\n %U", target = "inbox.org" } } },
clock = { persist = true },
extensions = { cli = {}, ql = {} },
}
Your whole init.lua works as --config too, but then every call loads all your plugins. --files GLOB (repeatable, or comma-separated) replaces agenda_files, and --dir DIR sets org_directory.
Nothing waits for input: prompts, confirmations and dangling-clock resolution are turned off, and a command that would need to ask (a capture template with %^{...}, say) fails with exit code 3.
Commands
TARGET, the heading a command works on, is one of:id:IDor a bare ID the heading with that IDFILE:LINEthe entry containing that line of FILEFILE::TITLEa title in FILE (home.org::*Groceries, as in a link)FILE::#CUSTOM_IDthe heading with that CUSTOM_ID title words a heading of the agenda files whose title contains them (ignoring case; an exact title wins), or an org-ql query when theqlextension is enabled FILE is an agenda file (a path, a file name or a name without.org) or any org file. When several headings match, nothing is changed: the command lists them (in JSON,details.candidates) and--pick Ntakes the Nth.
Each argument is one word, except a last one of free text: TEXT, QUERY, DATE, a property VALUE, or a TARGET with nothing after it (show, id, clock in, archive) takes the remaining words. So quote a TARGET of several words that other arguments follow (set, note, refile): org set todo "Buy milk" DONE. A word no argument takes is a usage error (exit 2), never dropped.
Reading:org agenda [VIEW] [--date DATE] [--span N] [--csv]The agenda as text, likeorg-batch-agenda. VIEW isday,week,fortnight,month,year(the date agenda with that span),todo [KEYWORD],tags MATCH,tags-todo MATCHor the key of one of youragenda.custom_commands; without it, the date agenda with youragenda.span.--datetakes anything the date prompt reads (2026-10-01,+1,fri).--csvisorg-batch-agenda-csv.org search [QUERY...] [--match MATCH] [--limit N]Headings of the agenda files matching QUERY, asfile:line: heading: the agenda search syntax (org-agenda-search), or org-ql queries whenqlis enabled.--matchtakes a tags/property match (+work-boss,PRIORITY="A", org-match-syntax) instead.org headlines [FILE...] [filters](alsoorg query) Headings of the agenda files (or of the FILEs given) passing every filter:--todo KW(a keyword, orany,openfor not done,done,none; repeatable),--tag TAG(own or inherited; repeatable, all must match),--property NAME=VALUE(NAMEalone: has the property; inherited asuse_property_inheritancesays, like a property match),--level Nor--level 1..2,--scheduled FROM..TOand--deadline FROM..TO(dates as--datereads them, either side optional, a single date for that day, orany,none),--file FILE(one of the agenda files),--match MATCHand--limit N. Archived and commented subtrees are left out unless--archived.org show TARGET [--children]A heading with its properties, planning, clocks, plain timestamps and body text;--childrenadds the child entries.org clock [status] [--short] [--format FMT]The running clock: the one saved inclock.persist_file(withclock.persist), else the first open CLOCK line in the agenda files.--formatexpands %e (elapsed), %t (heading), %T (total with earlier clocks), %E (effort), %f (file) and %s (start), and prints nothing without a clock;--shortusesstatus_format.org templates,org files,org tags,org keywordsThe capture templates (alsoorg capture --list), the agenda files, the tags used in the agenda files or defined intags, and the TODO keyword sequences with the priority range.org export FILE BACKEND [-o OUTPUT|--stdout]Export FILE with a back-end of:Org export(html,md,latex,ascii, ...) and print the output path, or write the text to stdout. Code blocks are evaluated as in:Org export, but one that would ask first (babel.confirm_evaluate,:eval query) stops the export with exit code 3 and nothing written, as in Emacs's batch export;babel.confirm_evaluate = falsein the CLI's configuration runs them without asking (for files you trust).
Writing (each saves the files it changed):org capture [-t KEY] [--field NAME=VALUE] [--input FILE] [--id] TEXT...Capture TEXT with template KEY (default:capture_template, elset, else the first). The template finishes at once, the text taking the place of%?(or filling%iwhen the template has one). TEXT-reads stdin.--fieldanswers a prompt of the template by its name, the text before any|:%^{Title}and%^{Title}pbyTitle,%^{Size [cm]|10}bySize [cm],%^gbyTags, a date prompt by its name orDate(names ignore case).--input FILE(-for stdin) reads a JSON object{ "template", "text", "fields" }instead.--idgives the new entry an ID.org clock in [--pick N] TARGET,org clock out,org clock cancelClock in a heading, clock out of the running clock, or cancel it.org set todo TARGET STATE [--note TEXT]Set the TODO keyword (noneor "" removes it), with org.nvim's logging, blocking and repeaters. A note the change logs gets TEXT, else it is logged without one.org set tags TARGET [TAGS] [--add TAG] [--remove TAG]Set the tags (a:bora,b; "" removes them all), or add and remove some. A tag has letters and digits (of any script,café) and_ @ # %, asorg-tag-resays.org set priority TARGET PRIORITYA,B, ... (a number with numeric#+PRIORITIES);noneremoves.org set property TARGET NAME VALUE,org set property TARGET NAME --deleteorg set scheduled TARGET DATE [--note TEXT]org set deadline TARGET DATE [--note TEXT]DATE as the date prompt reads it (2026-10-05 14:00,+2d,fri) or a timestamp ("<2026-10-05 Mon +1w>");noneremoves the date. As when you schedule in Neovim (org-dates), the old date's repeater and warning period stay, CLOSED is removed, and the change is logged perlog_reschedule/log_redeadline, with the--notetext or without a note.org note TARGET TEXTAdd a note to the entry's log, likeadd_note(org-todo); TEXT-reads stdin.org refile TARGET DESTINATIONMove the subtree under DESTINATION, a TARGET, or to the end of an org file given by its path.org archive TARGETArchive the subtree witharchive_default_command.org id TARGET [--create]The heading's ID;--createmakes one when it has none.
org schema [COMMAND], org help [--json], org version
Every command writes its result to stdout and messages to stderr (-v adds org.nvim's own messages, -q hides warnings). Exit codes: 0 done, 1 failed or nothing found (search, clock out without a clock), 2 bad usage, 3 input needed, 4 the file has unsaved changes in a running Neovim.
A write refuses a file that a running Neovim has unsaved changes in (its swap file says so), and --force writes anyway. Every file a command would write is checked before anything changes, so a refused command leaves all of them as they were (clock in checks the file of the running clock it clocks out of too). Writes go through utils.save_buffer, so the write hooks run: with crypt.encrypt_on_save the entries tagged crypt are encrypted, as when Neovim saves them (org-crypt). The hooks of all the files run before any is written; one that needs input, such as the passphrase of symmetric encryption, fails the command with exit code 3 and writes nothing, so give crypt.key (or CRYPTKEY) a public key for writes from the shell. Every write, a capture too, touches the clock stamp file, so a Neovim with the extension enabled rereads the files (see below).
JSON --json makes every command print one JSON object, the envelope:
{ "version": 1, "ok": true, "command": "set todo",
"data": { ... }, "warnings": [], "errors": [] }
version is the version of the JSON output: it changes only when a key is removed or changes meaning (new keys can appear). command is the command's full name (clock status, set tags, ...), or null for an error found before the command is known (an unknown command or option, a flag without its value), data its result, warnings org.nvim's warnings during the command. An error makes ok false, data null and errors one object { code, message, details }, still on stdout, with the exit code of the error:
usage(2) bad or missing argumentsunknown_command(2) no such commandunknown_option(2) no such option, or not one of this commandbad_value(2) a date, keyword, priority, template, file, match or JSON that cannot be read or names nothingconfig(2) the configuration file is missing or failsnot_found(1) no heading matches the targetambiguous(1) several headings match:details.candidatesno_clock(1) no running clockfailed(1) org.nvim refused (details.messagessays why)internal(1) an unexpected errorinput_needed(3) the command would have to ask:details.promptnames the question (answer a capture prompt with--field; a confirmation or a passphrase can't be answered)file_busy(4) unsaved changes in a running Neovim:details.file,details.pid
In JSON mode stdout holds nothing but that object and stderr stays empty unless -v, so 2>&1 is safe. A command that finds nothing exits 0 with an empty list. --jsonl prints one result per line instead (the items of agenda, the headings of search and headlines, the entries of templates, files and tags; other commands print their data on one line), and an error as the envelope line.
Thedataof each command:agenda{ view, key, start, end, items }; an item hasdate,time,end_time,type(scheduled, deadline, timestamp, todo, tagsmatch, ...),todo,priority,title,tags,category,file,line,id,level,timestamp,extra,doneandtext(the agenda line)search,headlinesa list of headingsshowan entry: a heading plusproperties(an object),effort_minutes,clock(count,running,minutes,total,subtree_minutes,subtree_total,entriesof{ start, end, minutes, line }),timestamps,bodyandchildren(entries, or null without--children)clock ...{ active, title, file, line, id, start, start_iso, minutes, elapsed, total, effort };clock outandcancel:{ active, title, file, minutes, duration, canceled }capture{ file, line, template, type, id, headline }set ...,note{ headline, field, old, new }(andrepeatedforset todo,nameforset property)refile{ headline, from: { file, line } }archive{ title, from, archive_file, headline }id{ id, created, headline }templatesa list of{ key, description, type, target, group }filesa list of{ file, title, category, headlines }tagsa list of{ name, count, defined }keywords{ sequences, todo, done, priority }export{ file, backend, output, text }
A heading is { file, line, end_line, level, id, custom_id, todo, todo_type ("todo", "done" or null), priority, title, raw_title, tags (with inherited ones), local_tags, category, outline_path, scheduled, deadline, closed, archived, commented }. A timestamp is { raw, date, time, end_time, start, end, active, repeater, warning }, where raw is the org text (<2026-10-02 Fri 10:00-11:00 +1w>) and start and end are ISO 8601 in local time (2026-10-02T10:00, 2026-10-02 without a time). Paths are absolute; a missing value is null.
org schema prints all of this as JSON: every command with its usage, arguments, flags, an input_schema (a JSON Schema of its arguments and flags) and the JSON Schema of its data, the error and exit codes, and the shared types under $defs. org schema set todo describes one command; org help --json (or org COMMAND --help --json) is the same in an envelope.
Examples
The running clock in the tmux status line (refreshed every 15 seconds):
set -g status-interval 15
set -g status-right "#(org clock status --format '#[fg=green]%e %t')"
Today's agenda mailed every morning, and the week written to a file every hour, with cron:
0 7 * * * org agenda day | mail -s "Agenda" me@example.com
0 * * * * org agenda week > ~/public/agenda.txt
Next three timed items for a status bar (SketchyBar, waybar, ...):
org agenda --jsonl | jq -r 'select(.time) | "\(.time) \(.title)"' | head -3
More jq one-liners:
# overdue deadlines, oldest first
org headlines --todo open --deadline ..yesterday --json |
jq -r '.data | sort_by(.deadline.start)[] | "\(.deadline.date) \(.title)"'
# minutes clocked on a task
org show id:4f1c... --json | jq '.data.clock.subtree_minutes'
# mark the first NEXT of a project done, by its ID
id=$(org headlines --todo NEXT --file projects --limit 1 --json | jq -r '.data[0].id')
org set todo "id:$id" DONE
# what failed, and why
org set todo "Write report" DONE --json | jq -r '.errors[] | "\(.code): \(.message)"'
Capture from a launcher or a shell alias, or with the answers of a template's prompts:
org capture -t t "Call the dentist"
pbpaste | org capture -t n -
org capture -t m --field Who=Ann --field When=fri "Talk about the offsite"
echo '{"template":"t","text":"Renew passport"}' | org capture --input - --json
An agent tool
The CLI makes a small tool interface for an AI agent: the agent runs org ... --json, reads ok, data and errors, and retries an ambiguous target with --pick N or an ID. org schema gives the input_schema of every command, ready to pass as a tool definition, for example to the Anthropic API:
{
"name": "org_set_todo",
"description": "Set the TODO keyword of an org heading (org set todo).",
"input_schema": {
"type": "object",
"properties": {
"target": { "type": "string", "description": "id:ID, FILE:LINE, FILE::TITLE or title words" },
"state": { "type": "string", "description": "a TODO keyword, or none" },
"note": { "type": "string", "description": "--note: text of a log note" }
},
"required": ["target", "state"]
}
}
The tool's handler runs org set todo TARGET STATE [--note NOTE] --json (the arguments as separate words, not through a shell) and returns the envelope. To generate one tool per command:
org schema | jq '[.commands[] | select(.name != "help") | {
name: ("org_" + (.name | gsub(" "; "_"))),
description: .summary, input_schema: .input_schema }]'
A read-only agent gets the commands whose writes is false.
Options (extensions.cli)
install_dirwherecli_installlinksorg("~/.local/bin")capture_templatetemplate key oforg capturewithout-t(nil)status_formatformat oforg clock status --short("%e %t")watch_clocka running Neovim follows the shell's clock (true)
Clock in the shell and in Neovim
Every write (org clock in, out and cancel, capture, set, ...) touches stdpath("data")/org/cli-clock.stamp. A Neovim with the extension enabled (and watch_clock) checks that file every two seconds and on FocusGained; when it changed, Neovim rereads changed files (:checktime), drops its running clock when the open CLOCK line is gone, or takes up an open CLOCK line of the agenda files, and says so. org clock status sees a clock started in Neovim, from clock.persist_file or the saved CLOCK line.
Limits
- Each call starts Neovim and parses the agenda files: about 0.25 s for 200 files of 25 entries each (the agenda itself is most of it, so a parse cache would gain little), fine for a status bar every few seconds, not for a tight loop.
- The CLI edits files on disk. A Neovim that has the same file open sees the change as a file changed outside it ('autoread'), at once with the extension enabled (it watches the stamp file). The CLI does not write a file with unsaved changes in a running Neovim (exit 4), but finds those only through the swap file in the default 'directory'; with swap files off, or on Windows, a Neovim buffer with unsaved changes asks as usual.