OF3 Re-Writing Tag Order in JavaScript (MacOS)

I’d like to automate the ordering of tags in my selected tasks according to the top-level grouping in the OF3 Tags list.

For instance, I want to be able to do this:

  1. Your Tag list looks like this:
  • A Items
    – Group A.1
    — Item A.1.1
    — Item A.1.2
    – Group A.2
    — Item A.2.1
    — Item A.2.2
  • B Items
    – Item B.1
    – Item B.2
  • C Items
    – Item C.1
    – Item C.2
  • D Items
    – Item D.1
    – Item D.2
  1. Your Actions/Tasks (regardless of project) look like this:

Task 1 : tags: [Item D.1, Item B.2, Item A2.1, Item A.1.2]
Task 2 : tags: [Item C.2, Item B.2, Item A.1.2, Item A2.1]

  1. You have a view and select the tasks of interest (in this case, I select Task 1 and Task 2)

  2. I then run a script that automatically orders the tags in my tasks according to the top level grouping. Note: sub-groups in the hierarchy is not necessary such that the “natural order” is taken. For instance, selecting Task 1 and Task 2 results in the following order:

Task 1 : tags: [Item A2.1, Item A.1.2, Item B.2, Item D.1]
Task 2 : tags: [Item A.1.2, Item A2.1, Item B.2, Item C.2]

In the above for Task 1, Item A2.1 and Item A.1.2 appear first because “A Items” is first in the Tags list. Note: the ordering of “A Items” is not important as long as they are grouped together. Item B.2 and Item D.1 follow the order of the top-level items “B Items” and “D Items” respectively.

In the above for Task 2, Item A.1.2 and Item A2.1 appear first because “A Items” is first in the Tags list. Note: in this case the natural ordering from the original task of these “A Items” happens to be in order from the Tags list but that’s just happenstance. The order of sub-items is not important as long as they are grouped together. Item B.2 and Item C.2 follow the order of the top-level items “B Items” and “C Items” respectively.

I have all of this logic written in the script below… with the exception of two related operations:

