Projects

A project is a task made up of multiple items, referred to in OmniFocus as actions. Projects are typically more complex than individual action items, and can include several related actions. The Projects perspective displays all of your projects in a list, which can be grouped into folders to create hierarchy.

Project Properties

As with most scriptable objects, instances of the Project class have properties that define their use and purpose. Here are the properties of a project:

IMPORTANT: every project has an invisible “root” task (referenced using the task property listed above) that represents the parent project.

Although the Project class has “task-like” properties, you may OPTIONALLY set the values of the “task-like” properties of a new or existing project, such as flagged, dueDate, effectiveDueDate, etc. by addressing the project’s root task.

A project’s root task is referenced by appending the project’s task property to Project object reference in your script statements, as shown in the example script shown below. (line 10)

omnifocus://localhost/omnijs-run?script=try%7Bvar%20cal%20%3D%20Calendar%2Ecurrent%0Avar%20now%20%3D%20new%20Date%28%29%0Avar%20today%20%3D%20cal%2EstartOfDay%28now%29%0Avar%20dc%20%3D%20new%20DateComponents%28%29%0Adc%2Eday%20%3D%2030%0Avar%20targetDate%20%3D%20cal%2EdateByAddingDateComponents%28today%2Cdc%29%0A%2F%2F%20NEW%20PROJECT%0Avar%20project%20%3D%20new%20Project%28%22My%20Project%22%29%0A%2F%2F%20ASSIGN%20DUE%20DATE%0Aproject%2Etask%2EdueDate%20%3D%20targetDate%7Dcatch%28err%29%7Bconsole%2Elog%28err%29%7D
New Project Due 30 Days from Today
 

var cal = Calendar.current var now = new Date() var today = cal.startOfDay(now) var dc = new DateComponents() dc.day = 30 var targetDate = cal.dateByAddingDateComponents(today,dc) // NEW PROJECT var project = new Project("My Project") // ASSIGN DUE DATE project.task.dueDate = targetDate

TIP: the target date in the example above was calculated using functions of the built-in Calendar class, which is supported by all Omni applications.

Project.ReviewInterval Class

Project.ReviewInterval is a value object which represents a simple repetition interval.

Properties:

Because an instance of the Project.ReviewInterval class is a value object rather than a proxy, changing its properties doesn’t affect any projects directly. To change a project’s review interval, update the value object and assign it back to the project’s reviewInterval property:

Change Project Review Interval


var project = projectNamed("Miscellaneous") var reviewIntervalObj = project.reviewInterval reviewIntervalObj.steps = 2 reviewIntervalObj.unit = "months" project.reviewInterval = reviewIntervalObj

NOTE: At one time these simple repetition intervals were also used for task repetitions, but over time those were replaced with the more flexible Task.RepetitionRule class. Eventually, expect to also replace this review interval with flexible repetition rules.

New Project

Instances of the Project class are created using the standard JavaScript new item constructor, with the name of the new project as the parameter passed into the function:

omnifocus://localhost/omnijs-run?script=try%7Bproject%20%3D%20new%20Project%28%22My%20Top%2DLevel%20Project%22%29%0Anew%20Task%28%22First%20Project%20Task%22%2C%20project%29%0Anew%20Task%28%22Second%20Project%20Task%22%2C%20project%29%7Dcatch%28err%29%7Bconsole%2Elog%28err%29%7D
New Project with Tasks
 

var project = new Project("My Top-Level Project") new Task("First Project Task", project) new Task("Second Project Task", project)
top-level-project

And here is an example of creating a folder containing multiple new projects:

omnifocus://localhost/omnijs-run?script=try%7BmasterFolder%20%3D%20new%20Folder%28%22Master%20Project%22%29%0Anew%20Project%28%22Preparation%22%2C%20masterFolder%29%0Anew%20Project%28%22Build%22%2C%20masterFolder%29%0Anew%20Project%28%22Finish%22%2CmasterFolder%29%7Dcatch%28err%29%7Bconsole%2Elog%28err%29%7D
New Folder with Projects
 

var masterFolder = new Folder("Master Project") new Project("Preparation", masterFolder) new Project("Build", masterFolder) new Project("Finish",masterFolder)
new-folder-with-projects

Here is an example script for creating a series of new projects with sequential names:

Project Sequence


var alphabet = 'abcdefghijklmnopqrstuvwxyz'.split('') alphabet.forEach(char => { new Project("Project " + char.toUpperCase()) })

Project Types

Projects are distinguished by their type, which varies based on how actions inside the project must be completed. Project type also affects how actions within the project show up according to the perspective’s View options.

