I have a document full of internal org-mode links of the form:

* Target Heading


[[Target Heading][Description]]

However, these break whenever I edit the heading that they link to, so I want to replace them with links of the form:

* Target Heading
  :ID:       403847B2-3D4C-4EFB-96DE-E0C5C52A02B5



I have started on some code that will follow the link at point, add an id to the heading it finds and save the link to that heading. I then want to update the link with the new target:

(defun adam-open ()
  (require 'org-id)
      (call-interactively 'org-open-at-point)
       (call-interactively 'org-store-link)))

The trouble with this is that because the link under point already points to a text-based link, org-insert-link fails to replace the target with the stored id. Also, org-insert-link works interactively

Is there any way to pass the id as a parameter to org-insert-link so that it overrides the default text-based target and avoids the interactive prompt?

2 Answers 2


You could use org-insert-link not totally interactively with (org-insert-link nil "other" "description") but that won't help.

The following functions are a quick-and-dirty solution to update your org-links maintaining your descriptions and creating org-ids for headers, if needed.

(defun adl/org-link-update ()
  (require 'org-id)
  (while (not (org-element-link-parser))
  (let ((list-of-links-p (org-element-property
          :contents-begin (org-element-link-parser))))
    (cond ((org-at-heading-p)
       (insert "[[id:")
       (insert "]")
       (delete-forward-char 1)
       (when list-of-links-p
         (zap-to-char 1 ?\])))

adl/org-link-update affects org-link at point and adl/org-link-update-buffer affects all org-links in current buffer.

(defun adl/org-link-update-buffer ()
  (while (not (org-next-link))
    (when (string= "fuzzy"
            :type (org-element-link-parser)))

EDIT: adl/org-link-update-buffer fixed.

https://orgmode.org/worg/dev/org-element-api.html#orgae0b143 for reference.

EDIT-2: adl/org-link-update fixed.

To differentiate "single links" from "lists of links", I used the fact that :contents-begin property is nil for "single links" as [[Heading 1]] and it is not nil for "lists of links" as [[Heading 1][Description]].

EDIT-3: adl/org-link-update fixed.

adl/org-link-update now affects only links targeting headings as @jagrg pointed out.

  • Thanks @adi! Can you think of how to limit this to internal org-mode links only?
    – Adam
    Commented Feb 11, 2019 at 16:22
  • sorry, I didn't consider other org-link types. I edited my answer.
    – adl
    Commented Feb 11, 2019 at 23:52
  • Hi @adl - this is a good start! the script works fine for a simple list of links of the format [[Heading 1][Description]] where each link is on its own line. But if the link is used in a sentence or if it is formatted as [[Heading 1]] the script fails. It would be great if this could be made robust against these additional use cases!
    – Adam
    Commented Feb 12, 2019 at 10:58
  • Hi @Adam - I'm considering that you want to use only adl/org-link-update-buffer. In this second edit, to adl/org-link-update works properly, you should place the cursor at the beginning of your org-link. Note that the beginning of a org-link is different from the beginning of displayed link text.
    – adl
    Commented Feb 13, 2019 at 0:04
  • That did it. Thanksn @adl!
    – Adam
    Commented Feb 13, 2019 at 12:01

Another way of doing this is to create (title . ID) element for each fuzzy link found in the buffer, and then replace the link with one that uses the heading's ID.

(defun adam-open ()
  (let (headings alist)
      (org-element-map (org-element-parse-buffer) 'link
    (lambda (link)
      (when (string= (org-element-property :type link) "fuzzy")
            (push (org-element-property :path link) headings))))
      (goto-char (point-min))
      (while (outline-next-heading)
    (let* ((elt (org-element-at-point))
               (title (org-element-property :title elt)))
      (when (member title headings)
      (goto-char (point-min))
      (while (outline-next-heading)
    (let* ((elt (org-element-at-point))
               (title (org-element-property :title elt))
               (id (org-element-property :ID elt)))
      (when (member title headings)
            (push (cons title id) alist))))
      (goto-char (point-min))
      (while (not (org-next-link))
    (org-element-map (org-element-parse-buffer) 'link
      (lambda (link)
            (let ((type (org-element-property :type link)))
              (when (and (string= type "fuzzy")
             (looking-at "\\[\\[\\([^]]*\\)\\(]]\\|]\\[[^]]*]]\\)"))
        (let* ((desc (match-string-no-properties 1))
               (id (assoc-default desc alist)))
          (when (member desc (mapcar #'car alist))
            (replace-match (format "[[id:%s][%s]]" id desc))))))))))))

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.