Skip to content


Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time

DOCT: Declarative Org Capture Templates

doct is a function that provides an alternative, declarative syntax for describing Org capture templates.


(defun my-org-template-hook ()
  (when (string= (org-capture-get :key t) "p1")
    (message "\"First Child\" selected.")))

(add-hook 'org-capture-mode-hook 'my-org-template-hook)

(setq org-capture-templates
      '(("p" "Parent")
        ("p1" "First Child"  entry (file+headline "~/" "One")
         "* TODO %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?"
         :prepend t)
        ("p2" "Second Child" entry (file+headline "~/" "Two")
         "* NEXT %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?"
         :prepend t)
        ("p3" "Third Child"  entry (file+headline "~/" "Three")
         "* MAYBE %^{Description} \n:PROPERTIES:\n:Created: %U\n:END:\n%?"
         :prepend t)))


(setq org-capture-templates
      (doct '(("Parent" :keys "p"
               :file "~/"
               :prepend t
               :template ("* %{todo-state} %^{Description}"
                          ":Created: %U"
               :children (("First Child"  :keys "1"
                           :headline   "One"
                           :todo-state "TODO"
                           :hook (lambda () (message "\"First Child\" selected.")))
                          ("Second Child" :keys "2"
                           :headline   "Two"
                           :todo-state "NEXT")
                          ("Third Child"  :keys "3"
                           :headline   "Three"
                           :todo-state "MAYBE"))))))




  • drop support for recursive %{keyword} expansion.


  • drop support for %doct(KEYWORD) syntax. Replaced with %{keyword}
  • doct-get now accepts an optional parameter to determine whether to query org-capture-plist or org-capture-current-plist.
  • adopt semver




Try it with the straight package manager:

(straight-use-package 'doct)

or use-package:

(use-package doct
  :ensure t
  ;;recommended: defer until calling doct
  :commands (doct))


Download doct and make sure it is in your load-path. Require it in your init file.

(require 'doct)



(doct declarations)

doct expects a declaration or a list of declarations as its sole argument and converts it into a list of `org-capture-templates`. Each declaration is either a child, parent, or group.


A child declaration must have:

  • a name
  • a :keys string
  • a template type
  • a target
  • a template

and may also have:

  • hook functions defined with the hook keywords
  • contexts declared via the :contexts keyword
  • additional :KEY VAL arguments


A parent declaration must have:

  • a name
  • a :keys string
  • a list of :children

and may also have additional properties inherited by its children.


A group is a special kind of parent declaration. Its children inherit its properties, but it is not added to the template selection menu. Its name must be the :group keyword. It may optionally have a descriptive string for the value of :group. It must not have a :keys value.

(doct '(("Work" :keys "w" :file "~/org/" :children
         ((:group "Clocked" :clock-in t :children
                  (("Phone Call" :keys "p" :template "* Phone call with %?")
                   ("Meeting"    :keys "m" :template "* Meeting with %?")))
          ("Browsing" :keys "b" :template "* Browsing %x")))))


(("w" "Work")
 ("wp" "Phone Call" entry (file "~/org/") "* Phone call with %?" :clock-in t)
 ("wm" "Meeting"    entry (file "~/org/") "* Meeting with %?"    :clock-in t)
 ("wb" "Browsing"   entry (file "~/org/") "* Browsing %x"))

Name & Keys

Every declaration must define a name. Unless it is a group , it must also define a :keys value. The name is the first value in the declaration. The :keys keyword defines the keys to access the template from the capture menu.

(doct '(("example" :keys "e" ...)))


(("e" "example" ...))


The :children keyword defines a parent’s children. Its value may be a single declaration or a list of declarations. The parent’s :keys prefix each child’s :keys.

(doct '(("parent" :keys "p"
         (("child" :keys "c"
           (("grandchild" :keys "g"
             :file ""
             :type plain
             :template "test")))))))


(("p" "parent") ("pc" "child") ("pcg" "grandchild" plain (file "") "test"))

Inherited Properties

A child inherits its ancestors’ properties. It may optionally override an inherited property by specifying that property directly.