By default, new projects have parallel actions.

To create a new project containing sequential actions, set the sequential property of the project or the project’s root task to true:

omnifocus://localhost/omnijs-run?script=try%7Bproject%20%3D%20new%20Project%28%22Sequential%20Project%22%29%0Aproject%2Etask%2Esequential%20%3D%20true%7Dcatch%28err%29%7Bconsole%2Elog%28err%29%7D
New Sequential Project
 

var project = new Project("Sequential Project") project.sequential = true
New Sequential Project (root task)


var project = new Project("Sequential Project") project.task.sequential = true

To create a new project with single actions (list), set the value of the project’s containsSingletonActions property to true:

omnifocus://localhost/omnijs-run?script=try%7Bvar%20project%20%3D%20new%20Project%28%22Single%20Actions%20Project%22%29%0Aproject%2EcontainsSingletonActions%20%3D%20true%7Dcatch%28err%29%7Bconsole%2Elog%28err%29%7D
New Single Actions Project
 

var project = new Project("Single Actions Project") project.containsSingletonActions = true

Project Functions

Here are the functions that can be used with an instance of the Project class.

NOTE: many of these functions, such as addAttachment(…) and addNotification(…) replicate the same functions used by the Task class and are often applied to projects by calling them on the root task of the project. See the task documentation for detailed explanation and examples.

Identifying Specific Projects

Projects can be identified by examining the value of their properties, with the most common example being the identification of projects by the value of their name property.

The projectNamed(…) function of the Database class provides a mechanism for locating the first occurence of a project with a specified name, located at the top-level of the database or host container. If no matching projects are found, a value of null is returned.

Identifying Top-Level Projects


projectNamed("My Top-Level Project") //--> [object Project: My Top-Level Project] projectNamed("Build") //--> null folderNamed("Master Project").projectNamed("Finish") //--> [object Project: Finish]

(01) Searching the top-level of the database library for a project specifed by name.

(03) The search finds no matching projects and the function returns a value of: null

(05) Using the folderNamed(…) function of the Database class to locate a project within a top-level folder.

As shown in the script above, projects can be stored within folder hierarchies. The projectNamed(…) method searches the top-level library directory.

To search througout the entire library folder hierarchies, you can use either the apply(…) function of the Library class, or by iterating the value of the flattenedProjects property of the Database class.

The following example script uses the apply() function to locate the first project whose name matches the specified name:

Reveal Named Project (apply function)


var targetName = "Roof Repair" var targetProject = null library.apply(item => { if (item instanceof Project){ if (item.name === targetName){ targetProject = item return ApplyResult.Stop } } }) if (targetProject){ var id = targetProject.id.primaryKey URL.fromString("omnifocus:///task/" + id).open() }

The following script provides the same functionality as the previous example, but instead iterates the flattenedProjects property of the Database class to locate the first project whose name matches the specified name:

Reveal Named Project (flattenedProjects property)


var targetName = "Roof Repair" var targetProject = flattenedProjects.byName(targetName) if(targetProject){ var id = targetProject.id.primaryKey URL.fromString("omnifocus:///task/" + id).open() }

To retrieve object references to all projects whose name matches a specified name, use the flattenedProjects property with a JavaScript filter() function:

Focus Named Projects


var targetName = "Summary" var targetProjects = flattenedProjects.filter(project => { return project.name === targetName }) if (targetProjects.length > 0 && app.platformName = 'macOS'){ var win = document.windows[0] win.perspective = Perspective.BuiltIn.Projects win.focus = targetProjects }

To identify projects by the value of their properties, use a technique involving the filtering of the flattenedProjects property in a manner similar to the previous script:

Projects Due Tomorrow


var fmatr = Formatter.Date.withStyle(Formatter.Date.Style.Short) var rangeStart = fmatr.dateFromString('1 day') var rangeEnd = fmatr.dateFromString('2 days') var targetProjects = flattenedProjects.filter(project => { var projectDueDate = project.effectiveDueDate return projectDueDate > rangeStart && projectDueDate < rangeEnd }) if (targetProjects.length > 0 && app.platformName = 'macOS'){ var win = document.windows[0] win.perspective = Perspective.BuiltIn.Projects win.focus = targetProjects }

TIP: The previous script demonstrates the use of the Formatter.Date class in order to specify a date/time range.

The Project.Status Class

When planning and subsequently reviewing a project, it can be useful to give it a status to indicate whether work is progressing or plans have changed.

TIP: Dropped and completed items accumulate in your database over time. If you find that things are becoming unwieldy, archiving can help lighten the load.

