I use org-archive-subtree to clean my org files. Most of my tasks lie under * Tasks header so I run the function on it using prefix (C-u C-c C-x C-s). I noticed that the function only asks for confirmation when it encounters a DONE task.

Would it be possible to change its behavior to:

1) Ask only when the item has DONE state and it has a CLOSED timestamp that's at least month old (or does not have this timestamp at all but is still in DONE). I like to review my last week seeing what I did so archiving too young tasks breaks this option for me.

2) Do the above checks but archive without asking if the conditions are met.

  • 1
    Until the desired function is found/written, a hack solution is to make a custom agenda view that shows tasks that meet this requirement, and then bulk archive from the agenda view.
    – mankoff
    Feb 22, 2018 at 6:27
  • There's an example of exactly that agenda-based approach here: doc.norang.ca/org-mode.html#Archiving I've been using that for a while in my own init and it's been working well. Feb 24, 2018 at 16:57
  • @StuartHickinbottom yes this seems to be exactly it. Unfortunately I cannot figure out how can I use this skipping function when calling the archive function.
    – kajman
    Mar 3, 2018 at 10:46
  • @kajman You can set up a custom agenda command to display that agenda, then use bulk agenda commands to mark them for archiving. e.g. the following then "C-c a A", then "m" to mark tasks you want to archive, then "B $" to archive the marked tasks. Not as long-winded as it sounds when you try it! (setq org-agenda-custom-commands (cons '("A" "Candidate trees for archiving" tags "-NOARCHIVE" ((org-agenda-overriding-header "Candidate tasks for archiving") (org-agenda-skip-function 'my-skip-non-archivable-tasks))) org-agenda-custom-commands)) Mar 3, 2018 at 17:59
  • @StuartHickinbottom Thanks this works great. Can you shed some light how did you come up with the solution? I feel almost retarded when I try configure my emacs, it always all seems like guessing to me and it takes forever to come with a solution like this. Do you have any hints, what to read, where too look etc? I tried looking into docus, read about the variables I thought could be changed and so on. Even having this function didn't help to configure this on my own. This seems simple but I cannot do it on my own so it irritates me a lot!
    – kajman
    Mar 5, 2018 at 17:24

3 Answers 3


I use this approach from the excellent page http://doc.norang.ca/org-mode.html#Archiving -- the whole of that page is worth a read, and Bernt Hansen deserves all the credit for this not me.

This creates a function used to produce a custom agenda showing tasks suitable for archiving. First, there's the function that skips tasks that aren't suitable for archiving:

;; Custom agenda skip function to skip entries with activity in the
;; past month used to find candidates for archiving. Adapted from
;; http://doc.norang.ca/org-mode.html#Archiving
(defun my/skip-non-archivable-tasks ()
  "Skip trees that are not available for archiving"
  (let ((next-headline (save-excursion (or (outline-next-heading) (point-max)))))
    ;; consider only tasks with done todo headings as archivable tasks
    (if (member (org-get-todo-state) org-done-keywords)
        (let* ((subtree-end (save-excursion (org-end-of-subtree t)))
               (daynr (string-to-number (format-time-string "%d" (current-time))))
               (a-month-ago (* 60 60 24 (+ daynr 1)))
               (last-month (format-time-string "%Y-%m-" (time-subtract (current-time) (seconds-to-time a-month-ago))))
               (this-month (format-time-string "%Y-%m-" (current-time)))
               (subtree-is-current (save-excursion
                                     (forward-line 1)
                                     (and (< (point) subtree-end)
                                          (re-search-forward (concat last-month "\\|" this-month) subtree-end t)))))
          (if subtree-is-current
              next-headline ;; has a date in this month or last month, so skip it
            nil)) ;; available to archive
      (or next-headline (point-max)))))

Next a custom agenda command is defined to show an agenda with tasks that can be archived.

;; Add an agenda view that utilises this skip function.
(setq org-agenda-custom-commands
       '("A" "Candidate trees for archiving" tags "-NOARCHIVE"
         ((org-agenda-overriding-header "Candidate tasks for archiving")
          (org-agenda-skip-function 'my/skip-non-archivable-tasks)))

Finally, use this as follows:

  1. Pull up the Agenda view of old tasks (no activity this or last month) with C-c a A (A is the custom agenda command defined above).
  2. To archive individually go to the entry and do C-c C-x C-a
  3. To bulk archive mark each entry in the agenda with m then bulk archive them with B $.

You do not give an example of the initial input and the desired output. However, if i understood correctly, assuming today is July 2020 and you have:

* Tasks
** TODO My task 1
SCHEDULED: <2020-06-28 Fri>
My task 1 description
** DONE My task 2
SCHEDULED: <2020-06-29 Sat>
My task 2 description
** DONE My task 3
SCHEDULED: <2020-06-30>
My task 3 description
** DONE My task 4
SCHEDULED: <2020-07-01>
My task 4 description  

this should do what you want:

(defun my-arch ()
  (org-map-entries 'my-arch-map
    (format-time-string "SCHEDULED<\"<%Y-%m-01>\"/DONE")

(defun my-arch-map ()
  (unless (string= (org-element-property :raw-value (org-element-at-point)) "Tasks")
    (setq org-map-continue-from (org-element-property :begin (org-element-at-point)))))

Specifically, if you put your cursor at Tasks level and execute M-x my-arch, the Task 2 and 3 (done in the past month) are archived.
Task 1, occurred in the past, but undone is not archived.
Task 4, done in the current month, is not archived.

As regards the code, org-map-entries applies a function (here my-arch-map) to everything matching a condition. The condition here is set dynamically on the basis of the current date, assumed something in July 2020. See orgmode.org/manual/Matching-tags-and-properties.html to learn about setting conditions.

The scope of org-map-entries is the tree, so nothing is done outside the tasks subtree.
After parsing an entry, org-map-entries skips to the next one. However, if your action implies removing the entry (such as here when archiving), the cursor is automatically placed on the next, so the skip would just cause an incorrect parsing here. To avoid this, with org-map-continue-from, you bring the cursor back to the beginning of current element, so the skip does not harm.

The unless condition is not strictly necessary here, but could turn handy for alternate matching. In fact, since the scope of org-map-entries is the tree, the whole Tasks subtree is parsed first and then the subtree elements. If you use a negative condition, such as archiving everything not marked as TODO, then the entire Tasks subtree matches and is archived disregarding its element values. Also is you condition is only related to the SCHEDULING property, no property at Tasks level means a matching and so again it would be archived. To avoid this edge cases, we set to never parse an entry, such as the main one, whose raw value is "Tasks".


This is an old question but recently tried to solve the same problem with org-ql. Hopefully, someone else stumbling on this will find it useful.

This is what I came up with

(defun +org-archive-archivable()
    "Automatically archive all items that were closed 30 days ago or before"
    (org-ql-select org-agenda-files `(closed :to ,(ts-format (ts-adjust 'day -30 (ts-now))))
      :action 'org-archive-subtree-default))

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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