For a variety of reasons it's possible to end up with duplicate heading Id's (manually-set ID conflicts, copy pasta, duplicate files, etc.).

When refiling you may get a message

WARNING: 17 duplicate IDs found, check *Messages* buffer

Followed by a (possibly) long list of locations of duplicate Id's:

Duplicate ID "271b2fbe-cb4f-4eb8-86d5-91544028ccae", also in file /home/erik/data.org
Duplicate ID "bc7ea3e9-f821-4bee-9ce6-255083721a8a", also in file /home/erik/data.org
Duplicate ID "24afb249-8481-46a5-8237-06a0c22295eb", also in file /home/erik/data.org
Duplicate ID "6a545fe0-bd19-4523-8a6d-f7500a114f5f", also in file /home/erik/data.org

It's fairly simple to kill/copy the id and search through the document to replace duplicate ids, but that is rather onerous. I'm curious what mechanisms are available in org-mode to assist in resolving these or alternatively what people may have come up with to resolve this occasional problem with their own agendas.

It looks like the duplicate logic is limited to what is currently baked into org-id-update-id-locations and would be easy to extract, but I'd like to hear some other thoughts before I write a helper.

  • id also be very interested on thoughts on this as i also often have the same issues
    – zeltak
    Commented Jul 17, 2016 at 11:01

2 Answers 2


for org-id-update-id-locations