With Omni Automation you can change the status of a project by changing the value of the project’s status property to one of the options of the Project.Status class:

Put Project On-Hold


var project = projectNamed("PROJECT A") project.status = Project.Status.OnHold

Here’s a script that will change the status of all active projects within a folder to be on-hold:

Put Active Projects On-Hold


var targetFolder = flattenedFolders.byName("Renovation") if (targetFolder){ targetFolder.flattenedProjects.forEach(project => { if(project.status === Project.Status.Active){ project.status = Project.Status.OnHold } }) }

And a version that reverses the previous action by changing the status of on-hold projects in a specified folder to be active:

Make On-Hold Projects Active


var targetFolder = flattenedFolders.byName("Renovation") if (targetFolder){ targetFolder.flattenedProjects.forEach(project => { if(project.status === Project.Status.OnHold){ project.status = Project.Status.Active } }) }

Here’s a sudo-perspective query that focuses all active projects due next month who have been tagged with a tag named “PRIORITY”:

Active Projects Due Next Month Tagged “PRIORITY”


var cal = Calendar.current var dc = cal.dateComponentsFromDate(new Date()) dc.day = 1 dc.month = dc.month + 1 var nextMonth = cal.dateFromDateComponents(dc) var nextMonthIndex = cal.dateComponentsFromDate(nextMonth).month var targetTagTitle = "PRIORITY" var matchedProjects = flattenedProjects.filter(project => { var dateDue = project.dueDate if(!dateDue){ projectDueMonthIndex = 0 } else { var projectDueMonthIndex = cal.dateComponentsFromDate(dateDue).month } return ( project.status === Project.Status.Active && projectDueMonthIndex === nextMonthIndex && project.tags.map(tag => tag.name).includes(targetTagTitle) ) }) if(matchedProjects.length > 0){ var win = document.windows[0] win.perspective = Perspective.BuiltIn.Projects win.focus = matchedProjects } else { new Alert("NO MATCHES", "No projects with the provided parameters were found.").show() }

Here’s an example plug-in that will change the status of the selected project to On-Hold and move it into a folder named: “Inactive”

Put Project On-Hold and Move to Folder
  

/*{ "type": "action", "targets": ["omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation.of.move-project-to-inactive-folder", "version": "1.0", "description": "Puts a project "On-Hold" and then moves it to the 'Inactive' folder.", "label": "Move to Inactive Folder", "shortLabel": "Move to Inactive", "paletteLabel": "Move to Inactive", "image": "gearshape" }*/ (() => { const action = new PlugIn.Action(function(selection, sender){ inactiveFolder = flattenedFolders.byName("Inactive") || new Folder("Inactive") project = selection.projects[0] moveSections([project], inactiveFolder) project.status = Project.Status.OnHold }); action.validate = function(selection, sender){ return (selection.projects.length === 1) }; return action; })();

If you want to get the value of the Project.Status property as a string:

Get Project Status String


function getStatusString(projectRef){ const {Active, Done, Dropped, OnHold} = Project.Status; switch(projectRef.status) { case Active: return 'Active'; case Done: return 'Done'; case Dropped: return 'Dropped'; case OnHold: return 'OnHold'; default: return null } }

*Thanks to Ryan M.

Listing Uncompleted Projects

One way to generate an array of uncompleted projects (those whose status is either Active or On Hold) is to use the filter() function on the value of the flattenedProjects property:

Listing Uncompleted Projects


uncompletedProjects = flattenedProjects.filter(project => { projStatus = project.status return ( projStatus === Project.Status.Active || projStatus === Project.Status.OnHold ) })

Project Tasks

Every instance of the Project class has a root task, represented by the value of the project’s task property.

By default, new projects have parallel actions. To create a project with sequential tasks, set the value of the root task’s sequential property to true:

New Sequential Project


var project = new Project("My Project") project.sequential = true new Task("Task One", project) new Task("Task Two", project) new Task("Task Three", project)

To create a project with single actions, set the value of the projects’s containsSingletonActions property to true:

New Single-Actions Project


var project = new Project("My Project") project.containsSingletonActions = true new Task("Task One", project) new Task("Task Two", project) new Task("Task Three", project)

Here's a script showing how to target a specific task within a specific project:

Flag Specific Task within Specified Project
 

var project = flattenedProjects.byName("Personel Roster") if(project){ var task = project.flattenedTasks.byName("Peer Review") if(task){task.flagged = true} }

Sorting Non-Sequential Project Tasks

