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:
after (Folder.ChildInsertionLocation r/o) • Returns a location refering to position just after this project.
attachments (Array of FileWrapper) • An array of FileWrapper objects representing the attachments associated with the Project’s root Task.
before (Folder.ChildInsertionLocation r/o) • Returns a location refering to position just before this project.
beginning (Task.ChildInsertionLocation r/o) • Returns a location referring to the position before the first Task directly contained in the root task of this project.
children (TaskArray r/o) • An alias for the tasks property.
completed (Boolean r/o) • True if the project has been marked completed. Note that a project may be effectively considered completed if a containing project is marked completed.
completedByChildren (Boolean) • If set, the project will be automatically marked completed when its last child Task is marked completed.
completionDate (Date or null) • If set, the project is completed.
containsSingletonActions (Boolean) • If the property’s value is set to true, the project contains single tasks, and has no next task.
defaultSingletonActionHolder (Boolean) • If the property’s value is set to true, this is the Project that inbox tasks that have enough information specified (as selected by the user’s preferences) will be filed into upon a clean-up.
deferDate (Date or null) • If set, the project is not actionable until this date.
dropDate (Date or null) • If set, the project is dropped.
dueDate (Date or null) • If set, the project should be completed by this date.
effectiveCompletedDate (Date or null r/o) • Returns the computed effective completion date for the Project, based on its local completionDate and those of its containers.
effectiveDeferDate (Date or null r/o) • Returns the computed effective defer date for the Project, based on its local deferDate and those of its containers.
effectiveDropDate (Date or null r/o) • Returns the computed effective drop date for the Project, based on its local dropDate and those of its containers.
effectiveDueDate (Date or null r/o) • Returns the computed effective due date for the Project, based on its local dateDue and those of its containers.
effectiveFlagged (Boolean r/o) • Returns the computed effective flagged status for the Project, based on its local flagged and those of its containers.
ending (Task.ChildInsertionLocation r/o) • Returns a location referring to the position before the first Task directly contained in the root task of this project.
flagged (Boolean) • The flagged status of the project.
flattenedChildren (TaskArray r/o) • An alias for flattenedTasks.
flattenedTasks (TaskArray r/o) • Returns a flat array of all tasks contained within this Project’s root Task. Tasks are sorted by their order in the database.
hasChildren (Boolean r/o) • Returns true if this Project’s root Task has children, more efficiently than checking if children is empty.
lastReviewDate (Date or null) • No documentation available.
name (String) • The name of the Project’s root task.
nextReviewDate (Date or null) • No documentation available.
nextTask (Task or null r/o) • Returns the very next task that can be completed in the project, or null if there is none or the project contains singleton actions.
notifications (Array of Task.Notification r/o) • An array of the notifications that are active for this project. (see related documentation)
parentFolder (Folder or null r/o) • The Folder (if any) containing this project.
repetitionRule (Task.RepetitionRule or null) • The object holding the repetition properties for this project, or null if it is not repeating. (see related documentation)
reviewInterval (Project.ReviewInterval or null) • The object holding the review repetition properties for this project. See also lastReviewDate and nextReviewDate.
sequential (Boolean) • If true, then children of this project form a dependency chain. For example, the first task blocks the second one until the first is completed.
shouldUseFloatingTimeZone (Boolean) • When set, the dueDate and deferDate properties will use floating time zones. (Note: if a Project has no due or defer dates assigned, this property will revert to the database’s default setting.)
status (Project.Status) • The current status of the project as a whole. This does not reflect the status of individual tasks within the project root task – a project may be marked with the Done status and its individual tasks will be left with the completion state they had, in case the status is changed again to Active.
tags (TagArray r/o) • Returns the Tags associated with this Project.
task (Task r/o) • Returns the root task of the project, which holds the bulk of the project information, as well as being the container for tasks within the project. IMPORTANT: if you wish to copy the project or move it to a location that requires tasks, you would use this task as the object to be copied or moved.
tasks (Array of Task r/o) • Returns all the tasks contained directly in this Project’s root Task, sorted by their library order.
taskStatus (Task.Status r/o) • Returns the current status of the 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)
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:
steps (Number) • The count of units to use for this interval (e.g. “14” days or “12” months).
unit (String) • The units to use (e.g. “days”, “weeks”, “months”, “years”).
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:
new Project(name:String, position:Folder or Folder.ChildInsertionLocation or null) → (Project) • Create an instance of the Project class.
New Project with Tasks
var project = new Project("My Top-Level Project")
new Task("First Project Task", project)
new Task("Second Project Task", project)
And here is an example of creating a folder containing multiple new projects:
New Folder with Projects
var masterFolder = new Folder("Master Project")
new Project("Preparation", masterFolder)
new Project("Build", masterFolder)
new Project("Finish",masterFolder)
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.
Sequential • Sequential projects have actions that need to be completed in a predetermined order; the first item must be finished before you can move on to the next. In a sequential project, there is only ever one action available at a time. (this is also, by definition, the project’s first available action).
Parallel • Parallel projects consist of actions that can be completed in any order. In a parallel project, all incomplete actions are available, and the first available is the first one in the list.
Single Actions • A single action list isn’t a project in the traditional sense; it’s a list of loosely-related items that aren’t interdependent (a shopping list is an example of this). In a single action list, all actions are considered both available and first available.
TIP: • The difference between parallel and sequential projects is most visible when Projects View options are set to show only Available actions. (Actions beyond the first available action in a sequential project are blocked, and therefore hidden.)
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:
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:
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.
taskNamed(name: String) → (Task or null) • Returns the first top-level Task in this project the given name, or null.
appendStringToNote(stringToAppend: String) → ( ) • Appends stringToAppend to the end of the Project’s root Task’s note.
addAttachment(attachment: FileWrapper) → ( ) • Adds attachment as an attachment to the Project’s root Task. If the attachment is large, consider using the addLinkedFileURL() function instead. Including large attachments in the database can degrade app performance. [see task attachment documentation]
removeAttachmentAtIndex(index: Number) → ( ) • Removes the attachment at index from this Project’s root Task’s attachments array. [see task attachment documentation]
markComplete(date: Date or null) → (Task) • If the project is not completed, marks it as complete with the given completion date (or the current date if no date is specified). For repeating project, this makes a clone of the project and marks that clone as completed. In either case, the project that has been marked completed is returned.
markIncomplete() → ( ) • If the project is completed, marks it as incomplete.
addNotification(info: Number or Date) → (Task.Notification) • Add a notification to the project from the specification in info. Supplying a Date creates an absolute notification that will fire at that date. Supplying a Double will create a due-relative notification. Specifying a due-relative notification when this project’s task’s effectiveDueDate is not set will result in an error. [see task notification documentation]
removeNotification(notification: Task.Notification) → ( ) • Remove an active notification for this project. Supplying a notification that is not in this task’s notifications array, or a notification that has task to something other than this project’s task results in an error. [see task notification documentation]
addTag(tag: Tag) → ( ) • Adds a Tag to this project, appending it to the end of the list of associated tags. If the tag is already present, no change is made. The moveTags() function of the Database class can be used to control the ordering of tags within the task.
addTags(tags: Array of Tag) → ( ) • Adds multiple Tags to this project, appending them to the end of the list of associated tags. For any tags already associated with the Task, no change is made. The Database function moveTags can be used to control the ordering of tags within the task.
removeTag(tag: Tag) → ( ) • Removes a Tag from this project. If the tag is not associated with this project, no change is made.
removeTags(tags: Array of Tag) → ( ) • Removes multiple Tags from this project. If a tag is not associated with this project, no change is made.
clearTags() → ( ) • Removes multiple Tags from this project. If a tag is not associated with this project, no change is made.
addLinkedFileURL(url: URL) → ( ) • Links a file URL to this task. In order to be considered a file URL, url must have the file scheme. That is, url must be of the form file://path-to-file. The file at url will not be added to database, rather a bookmark leading to it will be added. In order to add files to a task, use the addAttachment function. Linking files is especially useful for large files, as including large files in the database can degrade app performance. This function throws an error if invoked on iOS. [see task linked file documentation]
removeLinkedFileWithURL(url: URL) → ( ) • Removes the first link to a file with the given url. This removes the bookmark that leads to the file at url. If the file itself is present in the database, use the removeAttachmentAtIndex function instead. [see task linked file documentation]
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.
projectNamed(name:String) → (Project or null) • Returns the first top-level Project with the given name, or null.
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.
Active (Project.Status r/o) • The default status for a new or ongoing project. It can be useful to review active projects regularly to determine what progress you’ve made, and whether they are still things you want to do.
Done (Project.Status r/o) • Eventually you’ll reach the successful end of a project. Select the project and then choose Completed in the Status section of the inspector (this automatically marks any unfinished actions in the project complete). If you’d like to revisit a completed project, change your View options to All or search for the project with the Everything filter.
Dropped (Project.Status r/o) • If you’ve decided not to work on a project any further, you can Drop it completely. It disappears from the Projects list, and its actions are likewise hidden. You could delete the project instead, but then you wouldn’t have any record of it; keeping it around in a dropped state means you can go back and check on actions you’ve completed regardless of whether they’re from still-relevant projects, and so on. To find a dropped project in your database, choose All in View options or search for it with the Everything filter.
OnHold (Project.Status r/o) • If you’re not sure whether you want to continue a project, you can change the project’s status from Active to On Hold. If you’ve chosen to show only Available items in View options, the project and its actions are removed from the project list in the sidebar and outline. Projects placed on hold are still available for review and reconsideration if you decide to prioritize them again in the future.
all (Array of Project.Status r/o) • An array of all items of this enumeration. Often used when creating a Form menu element.
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:
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:
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()
}
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:
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):
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"]
*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:
(above) Interactive plug-in form. (below) Resulting packing list project.
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 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;
})();