(defun my/org-id-update-id-locations (&optional files silent)
  "Scan relevant files for IDs.
Store the relation between files and corresponding IDs.
This will scan all agenda files, all associated archives, and all
files currently mentioned in `org-id-locations'.
When FILES is given, scan these files instead."
  (if (not org-id-track-globally)
      (error "Please turn on `org-id-track-globally' if you want to track IDs")
    (let* ((org-id-search-archives
        (or org-id-search-archives
        (and (symbolp org-id-extra-files)
             (symbol-value org-id-extra-files)
             (member 'agenda-archives org-id-extra-files))))
        (or files
         ;; Agenda files and all associated archives
         (org-agenda-files t org-id-search-archives)
         ;; Explicit extra files
         (if (symbolp org-id-extra-files)
             (symbol-value org-id-extra-files)
         ;; Files associated with live Org buffers
         (delq nil
               (mapcar (lambda (b)
                 (with-current-buffer b
                   (and (derived-mode-p 'org-mode) (buffer-file-name))))
         ;; All files known to have IDs
       file nfiles tfile ids reg found id seen (ndup 0))
      (when (member 'agenda-archives files)
    (setq files (delq 'agenda-archives (copy-sequence files))))
      (setq nfiles (length files))
      (while (setq file (pop files))
    (unless silent
      (message "Finding ID locations (%d/%d files): %s"
           (- nfiles (length files)) nfiles file))
    (setq tfile (file-truename file))
    (when (and (file-exists-p file) (not (member tfile seen)))
      (push tfile seen)
      (setq ids nil)
      (with-current-buffer (org-get-agenda-file-buffer file)
        (goto-char (point-min))
        (while (re-search-forward "^[ \t]*:ID:[ \t]+\\(\\S-+\\)[ \t]*$"
                      nil t)
          (setq id (match-string-no-properties 1))
          (if (member id found)
;added logic
    (if org-clone-delete-id
                    (org-entry-delete nil "ID")
                  (org-id-get-create t))
;end of added logic
            (message "Duplicate ID \"%s\", also in file %s"
                 id (or (car (delq
                           (lambda (x)
                         (if (member id (cdr x))
                             (car x)))
            (when (= ndup 0)
              (sit-for 2))
            (setq ndup (1+ ndup)))
            (push id found)
            (push id ids)))
        (push (cons (abbreviate-file-name file) ids) reg))))))
      (org-release-buffers org-agenda-new-buffers)
      (setq org-agenda-new-buffers nil)
      (setq org-id-locations reg)
      (setq org-id-files (mapcar 'car org-id-locations))
      (org-id-locations-save) ;; this function can also handle the alist form
      ;; now convert to a hash
      (setq org-id-locations (org-id-alist-to-hash org-id-locations))
      (if (> ndup 0)
      (message "WARNING: %d duplicate IDs found, check *Messages* buffer" ndup)
    (message "%d unique files scanned for IDs" (length org-id-files)))

You will need to run it twice to verify the IDs have been updated. eval-region it to try out.

I was searching for something similar to this because org-clone-subtree-with-time-shift didn't update subheading ids when it clones. The issue was apparent when I ran org-id-update-locations because Duplicate id warning will pop up for all the subheadings of the clones.

I solved it by adding the following logic. It loops until it runs out of headings and updates the id of the heading.

  (while (outline-next-heading)
    (if org-clone-delete-id
        (org-entry-delete nil "ID")
        (org-id-get-create t)

The following is the updated org-clone-subtree-with-time-shift function.

 (defun my/org-clone-subtree-with-time-shift(n &optional shift)
    "Clone the task (subtree) at point N times.
  The clones will be inserted as siblings.

  In interactive use, the user will be prompted for the number of
  clones to be produced.  If the entry has a timestamp, the user
  will also be prompted for a time shift, which may be a repeater
  as used in time stamps, for example `+3d'.  To disable this,
  you can call the function with a universal prefix argument.

  When a valid repeater is given and the entry contains any time
  stamps, the clones will become a sequence in time, with time
  stamps in the subtree shifted for each clone produced.  If SHIFT
  is nil or the empty string, time stamps will be left alone.  The
  ID property of the original subtree is removed.

  In each clone, all the CLOCK entries will be removed.  This
  prevents Org from considering that the clocked times overlap.

  If the original subtree did contain time stamps with a repeater,
  the following will happen:
  - the repeater will be removed in each clone
  - an additional clone will be produced, with the current, unshifted
    date(s) in the entry.
  - the original entry will be placed *after* all the clones, with
    repeater intact.
  - the start days in the repeater in the original entry will be shifted
    to past the last clone.
  In this way you can spell out a number of instances of a repeating task,
  and still retain the repeater to cover future instances of the task.

  As described above, N+1 clones are produced when the original
  subtree has a repeater.  Setting N to 0, then, can be used to
  remove the repeater from a subtree and create a shifted clone
  with the original repeater."
    (interactive "nNumber of clones to produce: ")
    (unless (wholenump n) (user-error "Invalid number of replications %s" n))
    (when (org-before-first-heading-p) (user-error "No subtree to clone"))
    (let* ((beg (save-excursion (org-back-to-heading t) (point)))
           (end-of-tree (save-excursion (org-end-of-subtree t t) (point)))
            (or shift
                (if (and (not (equal current-prefix-arg '(4)))
                           (goto-char beg)
                           (re-search-forward org-ts-regexp-both end-of-tree t)))
                     "Date shift per clone (e.g. +1w, empty to copy unchanged): ")
                  "")))         ;No time shift
            (and (org-string-nw-p shift)
                 (or (string-match "\\`[ \t]*\\([+-]?[0-9]+\\)\\([dwmy]\\)[ \t]*\\'"
                     (user-error "Invalid shift specification %s" shift)))))
      (goto-char end-of-tree)
      (unless (bolp) (insert "\n"))
      (let* ((end (point))
             (template (buffer-substring beg end))
             (shift-n (and doshift (string-to-number (match-string 1 shift))))
             (shift-what (pcase (and doshift (match-string 2 shift))
                           (`nil nil)
                           ("d" 'day)
                           ("w" (setq shift-n (* 7 shift-n)) 'day)
                           ("m" 'month)
                           ("y" 'year)
                           (_ (error "Unsupported time unit"))))
             (nmin 1)
             (nmax n)
             (n-no-remove -1)
             (org-id-overriding-file-name (buffer-file-name (buffer-base-buffer)))
             (idprop (org-entry-get beg "ID")))
        (when (and doshift
                   (string-match-p "<[^<>\n]+ [.+]?\\+[0-9]+[hdwmy][^<>\n]*>"
          (delete-region beg end)
          (setq end beg)
          (setq nmin 0)
          (setq nmax (1+ nmax))
          (setq n-no-remove nmax))
        (goto-char end)
        (cl-loop for n from nmin to nmax do
                  ;; Prepare clone.
                    (insert template)
                    (goto-char (point-min))
                    (and idprop (if org-clone-delete-id
                                    (org-entry-delete nil "ID")
                                  (org-id-get-create t)))
                    (while (outline-next-heading)
                      (and idprop (if org-clone-delete-id
                                      (org-entry-delete nil "ID")
                                    (org-id-get-create t)))
                   (goto-char (point-min))
                    (unless (= n 0)
                      (while (re-search-forward org-clock-line-re nil t)
                        (delete-region (line-beginning-position)
                           (line-beginning-position 2)))
                      (goto-char (point-min))
                      (while (re-search-forward org-drawer-regexp nil t)
                        (org-remove-empty-drawer-at (point))))
                    (goto-char (point-min))
                    (when doshift
                      (while (re-search-forward org-ts-regexp-both nil t)
                        (org-timestamp-change (* n shift-n) shift-what))
                      (unless (= n n-no-remove)
                        (goto-char (point-min))
                        (while (re-search-forward org-ts-regexp nil t)
                      (goto-char (match-beginning 0))
                      (when (looking-at "<[^<>\n]+\\( +[.+]?\\+[0-9]+[hdwmy]\\)")
                        (delete-region (match-beginning 1) (match-end 1)))))))
      (goto-char beg)))
  • This looks nice. How does the function behave if I have duplicate IDs with attachments connected to one of them? Say I have 2 headers with428a53bf-cf55-4382-9788-c643a313a49a ID (i.e. a duplicate) but one of them have attachments stored in ~/orgattach/42/8a53bf-cf55-4382-9788-c643a313a49a?
    – teabow
    Commented Jul 17, 2020 at 6:56
  • Not graceful If I may say. one of them will keep the previous id while the other will change its id. since the script automatically picks which heading will change its id, the heading you care about may not remain with its attachment. It is possible to make this nicer by querying the user which id to replace or saving the old id in another drawer for the user to check when attachments aren't found. Commented Aug 1, 2020 at 1:36

M-x org-delete-property-globally did the trick for me, once I knew which file had the messy IDs.

  • This ended up being a sound a safe solution for me because the duplicate IDs were in a `basement.org' file---with only archived entries, whose IDs weren't particularly useful anymore. But I had to put all the PROPERTIES drawers immediately below the heading. To do that, I used the followin macro: ●●● find (isearch-forward) ":PROPERTIES:" ● TAB to colapse it ● kill it ● org-previous-visible-heading (C-c C-p for me) ● next-line (C-p) ● org-beginning-of-line (C-a) ● org-yank (C-y) ●●● After recording the macro I used C-u 0 f4 to run it in the entire buffer. Hope it helps! Commented Mar 30, 2021 at 15:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.