Should you wish to reorder the non-sequential tasks of a project, you can use a compare function with the JavaScript sort(…) method to sort the child tasks of a project alphabetically by name, and then re-order the tasks by using the moveTasks() function of the Database class. The following plug-in demonstrates this technique:

Here's the basic task sorting code for the selected project:

omnifocus://localhost/omnijs-run?script=var%20items%20%3D%20document%2Ewindows%5B0%5D%2Eselection%2Eprojects%0Aif%28items%2Elength%20%3D%3D%3D%201%29%7B%0A%09var%20project%20%3D%20items%5B0%5D%0A%09if%20%28%21project%2Esequential%29%7B%0A%09%09var%20tasks%20%3D%20project%2Etask%2Echildren%0A%09%09if%20%28tasks%2Elength%20%3E%201%29%7B%0A%09%09%09tasks%2Esort%28%28a%2C%20b%29%20%3D%3E%20%7B%0A%09%09%09%20%20var%20x%20%3D%20a%2Ename%2EtoLowerCase%28%29%3B%0A%09%09%09%20%20var%20y%20%3D%20b%2Ename%2EtoLowerCase%28%29%3B%0A%09%09%09%20%20if%20%28x%20%3C%20y%29%20%7Breturn%20%2D1%3B%7D%0A%09%09%09%20%20if%20%28x%20%3E%20y%29%20%7Breturn%201%3B%7D%0A%09%09%09%20%20return%200%3B%0A%09%09%09%7D%29%0A%09%09%09moveTasks%28tasks%2C%20project%29%0A%09%09%7D%0A%09%7D%0A%7D
Sort Tasks of Non-Sequential Project
 

var items = document.windows[0].selection.projects if(items.length === 1){ var project = items[0] if (!project.sequential){ var tasks = project.task.children if (tasks.length > 1){ tasks.sort((a, b) => { var x = a.name.toLowerCase(); var y = b.name.toLowerCase(); if (x < y) {return -1;} if (x > y) {return 1;} return 0; }) moveTasks(tasks, project) } } }

And here's the code put into an OmniFocus plug-in:

/*{ "type": "action", "targets": ["omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation.of.alpha-sort-project-tasks", "version": "1.1", "description": "This action will alphabetically sort the tasks of the selected non-sequential project.", "label": "Alpha-Sort Project Tasks", "shortLabel": "Sort Project Tasks", "paletteLabel": "Sort Project Tasks", "image":"list.bullet" }*/ (() => { const action = new PlugIn.Action(function(selection, sender){ project = selection.projects[0] if (!project.sequential){ var tasks = project.task.children if (tasks.length > 1){ tasks.sort((a, b) => { var x = a.name.toLowerCase(); var y = b.name.toLowerCase(); if (x < y) {return -1;} if (x > y) {return 1;} return 0; }) moveTasks(tasks, project) } } }); action.validate = function(selection, sender){ return (selection.projects.length === 1) }; return action; })();
Sort Non-Sequential Project Tasks by Name
  

/*{ "type": "action", "targets": ["omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation.of.alpha-sort-project-tasks", "version": "1.1", "description": "This action will alphabetically sort the tasks of the selected non-sequential project.", "label": "Alpha-Sort Project Tasks", "shortLabel": "Sort Project Tasks", "paletteLabel": "Sort Project Tasks", "image": "list.bullet" }*/ (() => { const action = new PlugIn.Action(function(selection, sender){ project = selection.projects[0] if (!project.sequential){ tasks = project.tasks if (tasks.length > 1){ tasks.sort((a, b) => { x = a.name.toLowerCase(); y = b.name.toLowerCase(); if (x < y) {return -1;} if (x > y) {return 1;} return 0; }) moveTasks(tasks, project) } } }); action.validate = function(selection, sender){ return (selection.projects.length === 1) }; return action; })();

To remove tasks from a project, use the deleteObject() function of the Database class:

Remove Project Tasks


var selectedProjects = document.windows[0].selection.projects if(selectedProjects.length === 1){ selectedProjects.tasks.forEach(task => { deleteObject(task) }) }

Focusing Most Recently Added/Edited Projects

Script that will focus the most recently modified project:

Most Recently Modified Project


sortedProjects = flattenedProjects if(sortedProjects.length > 0){ sortedProjects.sort((a, b) => b.task.modified - a.task.modified) document.windows[0].perspective = Perspective.BuiltIn.Projects document.windows[0].focus = [sortedProjects[0]] }

Script that will focus the most recently added project:

Most Recently Added Project