(1) removal of the old tags non-ordered tags for a task and (2) replacing it with the newly ordered tags in the task. This is line 57 in the script below (or see https://gist.github.com/doug4j/012a3de28e61669a11144df7525b9462/fde98ac8497c8bdc8268d0a9a1bdac3bc26de6cf#file-of3-reorder-task-tags-js-scpt-L57).

I see this capability done in AppleScript in the following code https://github.com/Rahlir/OmniFocusScripts/blob/31c0e78210ab069e78011597abb54a10f367f2cb/Sort%20Tags/sorttags.applescript#L15 . But, I prefer Javascript and simply can’t find out how to do this.

Any help offered would be wonderful. I’ve read the OmniFocus script doc (OmniFocus.sdef) and tried all sorts of things in Javascript but it hasn’t worked.

I found the following possibly related posts:

Example Script needing “replace with ordered tags” of3-reorder-task-tags.js.scpt.js (like 57 or here https://gist.github.com/doug4j/012a3de28e61669a11144df7525b9462#file-of3-reorder-task-tags-js-scpt-L57):

var app = Application('OmniFocus');
app.includeStandardAdditions = true;
var doc = app.defaultDocument;

var properRootTagOrder = getActiveRootOrderedTags(doc)

var content = doc.documentWindows.at(0).content;
var selectedTree = content.selectedTrees();
var selectedLen = selectedTree.length;
var totalTasks = 0;
var totalReorderedTasks = 0;

var tasks = [];
for (var i=0;i < selectedLen; i++) {
    var task = selectedTree[i];
    totalTasks = totalTasks + 1;
  }catch(e) {
    if (e.message === "User canceled.") {
      break; //get out of the loop (end the program)
    } else {
      // app.displayDialog("[Exception] Content Selected index [" + i + "] is not a task: " + e);	

tasks.forEach(task => {
  applyProperOrderToTask(task, properRootTagOrder);

function applyProperOrderToTask(task, properRootTagOrder) {
  var tags = task.value().tags()
  var thisDictRootTags = {}
  tags.forEach(tag => {
    //app.displayDialog("tag: " + tag.name())
    thisRootTag = getRootTag(tag)
    //app.displayDialog("root tag: " + thisRootTag.name())
    if (!(thisRootTag.id() in thisDictRootTags)) {
      thisDictRootTags[thisRootTag.id()] = []
    } else {
  var properlyOrderedTags = []
  properRootTagOrder.forEach( rootTag => { 
    if (rootTag.id() in thisDictRootTags) {
      thisDictRootTags[rootTag.id()].forEach(tag => {
  var properlyOrderedTagsStr = tagsNamesAsString(properlyOrderedTags)
  var tagsStr = tagsNamesAsString(tags)
  if (properlyOrderedTagsStr !== tagsStr) {
    //TODO: Apply actual change to task
    //Something like this but in Javascript https://github.com/Rahlir/OmniFocusScripts/blob/31c0e78210ab069e78011597abb54a10f367f2cb/Sort%20Tags/sorttags.applescript#L15
    //Logically I want to do something like this but the below does not work
    // task.value().tags.remove(tags) //Remove the original unsorted tags
    // task.value().tags.add(properlyOrderedTags) //Replace with the newly sorted orginal tags
    totalReorderedTasks = totalReorderedTasks + 1
    app.displayDialog("task '" + task.name()+ "' ordered tags [" + properlyOrderedTagsStr + "]")

function getRootTag(tag){
  parentTag = tag.container()
  if (parentTag.name().trim() === "OmniFocus") {
    return tag
  return getRootTag(parentTag)


function getActiveRootOrderedTags() {
  var allTags = doc.flattenedTags();
  var answer = ""
  var answer = []
  allTags.forEach(tag => {
    if (tag.container() !== null) {
      if (tag.container().name().trim() === "OmniFocus") {
        if (!tag.hidden()) {
  return answer

function tagsNamesAsString(tags) {
  if (tags === null) {
    return "";
  var answer = ""
  tags.forEach(tag => {
    if (answer === "") {
      answer = tag.name()
    } else {
      answer = answer + ", " + tag.name()
  return answer

app.displayDialog("Reordered " + totalReorderedTasks + " tasks from " + totalTasks +  " candidate tasks")

Doug Johnson

“The best way to find out what we really need is to get rid of what we don’t [need]”. - the life-changing magic of tidying up by Marie Kondo

1 Like

By studying this post Request: automate adding and removing tags to multiple tasks [Solved!] , I was able to get the script to work.

The trick to this missing block https://gist.github.com/doug4j/012a3de28e61669a11144df7525b9462/fde98ac8497c8bdc8268d0a9a1bdac3bc26de6cf#file-of3-reorder-task-tags-js-scpt-L57 in trying to remove the old tags and apply the correctly ordered tags was the following:

app.remove(task.value().tags(), { from: task.value().tags })
app.add(properlyOrderedTags, { to: task.value().tags })

as now used in this complete example:

The script is generic and now does exactly what I wanted.


  • This seems to only work when everything selected is “taggable” (In think in OF3 that’s Projects and Tasks),
  • Currently, there is no error handling to ensure that what is selected is taggable… so, for instance, if you select anything from the “tags” view (left) and the select from items view (right) this script currently can result in a loss of data (your tags can get lost for some selected items). Needlessly to say, I don’t want you to lose data!
  • Best I can tell, if you’ve selected the Projects view (left) and select anything (Right), this script will work correctly (and not lose data).

I’m sure that the above warnings can be overcome by some simple updates to the script that check for what views are selected and stops early in the program if everything isn’t “taggable”. This is a logical bit of error handling to do. I just don’t have time to do it right now.

Once tightened up with the error handling described above, I believe this is going to be a useful script for me. In the meantime, I’m going to cautiously use it (following the warnings above) to validate it does what I need/want.

I hope this complete example in its current state helps someone too. When I get to improving it, I’ll post updates.

Doug Johnson

“None of us our getting out of this alive.” - Scott Hansleman’s dad

1 Like

Should it help anyone, I added a “guard” to ensure you’re in the ‘Projects’ perspective here https://gist.github.com/doug4j/012a3de28e61669a11144df7525b9462#file-of3-reorder-task-tags-js-scpt-L8 which should prevent the data loss issue if your in the contexts view. (Also cleaned up some of the code.)

Hope this helps someone.

Doug Johnson

“There is a difference between being grounded and being stuck” -Unknown