For example:

(doct '(("Grandparent" :keys "g"
         :file ""
         :children ("Parent" :keys "p"
                    :children ("Child" :keys "c")))))

The “Child” template inherits its :file property from the “Grandparent” declaration. The “Parent” declaration could override this value:

(doct '(("Grandparent" :keys "g"
         :file ""
         :children ("Parent" :keys "p"
                    :file ""
                    :children ("Child" :keys "c")))))

And the “Child” would have its :file property set to “”.


The :type keyword defines the template’s entry type and accepts the following symbols:

An Org node with a headline. The template becomes a child of the target entry or a top level entry.
A plain list item, placed in the first plain list at the target location.
A checkbox item. Same as plain list item only it uses a different default template.
A new line in the first table at target location.
Text inserted as is.

doct-default-entry-type defines the entry type when the :type keyword is not provided.

For example, with doct-default-entry-type set to entry (the default):

(doct '(("example"
         :keys "e"
         :type entry
         :file "")))


(doct '(("example"
         :keys "e"
         :file "")))

Both return:

(("e" "example" entry (file "") nil))


The target defines the location of the inserted template text.

The first keyword declared in the following group exclusively sets the target. The :file keyword is not necessary for these.

:id “id of existing Org entry”
File as child of this entry, or in the body of the entry (see org-id-get-create and the Org Mode Manual)
:clock t
File to the currently clocked entry
:function (lambda () ;visit file and move point to desired location…)
This keyword is exclusive when used without the :file keyword. It is responsible for finding the proper file and location to insert the capture item. If :file defines a target file, then the function is only responsible for moving point to the desired location within that file.
(doct '(("example"
         :keys "e"
         :type entry
         :clock t
         ;;ignored because clock is first
         :function (lambda () (ignore))
         ;;also ignored
         :id "1")))


(("e" "example" entry (clock) nil))

The :file keyword defines the target file for the capture template. It may be:

  • a string:
