What is wrong:
You should not have an empty line between the headline and any scheduling information or drawers that follow it. An empty line signals the beginning of ordinary text, so that's presumably why you end up with multiple property drawers, but who adds the empty line? Even if you start with an empty line after the headline and a property drawer under it, org-id-get-create
will create another drawer, but unless you insert an empty line above that, no more such drawers will be added. It seems that you are inserting empty lines manually or through some other function.
You can run M-x org-lint
in your file. If it finds an empty line between the headline and the property drawer (possibly with other stuff in-between), it will complain with the (somewhat mysterious and misleading) message " Incorrect contents for PROPERTIES drawer". It will also identify a bunch of other common problems, so it's a good idea to run it occasionally on your Org mode files. It also indicates the line number(s) where it thinks there is a problem.
How to fix it
The following Org file documents my attempt to come up with a solution. It contains the necessary code (although you should heed the WARNINGS in the description: it is NOT turn-key code). It also contains a fairly long description of how to test the code before you unleash it on your file and how to go about unleashing it on your file.
Hope it helps!
* Example file
** DONE Sample Headline
CLOSED: [2023-11-24 Fri 12:16]
:PROPERTIES:
:ID: 09038df1-77b4-4ca7-9549-be6dbb23e78f
:VISIBILITY: folded
:END:
:PROPERTIES:
:ID: acecf527-7c1e-4786-8ff0-011dbab1c19d
:END:
:PROPERTIES:
:ID: c2201624-653e-4a84-ae64-df60317af2e6
:END:
:PROPERTIES:
:ID: 6a3cfa56-2433-4365-9d55-c3a1b8c1e968
:END:
:LOGBOOK:
:END:
[2023-09-14 Thu 08:59]
Some text (1)
** Another headline with messed-up priorities - multiple drawers but no empty line above.
:PROPERTIES:
:ID: abaebf32-e061-4019-ad86-02d11be0d33e
:END:
:PROPERTIES:
:ID: e1379898-de1b-476c-9e72-e512120b8a87
:END:
Some text(2)
** Another headline - just one drawer, so no problem here, so munging should leave this one unchanged
:PROPERTIES:
:ID: bc7a595a-13ec-48b1-9928-cefae2561433
:END:
Some text (3)
** Another headline - no properties drawer here - copy the whole thing as-is
Some text (4)
* Code :noexport:
The first code block below and the `org-map-entries' call in
`ndk/fix-priorities' is specific to my testing: I consider
the tree with the headline "Example file" above (which is at the
beginning of the buffer) and process all the headlines at level 2 of
that tree.
You need to do `C-c C-c' on the second code block to define the
functions used: that only needs to be done once unless modifications
are made to the functions. After a change, do `C-c C-c' again on the
second code block to load the new definition(s).
To test the four scenarios above, do `C-c C-c' on the first code block
and look at the file `munged-properties.org' in the current
directory. To retest (e.g. after adding another scenario as a level-2
headline above), delete the `munged-properties-copy.org' file from the
current directory (and kill its buffer if there is one), then rerun
the whole thing with `C-c C-c' on the first code block and check the
file again.
WARNING: This code should be considered throw-away code. You might
learn something by studying it and the techniques are applicable
widely, but the code itself is intended for the particular situation
described in the question and is probably not useful as-is in other
situations (it remains to be seen how useful it is even for *this*
situation :-))
WARNING: This is not a turn-key solution: you will have to adapt the
code to your circumstances. However, it is safe to experiment with it:
it does not modify the original file.
For the "real" run, I would recommend putting the code in a file, say
`fix-properties.el', modify the function `ndk/fix-priorities' for your
circumstances (see the comments in the function for some
possibilities - if none of them meet your requirements, follow up with
a comment explaining why and I will try to respond), load the file
with `M-x load-file RET fix-properties.el', execute the `setq' of the
destination buffer below *once* with
`M-: (setq destination-buffer (find-file-noselect "munged-properties-copy.org")) RET'
Then open your problematic file, run the (suitably modified) function
`ndk/fix-priorities' with
`M-: (ndk/fix-priorities)RET'
and save the destination buffer with
`M-: (with-current-buffer destination-buffer (save-buffer))RET'
Check the `munged-properties-copy.org' file and if it does not look
good, delete it and rerun the last two steps after whatever necessary
changes are made. If you modify the functions, reload the
`fix-priorities.el' file to get the new definitions. Lather, rinse,
repeat until you are happy with the result. Then replace your
original file with the `munged-properties-copy.el' file (but keep a
copy of the original just in case).
#+name: fix-priorities-test
#+begin_src elisp :results drawer
;; global
(setq destination-buffer (find-file-noselect "munged-properties-copy.org"))
(save-excursion
(goto-char (point-min))
;; this will write a "corrected" version (no multiple priority drawers, no empty lines between headline and priority drawer)
;; to the above destination buffer
(ndk/fix-priorities)
;; We then save the destination buffer to a file so we can eyeball it
(with-current-buffer destination-buffer
(save-buffer)))
#+end_src
#+RESULTS: fix-priorities-test
:results:
nil
:end:
Second code block:
#+name: fix-priorities-functions
#+begin_src elisp :results drawer
(defun ndk/fix-priorities ()
;; this selects all level-2 headings in the current tree
(org-map-entries #'ndk/munge "LEVEL=2" 'tree)
;; another possibility, maybe applicable to the OP's situation is
;; to select all top-level headlines
;(org-map-entries #'ndk/munge "LEVEL=1" 'file)
;; ... or perhaps all headlines
;(org-map-entries #'ndk/munge t 'file)
)
(defun ndk/delete-extra-property-drawers ()
"Delete all the :PROPERTIES: drawers except the last one."
(let (start end)
;; find the point before the first occurrence of the string `:PROPERTIES' and remember it
;; This calculation is probably "good enough", but it's making implicit assumptions about
;; the line where that first occurrence is found.
(goto-char (point-min))
(setq start (when (search-forward ":PROPERTIES:" nil t) (beginning-of-line) (point)))
;; find point at beginning of the last occurrence of the string `:PROPERTIES' and remember it
;; this is the one we'll keep
(goto-char (point-max))
(setq end (search-backward ":PROPERTIES:" nil t))
;; check that we have a well-defined region and delete it
(when (and start end)
(delete-region start end))))
(defun ndk/munge ()
"Copy the current heading and its contents to a temp buffer and
munge away. Currently the munging consists of deleting the extra
properties drawers and deleting any empty lines between the
heading (and its planning section, if applicable) and the
remaining property drawer. After that, append the contents of
the temp buffer to the (globally defined) destination buffer."
(let* ((heading (org-element-at-point))
(beg (org-element-property :begin heading))
(end (org-element-property :end heading))
(s (progn (widen) (buffer-substring-no-properties beg end))))
(with-temp-buffer
(insert s)
;; delete extraneous property drawers
(ndk/delete-extra-property-drawers)
;; delete empty lines
(flush-lines "^[[:blank:]]*$" 1 (point))
;; append result to destination buffer
(let ((s (buffer-substring-no-properties (point-min) (point-max))))
(with-current-buffer destination-buffer
(goto-char (point-max))
(insert s))))))
#+end_src
#+RESULTS: fix-priorities-functions
:results:
ndk/munge
:end:
regexp-replace
or is it a recurring problem that needs a more formal solution? Finally, why does it happen? I don't think this is a common problem, so I wonder if it's a bug in your version of Org mode.