sortedProjects = flattenedProjects if(sortedProjects.length > 0){ sortedProjects.sort((a, b) => b.task.added - a.task.added) document.windows[0].perspective = Perspective.BuiltIn.Projects document.windows[0].focus = [sortedProjects[0]] }

Project Tags

In OmniFocus, a tag represents an association that a task has to the world around it. A tag could be a person, place, or thing most relevant to completion of a project, or it could represent a mindset most applicable to execution of an action item.

An item can have as many tags as you find useful, and there is no specific purpose assigned to them; use tags to assign priority, group items by energy level or required equipment, or don’t use them at all.

The Tags perspective displays a list of your tags in the sidebar, and a list of all your actions grouped by the tags they belong to in the outline.

The following script examples demonstrate the use of tags with projects.

Retrieving Tags Assigned to a Project

To list the tags assigned to a project, the tags property of the project is used:

Get Names of Project Tags


project = flattenedProjects.byName("PROJECT A") if(project){ tagNames = project.tags.map(tag => { return tag.name }) console.log(tagNames) }

Assigning Tags to Projects

Existing database tags are assigned to a project by calling the addTag(…) function, passing in a reference to the tag object as the function parameter.

Here's a script assigns a specified tag to the specified project, creating the tag object if it does not exist:

Assign Tag to Project


project = flattenedProjects.byName("PROJECT A") if(project){ tag = flattenedTags.byName("Funded") || new Tag("Funded") project.addTag(tag) }

The addTags() function is used to assign each of an array of tag objects to a project:

Assign Multiple Tags to Project


project = flattenedProjects.byName("PROJECT A") if(project){ tagTitles = ["North","South","East","West"] tagObjs = new Array() tagTitles.forEach(title => { tagObj = flattenedTags.byName(title) || new Tag(title) tagObjs.push(tagObj) }) project.addTags(tagObjs) }

In the script above, note the use of the binary conditional statement (line 6) to either retrieve an object reference to an existing tag identified by name, or to create a new tag object with the provided title.

Removing Tags

Tags can be “unassigned” from projects using the removeTag(…) function, which takes a tag reference as its passed-in parameter. NOTE: the unassigned tag is simply unassigned from the project, it is not deleted from the database.

Remove Specified Tag from Project


project = flattenedProjects.byName("PROJECT A") if(project){ tag = flattenedTags.byName("Repaired") if(tag){project.removeTag(tag)} }

The following script example, clears all tags assigned to a specified project:

Clear Tags from Project


project = flattenedProjects.byName("PROJECT A") if(project){ project.clearTags() }
omnifocus://localhost/omnijs-run?script=try%7Bvar%20items%20%3D%20document%2Ewindows%5B0%5D%2Eselection%2Eprojects%0Aif%28items%2Elength%20%3E%200%29%7B%0A%09items%2EforEach%28project%20%3D%3E%20%7B%0A%09%09project%2EclearTags%28%29%0A%09%7D%29%0A%7D%7Dcatch%28err%29%7Bconsole%2Elog%28err%29%7D
Clear Tags from Selected Projects
 

items = document.windows[0].selection.projects if(items.length > 0){ items.forEach(project => { project.clearTags() }) }

Ordering Tags

Tags assigned to a project can be arbitrarily ordered by using the Tag-ordering properties and functions detailed in the Task class, and applying those properties and methods to the project instance’s root task (in Red):

Tag Ordering


project = new Project("Tag Ordering Example") tagRed = flattenedTags.byName("Red") || new Tag("Red") tagGreen = flattenedTags.byName("Green") || new Tag("Green") tagBlue = flattenedTags.byName("Blue") || new Tag("Blue") project.task.addTag(tagBlue) insertionLocation = project.task.beforeTag(tagBlue) project.task.addTag(tagGreen, insertionLocation) project.task.addTag(tagRed, project.task.beginningOfTags) id = project.id.primaryKey URL.fromString(`omnifocus:///task/${id}`).open()

See the Task documentation for more information regarding the ordering of tags.

Tag Ordering Demo

The following script will create and setup a demo project for tag sorting:

omnifocus://localhost/omnijs-run?script=project%20%3D%20new%20Project%28%22Colors%20of%20the%20Visible%20Light%20Spectrum%22%29%0Acolors%20%3D%20%5B%22Red%22%2C%20%22Orange%22%2C%20%22Yellow%22%2C%20%22Green%22%2C%20%22Cyan%22%2C%20%22Blue%22%2C%20%22Violet%22%5D%0AcurrentTags%20%3D%20flattenedTags%0Acolors%2Esort%28%29%2EforEach%28color%20%3D%3E%20%7B%0A%09tag%20%3D%20currentTags%2EbyName%28color%29%20%7C%7C%20new%20Tag%28color%29%0A%09project%2Etask%2EaddTag%28tag%29%0A%7D%29%0AtagTitles%20%3D%20project%2Etags%2Emap%28tag%20%3D%3E%20tag%2Ename%29%0Aconsole%2Elog%28tagTitles%29%0AURL%2EfromString%28%22omnifocus%3A%2F%2F%2Ftask%2F%22%20%2B%20project%2Eid%2EprimaryKey%29%2Eopen%28%29
Tag Ordering Demo Project
 

