org.nvim

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 -l skips your init.lua, so org loads its own configuration, the
first of:
  1. --config FILE
  2. $ORG_NVIM_CONFIG
  3. 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 to setup(), or calls require("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:ID or a bare ID         the heading with that ID
  FILE:LINE                  the entry containing that line of FILE
  FILE::TITLE                a title in FILE (home.org::*Groceries, as
                               in a link)
  FILE::#CUSTOM_ID           the 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 the
                               ql extension 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 N takes
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, like org-batch-agenda. VIEW is day, week,
    fortnight, month, year (the date agenda with that span),
    todo [KEYWORD], tags MATCH, tags-todo MATCH or the key of one
    of your agenda.custom_commands; without it, the date agenda with
    your agenda.span. --date takes anything the date prompt reads
    (2026-10-01, +1, fri). --csv is org-batch-agenda-csv.
org search [QUERY...] [--match MATCH] [--limit N]
    Headings of the agenda files matching QUERY, as file:line: heading:
    the agenda search syntax (org-agenda-search), or org-ql queries
    when ql is enabled. --match takes a tags/property match
    (+work-boss, PRIORITY="A", org-match-syntax) instead.
org headlines [FILE...] [filters]                   (also org query)
    Headings of the agenda files (or of the FILEs given) passing every
    filter: --todo KW (a keyword, or any, open for not done,
    done, none; repeatable), --tag TAG (own or inherited;
    repeatable, all must match), --property NAME=VALUE (NAME alone:
    has the property; inherited as use_property_inheritance says, like
    a property match), --level N or --level 1..2, --scheduled
    FROM..TO and --deadline FROM..TO (dates as --date reads them,
    either side optional, a single date for that day, or any, none),
    --file FILE (one of the agenda files), --match MATCH and
    --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; --children adds the child entries.
org clock [status] [--short] [--format FMT]
    The running clock: the one saved in clock.persist_file (with
    clock.persist), else the first open CLOCK line in the agenda files.
    --format expands %e (elapsed), %t (heading), %T (total with earlier
    clocks), %E (effort), %f (file) and %s (start), and prints nothing
    without a clock; --short uses status_format.
org templates, org files, org tags, org keywords
    The capture templates (also org capture --list), the agenda files,
    the tags used in the agenda files or defined in tags, 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,
    pdf, 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 = false in 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, else t,
    else the first). The template finishes at once, the text taking the
    place of %? (or filling %i when the template has one). TEXT -
    reads stdin. --field answers a prompt of the template by its name,
    the text before any |: %^{Title} and %^{Title}p by Title,
    %^{Size [cm]|10} by Size [cm], %^g by Tags, a date prompt by
    its name or Date (names ignore case). --input FILE
    (- for stdin) reads a JSON object { "template", "text", "fields" }
    instead. --id gives the new entry an ID.
org clock in [--pick N] TARGET, org clock out, org clock cancel
    Clock in a heading, clock out of the running clock, or cancel it.
org set todo TARGET STATE [--note TEXT]
    Set the TODO keyword (none or "" 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:b or a,b; "" removes them all), or add and remove
    some. A tag has letters and digits (of any script, café) and
    _ @ # %, as org-tag-re says.
org set priority TARGET PRIORITY
    A, B, ... (a number with numeric #+PRIORITIES); none removes.
org set property TARGET NAME VALUE, org set property TARGET NAME --delete
org 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>"); none removes 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 per
    log_reschedule / log_redeadline, with the --note text or without
    a note.
org note TARGET TEXT
    Add a note to the entry's log, like add_note (org-todo); TEXT -
    reads stdin.
org refile TARGET DESTINATION
    Move the subtree under DESTINATION, a TARGET, or to the end of an org
    file given by its path.
org archive TARGET
    Archive the subtree with archive_default_command.
org id TARGET [--create]
    The heading's ID; --create makes 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 arguments
  unknown_command (2)  no such command
  unknown_option (2)   no such option, or not one of this command
  bad_value (2)        a date, keyword, priority, template, file, match
                         or JSON that cannot be read or names nothing
  config (2)           the configuration file is missing or fails
  not_found (1)        no heading matches the target
  ambiguous (1)        several headings match: details.candidates
  no_clock (1)         no running clock
  failed (1)           org.nvim refused (details.messages says why)
  internal (1)         an unexpected error
  input_needed (3)     the command would have to ask: details.prompt
                         names 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.

The data of each command:
  agenda        { view, key, start, end, items }; an item has date,
                  time, end_time, type (scheduled, deadline,
                  timestamp, todo, tagsmatch, ...), todo, priority,
                  title, tags, category, file, line, id,
                  level, timestamp, extra, done and text (the
                  agenda line)
  search, headlines
                  a list of headings
  show          an entry: a heading plus properties (an object),
                  effort_minutes, clock (count, running,
                  minutes, total, subtree_minutes, subtree_total,
                  entries of { start, end, minutes, line }),
                  timestamps, body and children (entries, or null
                  without --children)
  clock ...     { active, title, file, line, id, start, start_iso,
                  minutes, elapsed, total, effort }; clock out and
                  cancel: { active, title, file, minutes, duration,
                  canceled }
  capture       { file, line, template, type, id, headline }
  set ..., note
                  { headline, field, old, new } (and repeated for
                  set todo, name for set property)
  refile        { headline, from: { file, line } }
  archive       { title, from, archive_file, headline }
  id            { id, created, headline }
  templates     a list of { key, description, type, target, group }
  files         a list of { file, title, category, headlines }
  tags          a 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_dir        where cli_install links org ("~/.local/bin")
  capture_template   template key of org capture without -t (nil)
  status_format      format of org clock status --short ("%e %t")
  watch_clock        a 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.