Org-capture template expansion properties & prompt references

Hello,

When creating various org-capture template files (to be used as (file /path/to/template-file) in org-roam-capture-templates), the need to include properties came up, along with the need to refer to the previous prompts in the template.

In ordinary org-caputre-templates, these issues do not exist (insert the property drawer inside the file itself, and use %\N to refer to the Nth prompt in the capture template).

In org-roam-capture-templates however, as described over here, always creates the ID (and inserts it into the property drawer – creating it if needed) before expanding and inserting the capture template file – resulting in 2 property drawers overall.

There is the option to parse the template file to include its property drawer in the header (file+head :target option) – which seems to be inserted before the property drawer is inserted – causing the ID property to be placed in the correct drawer. But then the other issue occurs – from my understanding of the code, the insertion and expansion of the header is a different one from the insertion and expansion of the template file itself – as a result, they do not share prompt references (and I don’t know if there are other effects).

Another option is to delete the ID property and add it after the capture process is complete. However, I do not really like this as it seems risky, I can’t ensure other things don’t rely on it – whether it is org-roam or any 3rd party library I might want to use in the future.

Is there another way to solve both these issues (properties and prompt references in capture template files)?

Not sure if I understand what you are trying to achieve but why not use the ordinary org-capture instead of the org-roam one?

Not sure if I understand what you are trying to achieve

Being able to use prompt references and properties in the org-capture template. If you use a normal template file, it would be inserted in a way that creates 2 property drawers (because org-roam-capture) generates one for the ID in a previous step of the capture.

Separating the property drawer and the rest of the template would result in the reference prompts (if I have a prompt %^{my prompt} I can use %\1 later in the template, it would expand to the first prompt in the template, instead of prompting again). This is caused by the fact the expansion of the head (the head part of file+head) and the expansion of the template file itself are performed as separate steps.

why not use the ordinary org-capture instead of the org-roam

If I am not mistaken, I lose a lot of built-in functionality (such as org-roam-node-* functions) – which I might have to implement again by myself. The bigger issue is that I cannot control the usage of these functions by 3rd party packages.

Moreover, the additional ${} syntax is very handy, at least for my use cases.

It is mainly the first point, though.

Your issue seems to be coming from your advanced understanding and usage of org-capture-templates and is well beyond me. I tried to understand what you wrote a few hours ago (the morning in my time zone) and tried it just now after your update just now. I still don’t know what the issue is. Now here is how I read it and why I mentioned org-capture, instead of org-roam-capture – you can ignore my rambling, really. Hopefully someone more knowledgeable can jump in and help you.

  1. org-roam-capture-templates do not work in the way you want.

  2. org-capture-templates do what you want, but they do not have a useful ${} (but it is not essential for your use case)

  3. The reason why 2. is not a solution is because
    a. You will lose “a lot of built-in functionality (such as org-roam-node-* functions)”
    b. You cannot control the usage of these functions by 3rd party packages

I am saying don’t worry about 3. altogether.

For point a. org-roam-node-* functions work if you have an ID (Org-ID) no matter how you add them. I even use denote package to create files with IDs together with Org-roam. The only requirement for a file and a headline to be an Org-roam node is an ID (Org-ID).

For point b. I am not sure if this is a material issue at the moment. If it becomes a material issue, then you can think of a solution then. I would choose to move on with using org-capture-templates as you seem to know that it works for your use case. b. is non-issue at the moment; merely a future possibility.

Perhaps a small example would help?
Let’s say I want to include the property FOO in my template.

("d" "default" plain 
":PROPERTIES:
:FOO: bar
:END:
%?" 
:target (file "%<%Y%m%d%H%M%S>-${slug}.org")
:unnarrowed t)

The resulting capture buffer is:

:PROPERTIES:
:ID:       0893b791-ab10-4f06-b84b-7b077fd84a4a
:END:
:PROPERTIES:
:FOO: bar
:END:
<cursor position>

Which is not intended.

Moving the property drawer to the head, helps:

("d" "default" plain 
"%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" 
":PROPERTIES:
:FOO: bar
:END:
")
  :unnarrowed t)

The resulting capture buffer is:

:PROPERTIES:
:FOO: bar
:ID:       33538392-1be8-4d0c-89ba-7579500c1329
:END:
<cursor position>

But then I cannot use prompt references, for example:

("d" "default" plain 
"This is a note about %\\1
%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" 
":PROPERTIES:
:FOO: %^{foo prompt}
:END:
")
  :unnarrowed t)

This results in an error (org-capture-fill-template: Wrong type argument: stringp, nil) – as it cannot find the 1st prompt in the template to refer to. This is because the template and the head are expanded at different times. For clarification, this works as expected:

("d" "default" plain 
"%?"
:target (file+head "%<%Y%m%d%H%M%S>-${slug}.org" 
":PROPERTIES:
:FOO: %^{foo prompt} %\\1
:END:
")
  :unnarrowed t)

At the prompt (which happens once), I can enter bar and the result would be:

:PROPERTIES:
:AUTHOR: bar bar
:ID:       e773b30b-028e-42fa-a539-6bf2cf33c9ef
:END:
<cursor position>

To conclude, my issue is how to solve these problems at the same time.

Sadly, it is:

Mainly for org-roam-node properties such as ${title} – if there is a nice alternative, I am open to suggestions.

My fault for not being clear enough – but I meant for functions like org-roam-node-find, which I know would work if I have an ID like you have said – but sometimes do rely on org-roam-capture-. For example, entering a nonexistent node at the prompt of org-roam-node-find invokes org-roam-capture- – which is a behavior I would like to have (falling back to a capture, just not necessarily org-roam-capture-).
This specific functionality is easy to reimplement to work with org-capture – but I am not so sure about the other org-roam-node-* functions.

Fair enough, that is a good point.

To be honest, I would have thought the following worked, but I trust that you have confirmed it does not.

I’m away for the weekend without my computer so I cannot test the idea below; I’d create a custom function and add it to org-capture-after-finalize-hook. It’s a simple variation of the solution discussed in the thread you linked above.

The custom function should be something like this below. I am writing it on iPhone so am not checking the function name, parenthesis, or syntax. I hope something like this works

(let ((var (read-string “Enter author: “)))
  (org-add-property “AUTHOR” var)
;; I assume the point is already in the correct location
;; if not, move point, probably (plont-max) will work
;; if the capture buffer is narrow
  (insert var)))

I don’t fully understand how this approach would solve the second problem (referencing the prompt in the template):

But I guess I need to compromise and enter the same input for multiple prompts (discard the usage of prompt references altogether).

I thought there might be a solution which is less risky than what you have suggested here.

I think finding the correct point would be rather difficult and unintuitive in the general case (as the references can occur at arbitrary places in the template).

Maybe. I don’t have the same level of understanding of the “risk” as yours, so I can’t really comment.

This example does not. I would put the whole node-capture/node-find function in a let form (and create a custom command). Then, I believe, you can evaluate the let-bound-variable in the template and place it anywhere in the template (?) This way you only would need a prompt once.

In this case, I cannot be sure that someday the code of org-roam-capture.el or its dependencies would depend on the ID property to be present at capture time, just as it is now.

I can advise functions to return the ID of the node instead (like you have done), but I cannot prevent all possible methods of retrieving this property (and advise them accordingly), because there are a lot of ways.

My (irrational?) fear of advices stems from the fact it’s always another place to look when a bug is found – as I change internal logic. That is why I try to the best of my ability to not use advices or private (double-dashed --) symbols.

I would like to hear your take about it, though – as I’ve seen a lot of people and package authors in particular advising functions. How can you ensure it won’t be a cat-and-mouse game?

Interesting. I need to think a little about how to implement it – as I don’t think it’s very intuitive (the capture template depend on the let-bound-variables which are created based on the template itself – a somewhat circular behavior).

Also, it would change the capture template to have implementation details (the calls for the evaluation of the let-bound-variables instead of the “normal” way of referencing prompts) – not a dealbreaker by any means, but still worth noting.

I see there are a couple of different topics; let me unpick them.

Custom function with let-bound-variables for org-roam-capture

I think the implementation below avoids the “circular behavior” and prompts for a property. It works on my end.

I tried to implement something different with a let-bound-variable, I failed. I needed to define the variable anyway with defvar, so perhaps my original idea was not possible syntactically.

(add-to-list 'org-roam-capture-templates
             '("s" "special" plain ""
               :target
               (file+head "./${id}.org" "
#+title: ${title}
#+author: %(read-string \"Author: \")
#+id: ${id}

")))

The code org-roam-capture.el may change someday

In general, I would agree. For the case of org-roam-capture, however, I personally do not worry about this because org-roam is in maintenance since the end of 2021. See this blog article from Jethro, Org-roam’s author. The API and internals have been stable since then.

org-roam-capture also relies on org-capture, which is more actively developed but with backward compatibility carefully considered.

Use of advice in general and in md-roam

I don’t think it’s irrational. I would avoid advising for the same reason you mention: “as I change internal logic”.

In the particular case of md-roam, the concern for this risk might be exaggerated. If we follow this definition:

(R)isk = (P)robability × (M)agnitude

I consider P and M are very small, and thus R negligible (for me).

For P, Md-roam is advising functions from stable Org-roam, as noted in the previous point above, the probability of a major change is low.

For M, md-roam is my “personal config” shared via GitHub. It’s not distributed more widely; not available in MELPA and ELPA. Should advised functions change, I should not have to worry about much of the impact. In the end, I can just create notes in markdown without using md-roam until I have some time to fix the problem (which should be very infrequent anyway).

Please disregard this. I keep forgetting about your “template” requirement.

This below works on my end.

I am not really concerned with the “circular behavior” or that the “capture template to have implementation details (the calls for the evaluation of the let-bound-variables instead of the “normal” way of referencing prompts)”.

An inner scope can work with variables in the outer scope. In this case, it’s only me using this let-binding, and can guarantee that it works for me and I don’t need to worry about usage by others.

(defvar var "")

(setq org-roam-capture-templates
             '(("s" "special" plain ""
               :target
               (file+head "./${id}.org" "
#+title: ${title}
#+author: %(identity var)
#+id: ${id}

This is a template %(identity var)

"))))


(let ((var (read-string "Author: ")))
  (org-roam-capture nil "s"))