Omni Automation: Prerequisites & Task Dependencies

Please note that Omni Automation for OmniFocus is still in development and details are subject to change before it officially ships. If you have questions, please refer to Omni's Slack #automation channel.

Issue

Though OmniFocus has options for sequential and parallel action groups which provide some capacity for dealing with tasks which are dependant on each other, on occasion these are insufficient for my needs.

Context

I generally run up against two general scenarios where I find this set of automations useful:

  • Sometimes a task in one project can’t be started until a task in a second project has been completed; but the projects are nevertheless (at least in my mind, and often for the purpose of other workflows) distinct. (An example of this: accounting work for two related (but distinct) parties.
  • Sometimes the “dependency” relationship could be reflected using the built-in sequential and parallel task action group structure, but the level of complexity is such that the nesting of tasks becomes difficult to manage or just unwieldy to look at. (As an example: university coursework, which is often broken down into weekly components that should be completed sequentially. There are often also assignments attached to this that shouldn’t block further coursework. Sometimes there are optional readings, or only part of the coursework needs to be done before you can complete a particular part of an assignment.)

In addition, there are some scenarios (usually overlapping with the above) where the way I am thinking about a task is in terms of “sequential projects”. Again, university coursework is a good example here: to my way of thinking, each week’s content is its own project. But they still need to be done in order i.e. each week is dependant on the previous week.

Solution

I have written a plugin that contains a series of Omni Automation Scripts that use tags to designate one task as being dependant on another.

To achieve this, in my OmniFocus database I use three tags which are set up as subtags under a parent ‘Dependency’ tag:

  • Make Prerequisite: This is a temporary tag. Refer to the ‘Usage Notes’ below for details of how this is used.
  • 🔑: This tag denotes a task that is required to be completed before another (dependant) task becomes available.
  • 🔒 Dependant: This tag denotes a task that is currently unavailable because it is waiting for another task to be completed. This tag has an “on hold” status.

In addition, the ‘notes’ field is used to track more specific details of the prerequisite/dependant tasks:

  • A prerequisite task will have a note similar to [DEPENDANT: omnifocus:///task/elnhPEQEJzq] Task 2
  • A dependant task will have a note similar to [PREREQUISITE: omnifocus:///task/mEur73AEC07] Task 1

This means that a task can have more than one dependant or prerequisite task, which allows for greater complexity.

This solution also requires two Omni Automation scripts:

  • Add Prerequisite: This adds the ‘🔒 Dependant’ tag and adds the ‘🔑’ tag to the task that has been marked ‘Make Prerequisite’. It also prepends the details above to the notes of both tags. Then, it removes the ‘Make Prerequisite’ tag.
  • Complete For Prerequisite: This action (designed to be run on tasks once they have been completed) marks the task as done, finds any dependant tasks included in the note, removes the prerequisite from the dependant tasks’ note, and, if no other prerequisites remain for that task, removes the ‘🔒 Dependant’ tag. It also checks any parents of the task that might be completed at the same time, and does the same for those.

Future improvement: I am hopeful that in the future Omni Automation will allow us to automatically run a script every time a task is completed. That would eliminate the need to remember to run the ‘Complete For Prerequisite’ script, which would be great. In the meantime, I have also written a ‘Check Prerequisites’ script as a back-up. I run this daily as part of a ‘New Day’ script. It checks all the tasks that are waiting and, if all their dependent tasks have been completed, makes them available.

The Scripts

I have written this as an OmniFocus plugin which can be downloaded from here. In case you are curious I have included the scripts below, but if you like you can skip these and head down to ‘Set-Up’ below.

Add Prerequisite

var _ = (function() {
	var action = new PlugIn.Action(function(selection, sender) {
		config = this.dependencyConfig;

		// configure tags
		markerTag = config.markerTag();
		prerequisiteTag = config.prerequisiteTag();
		dependantTag = config.dependantTag();

		task = selection.tasks[0] || selection.projects[0].task;

		// GET PREREQUISITE
		// get all tasks tagged with 'prerequisite'
		prereqTask = markerTag.tasks[0];
		prereqTaskId = prereqTask.id.primaryKey;

		// DEAL WITH SELECTED (DEPENDENT) NOTE
		task.addTag(dependantTag); // add waiting tag to selected note
		task.note =
			"[PREREQUISITE: omnifocus:///task/" +
			prereqTaskId +
			"] " +
			prereqTask.name +
			"\n\n" +
			task.note; // prepend prerequisite details to selected note

		if (task.project !== null) {
			task.project.status = Project.Status.OnHold;
		}

		// DEAL WITH PREREQUISITE TASK
		prereqTask.addTag(prerequisiteTag); // add tag to prerequisite
		prereqTask.note =
			"[DEPENDANT: omnifocus:///task/" +
			task.id.primaryKey +
			"] " +
			task.name +
			"\n\n" +
			prereqTask.note; // prepend dependant details to prerequisite note
		prereqTask.removeTag(markerTag); // remove marker tag used for processing;
	});

	action.validate = function(selection, sender) {
		return (
			(selection.tasks.length === 1 || selection.projects.length == 1) &&
			this.dependencyConfig.markerTag().tasks.length == 1
		);
	};

	return action;
})();
_;

Complete For Prerequisite

var _ = (function() {
	var action = new PlugIn.Action(function(selection, sender) {
		// if called externally (from script) generate selection object
		if (typeof selection == "undefined") {
			selection = document.windows[0].selection;
		}

		task = selection.tasks[0] || selection.projects[0].task;

		// mark the task as complete
		task.markComplete();

		// check dependants
		this.dependencyLibrary.checkDependantsForTaskAndAncestors(task);
	});

	action.validate = function(selection, sender) {
		return selection.tasks.length === 1 || selection.projects.length === 1;
	};

	return action;
})();
_;

Check Prerequisites

var _ = (function() {
	var action = new PlugIn.Action(function(selection, sender) {
		// config
		config = this.dependencyConfig;
		dependantTag = config.dependantTag();
		prerequisiteTag = config.prerequisiteTag();

		dependencyLibrary = this.dependencyLibrary;

		// get all remaining tasks that are waiting on prerequisites
		remainingTasks = [];
		dependantTag.tasks.forEach(function(task) {
			if (task.taskStatus === Task.Status.Blocked) {
				remainingTasks.push(task);
			}
		});

		// for each task that is waiting:
		remainingTasks.forEach(function(dependentTask) {
			// use regex to find [PREREQUISITE: taskid] matches in the notes and capture task IDs
			regex = /\[PREREQUISITE: omnifocus:\/\/\/task\/(.+)\]/g;
			var regexArray = [];
			prerequisiteTasksArray = [];
			while ((regexArray = regex.exec(dependentTask.note)) !== null) {
				// for each captured task ID
				prerequisiteTaskId = regexArray[1];
				// get the task with that ID and push to array
				prerequisiteTag.tasks.forEach(function(task) {
					if (task.id.primaryKey == prerequisiteTaskId) {
						prerequisiteTasksArray.push(task);
						return ApplyResult.Stop;
					}
				});
			}

			// or each prerequsite task that has been captured
			prerequisiteTasksArray.forEach(prerequisiteTask => {
				dependencyLibrary.checkDependants(prerequisiteTask);
			});
		});
	});

	action.validate = function(selection, sender) {
		return true;
	};

	return action;
})();
_;

Functions Library

This is used to perform the checks in the ’Completed For Prerequisite’ and ‘Check Prerequisites’ scripts.

var _ = (function() {
	var dependencyLibrary = new PlugIn.Library(new Version("1.0"));

	dependencyLibrary.dependantTag = function() {
		return PlugIn.find("com.KaitlinSalzke.DependencyForOmniFocus")
			.library("dependencyConfig")
			.dependantTag();
	};

	dependencyLibrary.checkDependants = task => {
		dependantTag = dependencyLibrary.dependantTag();

		//get task ID of selected task
		var prerequisiteTaskId = task.id.primaryKey;
		var prerequisiteTask = task;

		if (prerequisiteTask.completed) {
			// use regex to find [DEPENDANT: taskid] matches in the notes and capture task IDs
			regex = /\[DEPENDANT: omnifocus:\/\/\/task\/(.+)\]/g;
			var regexArray = [];
			while ((regexArray = regex.exec(prerequisiteTask.note)) !== null) {
				// for each captured task ID
				dependantTaskId = regexArray[1];
				// get the task with that ID
				var dependantTask = null;
				dependantTag.tasks.forEach(function(task) {
					if (task.id.primaryKey == dependantTaskId) {
						dependantTask = task;
						return ApplyResult.Stop;
					}
				});

				// remove the prerequisite tag from the dependant task
				regexString =
					"[PREREQUISITE: omnifocus:///task/" + prerequisiteTaskId + "].+";
				RegExp.quote = function(str) {
					return str.replace(/([?*^$[\]\\(){}|-])/g, "\\$1");
				};
				regexForNoteSearch = new RegExp(RegExp.quote(regexString));
				dependantTask.note = dependantTask.note.replace(regexForNoteSearch, "");
				// check whether any remaining prerequisite tasks listed in the note
				// (i.e. whether all prerequisites completed) - and if so
				if (!/\[PREREQUISITE:/.test(dependantTask.note)) {
					// if no remaining prerequisites, remove 'Waiting' tag from dependant task
					// (and if project set to Active)
					dependantTask.removeTag(dependantTag);
					if (dependantTask.project !== null) {
						dependantTask.project.status = Project.Status.Active;
					}
				}
			}
		}
	};

	dependencyLibrary.getParent = task => {
		parent = null;
		if (task.containingProject == null) {
			project = inbox;
		} else {
			project = task.containingProject.task;
		}
		project.apply(item => {
			if (item.children.includes(task)) {
				parent = item;
				return ApplyResult.Stop;
			}
		});
		return parent;
	};

	dependencyLibrary.checkDependantsForTaskAndAncestors = task => {
		// get list of all "parent" tasks (up to project level)
		listOfTasks = [task];
		parent = dependencyLibrary.getParent(task);
		while (parent !== null) {
			listOfTasks.push(parent);
			parent = dependencyLibrary.getParent(parent);
		}

		// check this task, and any parent tasks, for dependants
		listOfTasks.forEach(task => {
			dependencyLibrary.checkDependants(task);
		});
	};

	return dependencyLibrary;
})();
_;

Set-Up

  1. Create the three tags identified above, as sub tasks of a tag named ‘Dependency’ (in the ‘Solution’ section of this post). The dependant task tag should be set to ‘on hold’ to reflect the fact that you can’t make any progress on these items until the prerequisite task has been completed.
  2. Add the plugin file linked above to your OmniFocus plugin directory. If you need some more instructions on doing this, you can refer to this post.
  3. If your tag set-up is different to mine, you will need to alter the config script to reflect that. To do so, right click on the plugin file and click ‘Show Package Contents’, then navigate to ‘Resources/dependencyConfig.js’. Open the file in a text editor to update the tag names as indicated by the comments in the file. (To refer to a top-level tag, use tagNamed("Name"). If your tag is nested other other tags, you chain these together for as many levels as you need e.g. tagNamed("Activity Type").tagNamed("Activity Type: ⏳ Waiting").tagNamed("🔒 Other task")

Usage Notes

To make one task a prerequisite of another:

  1. Tag the prerequisite task with ‘Make Prerequisite’.
  2. Select the dependant task.
  3. Run the ‘Add Prerequisite’ Omni Automation Script.

Instead of a task complete, you can run the ‘Complete For Prerequisite’ script. (I have assigned a keyboard shortcut to make this easier)

If you would like to check for any dependant tasks at any other time, you can use the functions checkDependants or checkDependantsForTaskAndAncestors from the library file in your scripts.

Optionally, run the ‘Check Prerequisites’ script as a backup, perhaps as part of a daily or weekly review.

Update 2019-09-23: I have now “upgraded” these series of scripts into a single plugin for easier use. In addition, I have fixed a number of small issues and added some functionality: the actions now work with projects (not just tasks) and the “Complete For Prerequisite” script now also checks the action groups and project the task is contained in to see if they are prerequisites themselves and then determine whether there are any new dependant tasks available. The tags used have also been changed slightly. The information above has been updated to reflect these changes.

5 thoughts on “Omni Automation: Prerequisites & Task Dependencies”

  1. Great script ideas! I look forward to using such approaches to run automation on macOS and iOS.

    I have two thoughts, with a pardon up front because I am not conversant in the syntax or best practices of the scripting language. First, I see that the tags that are used are exposed at the start of the function calls. Would they be better placed as globals external to the function call? I am used to seeing such globals in pubic-distributions of the AppleScripts for OmniFocus. Secondly, how do the scripts handle cases where the note field already has content? For example, I have an AppleScript that populates the note field with a URL callback. This allows me to link projects or tasks to external content (in the case, figures associated with the project that are located on Kanban boards in Curio). Will your script respect the pre-existing note field content?

    My second question prompts me to think over my own approach in using the note field to hold URL callbacks. I have to say that at some point, I must wonder whether we will have to request that OmniGroup include user-defined meta fields for projects or tags. That way, we can avoid programming to handle cases where different scripts set/reset/delete the entire note field because they think it is entirely their own private meta-container. Alternatively, this question may give pause to think about the utility and eventual need for <meta=taskdependencies> container designations to surround the script content within the note field itself so as to restrict the potential destructive actions of scripts to their own boxes.

    1. Thanks Jeffrey! I am very excited by the possibilities that Omni Automation affords. It’s already very powerful and I suspect the Omni team still have a lot they would like to add. (I hope so!)

      In response to your first question: you are possibly right. I am a Javascript amateur myself (accountant by day; wannabe coder by night), so it’s more than likely that the code is imperfect. It may be the case that it would be best practice to place them outside the function call. I should do some experimentation and research on this and adjust if needed. (I actually would ideally put these in a config file separately too but I’m experiencing some inconsistency with getting that to work, so I need to play around with that a bit more.)

      Re the second question: I also like to use URLs (and occasionally actual notes) in the notes field, so the scripts add the [DEPENDANT:] or [PREREQUISITE:] tags to the beginning of the note and should leave everything else in tact. (You might, if anything, end up with some extra white space when a prerequisite task is marked as completed and the tag deleted from the dependent task—-the regular expressions might need a little tweaking in this regard.)

      I think that would be an interesting addition to OmniFocus and I think you make a valid point regarding the potentially destructive nature of using the notes field in this way and the possible options. For me I have chosen this format and will probably stick with it—-because at this stage I think it is unlikely to conflict with other scripts. [PREREQUISITE: omnifocus:///task/taskID] (or similar) is not something I expect to come up naturally in any of my notes in the near future!

      1. I find that using a header-type configuration is generally the easiest approach to maintain in scripts that I know may end up mostly in the hands of technically-knowledgeable users. It does however open the door to the potential for non-tech-inclined users to munge the code when all they wanted was to change a preference. The balance is that it avoids having to manage yet another file in addition to the files that are purely the code base.

        I like your approach to use [META-NAME: …] as a container for script variables in the notes field in OmniFocus. I will have to “borrow” it. I see an immediate benefit in my AppleScript that links Curio OmniFocus with URLs in the OF note field. It will allow me to avoid having to wipe the note field clean each time I make a link.

        As a colleague in another forum said, you have a text-parsing problem and decide to use REGEX to solve it. Congratulations, now you have two problems.

        1. I think I will still need to have configuration in the header that links to the config file, so I think as a compromise I will likely end up using this but with some configuration information/example settings commented out. I’m starting to use the same tags between scripts and I’d like to minimise fiddling if I decide to change the nesting or the name or something in my OF database.

          Borrow away!

    2. Incidentally, I’ve just done some quick testing and moving the tags used outside of the function calls throws an error and breaks the script. So, I think I’ll stick with leaving them where they are for now!

Leave a Reply