project = new Project("Colors of the Visible Light Spectrum") colors = ["Red", "Orange", "Yellow", "Green", "Cyan", "Blue", "Violet"] currentTags = flattenedTags colors.sort().forEach(color => { tag = currentTags.byName(color) || new Tag(color) project.task.addTag(tag) }) tagTitles = project.tags.map(tag => tag.name) console.log(tagTitles) URL.fromString("omnifocus:///task/" + project.id.primaryKey).open()

The tag titles will be logged to the console in alphabetical order, not in their actual scientific order:

Resulting Tag Titles


["Blue", "Cyan", "Green", "Orange", "Red", "Violet", "Yellow"]

And the following script will redorder the project’s tags into the provided scientific order. Note the appropriate functions and properties (in Red):

omnifocus://localhost/omnijs-run?script=project%20%3D%20flattenedProjects%2EbyName%28%22Colors%20of%20the%20Visible%20Light%20Spectrum%22%29%0Acolors%20%3D%20%5B%22Red%22%2C%20%22Orange%22%2C%20%22Yellow%22%2C%20%22Green%22%2C%20%22Cyan%22%2C%20%22Blue%22%2C%20%22Violet%22%5D%0AcurrentTags%20%3D%20flattenedTags%0AorderedTags%20%3D%20colors%2Emap%28color%20%3D%3E%20currentTags%2EbyName%28color%29%29%0AorderedTags%2EforEach%28%28tag%2C%20index%29%20%3D%3E%20%7B%0A%09if%28index%20%3D%3D%3D%200%29%7B%0A%09%09project%2Etask%2EmoveTag%28tag%2C%20project%2Etask%2EbeginningOfTags%29%0A%09%7D%20else%20if%28index%20%3D%3D%3D%20orderedTags%2Elength%29%7B%0A%09%09project%2Etask%2EmoveTag%28tag%2C%20project%2Etask%2EendingOfTags%29%0A%09%7D%20else%20%7B%0A%09%09insertionPostion%20%3D%20project%2Etask%2EafterTag%28orderedTags%5Bindex%2D1%5D%29%0A%09%09project%2Etask%2EmoveTag%28tag%2C%20insertionPostion%29%0A%09%7D%0A%7D%29%0AtagTitles%20%3D%20project%2Etags%2Emap%28tag%20%3D%3E%20tag%2Ename%29%0Aconsole%2Elog%28tagTitles%29%0AURL%2EfromString%28%22omnifocus%3A%2F%2F%2Ftask%2F%22%20%2B%20project%2Eid%2EprimaryKey%29%2Eopen%28%29
Reorder Tags to Provided Order
 

project = flattenedProjects.byName("Colors of the Visible Light Spectrum") colors = ["Red", "Orange", "Yellow", "Green", "Cyan", "Blue", "Violet"] currentTags = flattenedTags orderedTags = colors.map(color => currentTags.byName(color)) orderedTags.forEach((tag, index) => { if(index === 0){ project.task.moveTag(tag, project.task.beginningOfTags) } else if(index === orderedTags.length){ project.task.moveTag(tag, project.task.endingOfTags) } else { insertionPostion = project.task.afterTag(orderedTags[index-1]) project.task.moveTag(tag, insertionPostion) } }) tagTitles = project.tags.map(tag => tag.name) console.log(tagTitles) URL.fromString("omnifocus:///task/" + project.id.primaryKey).open()
Final Tag Order


["Red", "Orange", "Yellow", "Green", "Cyan", "Blue", "Violet"]
the-visible-light-spectrum

*Chart courtesy of ThoughtCo.

Flagging Projects

Projects can be “flagged” by setting the value of project’s flagged property to true:

Flag Overdue Projects


var today = Calendar.current.startOfDay(new Date()) var projects = flattenedProjects.filter(project => { return project.effectiveDueDate < today }).forEach(project => { project.flagged = true })

TIP: The properties and functions of the shared Calendar class can be used to generate date objects.

Packing List Plug-In