(doct ... :file "/path/to/")
;;empty string defaults to `org-default-notes-file'
(doct ... :file "")
  • a function:
(doct ... :file (lambda () (concat (read-string "Org Capture Path: ") ".org")))
;;or a function symbol
(doct ... :file my/get-file-path)
  • or a variable:
(doct ... :file my/file-path)

The following keywords refine the target file location:

:headline “node headline”
File under unique heading in target file.
:olp (“Level 1 heading” “Level 2 heading”…)

Define the full outline in the target file.

:datetree nil|t
Requires use of the :file keyword. If :datetree has a non-nil value, create a date tree for today’s date. If :olp is given, the date tree is added under that heading path. Use a non-nil :time-prompt property to prompt for a different date. Set the :tree-type property to the symbol week create a week tree instead of the default month tree.
:regexp “regexp describing location”

File to the entry matching regexp in target file

:function location-finding-function

If used in addition to the :file keyword, the value should be a function that finds the desired location in that file. If used as an exclusive keyword (see above), the function must locate both the target file and move point to the desired location.


The :template keyword defines the template for creating the capture item. It may be either a string, list of strings, or a function. doct joins the list with new lines. A function must return the template text.

(doct '((... :template ("Test" "One" "Two"))))


((... "Test\nOne\nTwo"))

The :template-file keyword defines a file containing the text of the template. For example:

(doct '((... :template-file "~/org/templates/template.txt")))

will use the text of template.txt as the template string.

The first keyword declared overrides any additional template declarations.

Additional options

Key-value pairs define additional options.

(doct '((... :immediate-finish t)))


((... :immediate-finish t))

see the Org Mode Manual for a full list of additional options.

Custom data

doct stores unrecognized keywords on the template’s org-capture-plist as members of the doct-custom plist. This makes a template’s metadata accessible during capture. See %{KEYWORD} Expansion for details on using that data.

The :custom keyword accepts a plist. doct copies the plist’s values to the doct-custom plist. This is only necessary if you wish to use a keyword which doct already uses.

For example:

(doct '(("Music Gear" :keys "m" :file ""
         :custom (:keys "Moog"))))


(#1="m" #2="Music Gear" entry (file #3="") nil
    :doct (#2# :keys #1# :file #3# :custom #4=(:keys "Moog") :doct-custom #4#))))

%{KEYWORD} Expansion

A declaration :template may include a keyword’s value during capture. The syntax is similar to other, built-in “%-escapes”. %{KEYWORD} will insert the value declared with :KEYWORD in the declaration.

For example, with:

(doct '(("Parent" :keys "p"
         :file ""
         :template "* %{todo-state} %?"
         :children (("One" :keys "1" :todo-state "TODO")
                    ("Two" :keys "2" :todo-state "IDEA")))))

Each child template has its :todo-state value expanded in the inherited :template.

Values should be strings, functions or nil.

(doct '(("%{string}" :keys "s" :type plain :file ""
         :string "string"
         :template "%{string}")))

Is replaced with:

(doct '(("%{fn}" :keys "f" :type plain :file ""
         :fn (lambda () "string returned from function")
         :template "%{fn}")))

Is replaced with:

"string returned from function"
(doct '(("%{nil}" :keys "f" :type plain :file ""
         :nil nil
         :template "%{nil}")))

Is replaced with the empty string


Custom keywords take precedence over other declaration keywords. For example, with:

(doct '(("Music Gear" :keys "m" :file "" :type plain
         :custom (:keys "Moog")
         :template "%{keys}")))

The “Music Gear” template expands to “Moog” instead of “m”. Nil values expand to an empty string.


Adding the following hook keywords in a declaration adds its value to the appropriate org-capture hook. The value may be a function or a variable.

:hook function

Runs FUNCTION when entering the org-capture-mode minor mode.

:prepare-finalize function

Runs FUNCTION before the finalization starts. The capture buffer is current and narrowed.

:before-finalize function

Runs FUNCTION right before a capture process finalizes. The capture buffer is still current and widened to the entire buffer.

:after-finalize function
Runs FUNCTION right after a capture process finalizes. Suitable for window cleanup.

For example:

(doct `(("example"
         :keys "e"
         :file ""
         :hook ,(defun my/fn  ()

runs my/fn during the org-capture-mode-hook when selecting the “example” template.


The :contexts keyword defines contextual rules for a template. Its value may be a single contextual rule or a list of rules. The following keywords are available to create contextual rules:

:in-buffer regexp
Show template when REGEXP matches the current buffer’s name.
(doct '(("Only in *scratch*" :keys "n" :file "" :contexts (:in-buffer "^\\*scratch\\*$"))))
:unless-buffer regexp
Show template unless REGEXP matches the current buffer’s name.
(doct '(("Except in *scratch*" :keys "n" :file "" :contexts (:unless-buffer "^\\*scratch\\*$"))))
:in-file regexp
Show template when REGEXP matches the current buffer’s file name.
(doct '(("Only in" :keys "n" :file "" :contexts (:in-file "work\\.org$"))))
:unless-file regexp
Show template unless REGEXP matches the current buffer’s file name.
(doct '(("Except in" :keys "n" :file "" :contexts (:unless-file "work\\.org$"))))
:in-mode regexp
Show template when REGEXP matches the current buffer’s major mode.
(doct '(("Only in org-mode" :keys "n" :file "" :contexts (:in-mode "org-mode"))))
:unless-mode regexp
Show template unless REGEXP matches the current buffer’s major mode.
(doct '(("Except in org-mode" :keys "n" :file "" :contexts (:unless-mode "org-mode"))))
:when condition
Show template when CONDITION evaluates to a non-nil value. Condition may be a function or a single form.
(doct '(("Show when my/predicate-p returns t" :keys "n" :file "" :contexts (:when my/predicate-p))))
(doct '(("1/3 chance of showing" :keys "n" :file "" :contexts (:when (= 2 (random 3))))))
:unless condition
Show template when CONDITION evaluates to a nil value. Condition may be a function or a single form.
(doct '(("Show when my/predicate-p returns nil" :keys "n" :file "" :contexts (:unless my/predicate-p))))
(doct '(("2/3 chance of showing" :keys "n" :file "" :contexts (:unless (= 2 (random 3))))))
:function function
Show template when FUNCTION returns non-nil. The function is not passed any arguments.
(doct '(("Between 9AM and 5PM" :keys "n" :file ""
         :contexts (:function (lambda () (<= 9 (string-to-number (format-time-string "%H")) 17)))))))

Adding :keys to a rule does the same as above, but remaps the template’s keys to the template with keys matching the :keys string. For example:

(doct '(("In *scratch* remapped to t, else use original template"
         :keys "n" :file "" :contexts ((:unless-buffer "^\\*scratch\\*$" :keys "n")
                                       (:in-buffer     "^\\*scratch\\*$" :keys "t")))))

The above rule keywords, spare :function, :when, and :unless may also take a list of strings for their values.

(doct '(("Only in org-mode or emacs-lisp-mode" :keys "n" :file ""
         :contexts (:in-mode ("org-mode" "emacs-lisp-mode")))))

Disabling Templates

Setting the :disabled keyword to t disables a template. The template’s declaration is not error checked. This can be useful if you don’t have the time to deal with an error right away. For example:
(doct '((:group "All" :file "" :children
                ((:group "Enabled" :children
                         (("One"   :keys "1")
                          ("Two"   :keys "2")
                          ("Three" :keys "3")))
                 (:group "Disabled" :disabled t :children
                         (("Four" :keys 4)
                          ("Five" :keys 5)
                          ("Six"  :keys 6)))))))


(("1" "One"   entry (file "") nil)
 ("2" "Two"   entry (file "") nil)
 ("3" "Three" entry (file "") nil))

Normally template “Four” would throw an error because its :keys are not a string.

Disabling Warnings

The :warn keyword disables doct’s warnings on a per-declaration basis. For example:

(let ((doct-warnings t))
  (doct '(("Ignore unbound symbol warnings" :keys "i"
           :warn (:not unbound)
           :file     unbound-variable
           :function unbound-function)
          ("Warn here, though" :keys "w"
           :file     unbound-variable
           :function unbound-function))))

For global control of warnings and an explanation of accepted values see doct-warnings in Custom Variables.


(doct-add-to list declarations &optional append)

Return a copy of LIST with converted DECLARATIONS added. If APPEND is non-nil, add to back of LIST. Otherwise, add to the front of LIST. DECLARATIONS are passed to doct and must be the same of the same form doct accepts.


;; `org-capture-templates' set earlier elsewhere...

(setq org-capture-templates
      (doct-add-to org-capture-templates
                   '("example" :keys "e" ...)

Custom Variables

doct supports the following variables for customization:
doct-default-entry-type ‘entry
The default template entry type. It can be overridden on a per-declaration basis by using the :type keyword.
Abnormal hook run after converting declarations to templates. Hook functions run with the list of templates as their only argument. The templates are not flattened at this point and are of the form:
(((parent) (child)...)...).
When non-nil, doct will issue warnings. Valid values are:
warn in all cases
do not warn

Or a list containing any of the following symbols:

warn when a symbol is unbound during conversion
warn when %{KEYWORD} is not found on the declaration during conversion.
warn when %{KEYWORD} expansion does not return a string.
warn when the expanded template string does not match the capture template’s entry type
warn when the :template-file’s file is not found during conversion
warn when additional options are not the proper type

If the list’s first element is the :not keyword, the list of warnings is disabled. It can be overridden on a per-declaration basis with the :warn keyword.”

For example:

(let ((doct-warnings t))
  (doct '(("Ignore unbound symbol warnings" :keys "i"
           :warn (:not unbound)
           :file     unbound-variable
           :function unbound-function)
          ("Warn here, though" :keys "w"
           :file     unbound-variable
           :function unbound-function))))


Pull/feature requests, code review, angry comments are all welcome.

Please add a test to the test suite if you introduce any changes.

Thanks, nv


DOCT: Declarative Org Capture Templates for Emacs








No releases published

Sponsor this project



No packages published