To demonstrate how the project-related properties and functions can be used to create automation tools, here are some OmniFocus plug-ins for you to examine and install.

The first example action plug-in incorporates the use of Action Forms to present user input controls for creating and displaying a packing list based upon the title and date parameters indicated by the user:

packing-list-input

(above) Interactive plug-in form. (below) Resulting packing list project.

packing-list

Note that the script includes code that determines the appropriate number of certain clothing items to pack, based on the length (in days) of the planned trip.

Packing List for Trip
  

/*{ "type": "action", "targets": ["omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation.packing-list-for-trip", "version": "2.1", "description": "Creates a project of single-actions used as a packing list.", "label": "Packing List for Trip", "paletteLabel": "Packing List", "image": "list.bullet.rectangle" }*/ (() => { const action = new PlugIn.Action(async function(selection, sender){ inputForm = new Form() dateFormat = Formatter.Date.Style.Short dateFormatter = Formatter.Date.withStyle(dateFormat, dateFormat) taskNameField = new Form.Field.String( "tripTitle", "Title", "My Trip" ) departureDateField = new Form.Field.Date( "departureDate", "Leave", null, dateFormatter ) returnDateField = new Form.Field.Date( "returnDate", "Return", null, dateFormatter ) inputForm.addField(taskNameField) inputForm.addField(departureDateField) inputForm.addField(returnDateField) inputForm.validate = function(formObject){ currentDateTime = new Date() departureDateObject = formObject.values["departureDate"] departureDateStatus = (departureDateObject && departureDateObject > currentDateTime) ? true:false returnDateObject = formObject.values["returnDate"] returnDateStatus = (returnDateObject && returnDateObject > departureDateObject) ? true:false textValue = formObject.values["tripTitle"] textStatus = (textValue && textValue.length > 0) ? true:false validation = (textStatus && departureDateStatus && returnDateStatus) ? true:false return validation } formObject = await inputForm.show("Enter the trip title and travel dates:","Continue") tripTitle = formObject.values['tripTitle'] StartDate = formObject.values['departureDate'] EndDate = formObject.values['returnDate'] tripDuration = parseInt((EndDate - StartDate)/86400000) projectName = "Packing List for " + tripTitle project = new Project(projectName) project.status = Project.Status.Active project.containsSingletonActions = true packingItems = ["Meds","Toothbrush","Toothpaste","Floss","Razor","Shaving Gel","Hair Brush","Deodorant","Underwear","Socks","Shirts","Pants","Belt"] packingItems1PerDay = ["Underwear","Socks","Shirts"] packingItems1Per2Day = ["Pants"] packingItems.forEach(packingItem => { amount = (packingItems1PerDay.includes(packingItem)) ? tripDuration : 1 if (packingItems1PerDay.includes(packingItem)){ amount = tripDuration } else if (packingItems1Per2Day.includes(packingItem)){ amount = tripDuration / 2 amount = (amount < 1) ? 1 : Math.ceil(amount) } else { amount = 1 } suffix = (amount > 1) ? ` (${amount})` : "" task = new Task(packingItem + suffix, project) task.dueDate = StartDate task.note = "" }) tasks = project.task.children if (tasks.length > 1){ tasks.sort((a, b) => { var x = a.name.toLowerCase(); var y = b.name.toLowerCase(); if (x < y) {return -1;} if (x > y) {return 1;} return 0; }) moveTasks(tasks, project) } projID = project.id.primaryKey URL.fromString("omnifocus:///task/" + projID).open() }); action.validate = function(selection, sender) { return true }; return action; })();

Moving Projects Plug-In

Here’s an OmniFocus plug-in that will move the selected projects into a folder created using the user-provided name. There is an option (checkbox) for indicating that the folder name should be unique and not in current use by any other folder in the database. NOTE: the folder title comparisons are case-insensitive.

move-projects-into-new-folder
Move Selected Projects into a New Folder
  

/*{ "type": "action", "targets": ["omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation.of.move-selected-projects-into-folder", "version": "1.8", "description": "Move the selected projects into a new top-level folder.", "label": "Move Selected Projects into New Folder", "shortLabel": "Move Projects", "paletteLabel": "Move Projects", "image": "plus.rectangle.on.folder.fill" }*/ (() => { const action = new PlugIn.Action(async function(selection, sender){ // CONSTRUCT THE FORM inputForm = new Form() // CREATE FORM ELEMENTS: TEXT INPUT, CHECKBOX textField = new Form.Field.String("folderName", null, null) checkSwitchField = new Form.Field.Checkbox( "ensureUniqueName", "Ensure folder name is unique", false ) // ADD THE ELEMENTS TO THE FORM inputForm.addField(textField) inputForm.addField(checkSwitchField) // VALIDATE FORM CONTENT inputForm.validate = function(formObject){ // EXTRACT VALUES FROM THE FORM’S VALUES OBJECT textValue = formObject.values['folderName'] checkboxValue = formObject.values['ensureUniqueName'] if (!textValue){return false} if (!checkboxValue){return true} if (checkboxValue){ folderNames.forEach(function(name){ if (name.toLowerCase() === textValue.toLowerCase()){ throw "ERROR: That folder name is in use." } }) return true } } // GET THE NAMES OF ALL FOLDERS folderNames = new Array() library.apply(item => { if (item instanceof Folder){folderNames.push(item.name)} }) // DIALOG PROMPT AND OK BUTTON TITLE formPrompt = "Enter name for new top-level folder:" buttonTitle = "Continue" // DISPLAY THE FORM DIALOG formObject = await inputForm.show(formPrompt, buttonTitle) // PERFORM PROCESSES USING FORM DATA textValue = formObject.values['folderName'] folder = new Folder(textValue) moveSections(selection.projects, folder) // SHOW THE FOLDER fldID = folder.id.primaryKey urlStr = "omnifocus:///folder/" + fldID URL.fromString(urlStr).open() }); action.validate = function(selection, sender){ return (selection.projects.length > 0) }; return action; })();

The Push-Out Project Due Date Plug-In

Here’s a plug-on that will add the indicated number of days to the due date of each selected project. If a project has no due date, its target date will be based upon the current date.

Push-Out Project Due Date
  

/*{ "type": "action", "targets": ["omnifocus"], "author": "Otto Automator", "identifier": "com.omni-automation.of.push-project-due-dates-by-amount", "version": "1.6", "description": "Will add the indicated number of days to the due date of each selected project. If project has no due date, target date will be based upon current date.", "label": "Push Out Project Due Dates", "shortLabel": "Push Due Dates", "paletteLabel": "Push Due Dates", "image": "xcalendar.badge.clock" }*/ (() => { const action = new PlugIn.Action(async function(selection, sender){ dayCount = 90 dayIndexes = new Array() dayIndexStrings = new Array() for (var i = 0; i < dayCount; i++){ dayIndexes.push(i) dayIndexStrings.push(String(i + 1)) } inputForm = new Form() dayMenu = new Form.Field.Option( "dayMenu", null, dayIndexes, dayIndexStrings, 0 ) checkSwitchField1 = new Form.Field.Checkbox( "shouldIncludeTasks", "Apply to all project tasks", true ) checkSwitchField2 = new Form.Field.Checkbox( "shouldIncludeDefers", "Push out deferment dates", true ) inputForm.addField(dayMenu) inputForm.addField(checkSwitchField1) inputForm.addField(checkSwitchField2) inputForm.validate = function(formObject){ return true } formPrompt = "Number of days to add to project due dates:" buttonTitle = "OK" formObject = await inputForm.show(formPrompt, buttonTitle) dayMenuIndex = formObject.values["dayMenu"] dayIndexString = dayIndexStrings[dayMenuIndex] pushoutDuration = parseInt(dayIndexString) shouldIncludeTasks = formObject.values["shouldIncludeTasks"] shouldIncludeDefers = formObject.values["shouldIncludeDefers"] cal = Calendar.current dc = new DateComponents() dc.day = pushoutDuration selection.projects.forEach(proj => { currentDueDate = proj.task.dueDate currentDueDate = (currentDueDate != null) ? currentDueDate : new Date() newDueDate = cal.dateByAddingDateComponents(currentDueDate,dc) proj.task.dueDate = newDueDate if(shouldIncludeDefers){ currentDeferDate = proj.task.deferDate if(currentDeferDate){ newDeferDate = cal.dateByAddingDateComponents(currentDeferDate,dc) proj.task.deferDate = newDeferDate } } if(shouldIncludeTasks){ proj.task.children.forEach(item =>{ currentDueDate = item.dueDate currentDueDate = (currentDueDate != null) ? currentDueDate : new Date() targetDate = cal.dateByAddingDateComponents(currentDueDate,dc) item.dueDate = targetDate if(shouldIncludeDefers){ currentDeferDate = item.deferDate if(currentDeferDate){ newDeferDate = cal.dateByAddingDateComponents(currentDeferDate,dc) item.deferDate = newDeferDate } } }) } }) }); action.validate = function(selection, sender){ return (selection.projects.length > 0) }; return action; })();