- Published
- 8 December 2020
- Tagged
I can't believe Omni-automation has been out for so long and I haven't talked about it.
The tl;dr of the site is that Omni Group recently (as in six months ago) supercharged the automation capability of all their apps (OmniFocus, OmniGraffle, OmniOutliner, OmniPlan), by giving them a really robust Javascript Core framework. That sentence will either send you to sleep, or make you quite excited, and that reaction will tell you whether you care an inch for this post or not.
With your expectations set, let's see how I can develop a really simple Javascript plugin for OmniFocus in about half an hour.
Scope everything
I think every GTDer has their own complex relationship with contexts (which OmniFocus now implements as "tags"). These days, one of my best uses for context is dividing tasks into various "scopes", those being:
- Scope : Mosquito: a tiny task it would take you about thirty seconds to complete. It's often handy to bundle these up and do a bunch of them at once.
- Scope : Evening: a task low-energy enough that I can do it of an evening, after I get home from work.
- Scope : Weekend: a task big enough that I need a couple of hours during daylight for it. This is the kind of thing I should reserve for the weekend.
- Scope : No-scope: a task that doesn't really have a scope. These days I queue up some monthly goals as tasks, for example - they don't actually have a scope.
These have a nice functional feel to them: if I find myself with a spare evening, I can quickly shuttle across to evening-scoped tasks to see what I should be getting on with; conversely, I can ensure the big stuff doesn't clutter up my to-do list until the weekend.
The problem, however, is that I have no way of checking which tasks aren't scoped. While OmniFocus' perspectives are very powerful, they have no capacity for identifying tasks which don't have one or more tags.
So how do we make sure every task has a scope? The solution is to build a script which:
- Check there's such a tag as Scope : Unknown
- Iterates through all the tasks in OmniFocus
- Checks to see if they have a tag whose parent is Scope
- If they don't, assigns them the tag Scope : Unknown
Back when Applescript was our only tool, I'd shudder to think of the steps involved in building a script like this. With Javascript Core automation, though, it's a breeze.
Setting up
Before we start, you'll need a copy of OmniFocus which supports Omni Automation. I don't know whether this is supported with the OmniFocus Standard licence - you may need to have a Pro licence for this one to work. You will be able to tell if your copy of OmniFocus supports automation by checking OmniFocus' menu bar: the Automation menu item should be up there, three from the end. If so, you're good to go.
To get started, you'll need to locate the place where OmniFocus automation scripts are stored on your machine. Thankfully, that's easy to do: select Automation > Plug-Ins..., and you'll find yourself faced with a window showing the locations of your script folders. I get two, for what it's worth: one for scripts on my local machine, and one for scripts stored in iCloud (which can be synced with OmniFocus for iOS, if you're using OmniFocus on your iOS device).
You can reveal either of these folders (by right-clicking on the heading and selecting Reveal in Finder), but you can also create a plugin by clicking the + sign next to each folder. To start with, a one-file plugin is fine.
Building the basic plugin
If you make a new plugin through the Plug-Ins window, you'll get a file that looks something like the below:
/*{
"author": "Author Name",
"targets": ["omnifocus"],
"type": "action",
"identifier": "com.mycompany.foobar",
"version": "0.1",
"description": "A plug-in that...",
"label": "foobar",
"mediumLabel": "foobar",
"paletteLabel": "foobar",
}*/
(() => {
var action = new PlugIn.Action(function(selection) {
// Add code to run when the action is invoked
console.log("Invoked with selection", selection);
});
// If needed, uncomment, and add a function that returns true if the current selection is appropriate for the action.
/*
action.validate = function(selection){
};
*/
return action;
})();
This file consists of:
- A JSON-formatted header, surrounded by comment (
/*...*/
) indicators. This is our metadata for the plugin. - A function, in which...
- We create a plug-in with an action, and
- We (optionally) provide a
validate
block, which tells OmniFocus when it should allow you to run this plug-in. This allows you to grey out an action unless a task is selected, for example.
The metadata header is, frankly, pretty boring. The real meat of the plugin is in the Javascript below.
The action itself - what happens when we run the plugin - is represented by the Javascript function we provide as the first argument to the PlugIn
object. It receives one argument - the current OmniFocus selection - and is not expected to return anything.
Now I know what you're thinking to yourself: "How do I do stuff in OmniFocus inside of this Javascript?". Well, the good news is that OmniFocus' structure of projects and tasks is well-represented in here through a variety of objects and functions. For example, this code will output the name of each one of your projects to the automation console (accessible by selecting Automation > Show Console):
flattenedProjects.forEach(e => console.log(e.name))
And yes, you can just run this code in the console and see it do its stuff. You'll also notice a button in the top-right of your console: API Reference (also accessible through the Automation menu). OmniFocus keeps its functions and objects close-to-hand for you.
(In fact, it's actually easier to write your plug-in in the console, and then copy it into your plug-in file, than to write and test it in the plug-in file itself. This is because OmniFocus will only reload plug-ins when you relaunch the app.)
Right, let's work our way through our plan, then.
1. Check there's such a tag as Scope : Unknown
My normal way of dealing with things in automation systems like AppleScript is:
- Make some assumptions about how stuff is ordered, and how we can access it in code.
- Try this out.
- Work out why it fails.
- Iterate until it works.
The better the automation language (in my head), the less time I spend on steps three and four. The new Omni Automation framework is surprisingly good: I figure the best way to find the Scope : Unknown tag is to check that both the top-level Scope tag exists, and that it has an Unscoped sub-tag. With a little consulting of the API[1]:
let scopeTag = tagNamed("Scope")
let scopeUnknownTag = (scopeTag && scopeTag.tagNamed("Unknown"))
Amazingly, this works right off the bat! That's OK, I figure I'll hit issues in the next steps where things get more complex.
2. Iterate through all the tasks in OmniFocus
OK, looping was always an issue in AppleScript. Oh, sure, it should be as simple as a for each item in itemlist...
, but it never was. Thankfully, we have JavaScript to fall back on here:
flattenedTasks.forEach(task => {...})
Nice and easy!
3. Check to see if they have a tag whose parent is Scope
This is somewhat harder. There's no obvious tag.parentTag()
method, so we will probably need to assemble an array of Scope
's children. Still, that's doable. Following this, we need to check to see if a task has any of these Scope
tags. We can do this by filter
ing the tags, and seeing if any pass:
let scopeChildren = scopeTag.children
flattenedTasks.forEach(task => {
let taskIsScoped =
(task.tags.filter(tag => scopeChildren.indexOf(tag) > -1).length > 0)
})
4. If they don't, assign them the tag Scope : Unknown
So! We've identified our "Unknown Scope" tag (the one we want to apply to our wayward tasks), and we've found every task we should apply it to. How do we actually do the applying? Following a common theme: more easily than expected. Each task
object has an addTag()
function, which does exactly what you'd expect:
if (!taskIsScoped) {
task.addTag(scopeUnknownTag)
}
Putting it all together
Here's the whole thing. I've thrown in some guards so you don't error out if the script can't find the right tags. As a bonus, it will also tell you how many tasks it's scoped for you.
let scopeTag = tagNamed("Scope")
let scopeUnknownTag = (scopeTag && scopeTag.tagNamed("Unknown"))
let numberOfScopedTasks = 0
if (scopeTag && scopeUnknownTag) {
let scopeChildren = scopeTag.children
flattenedTasks.forEach(task => {
let taskIsScoped =
(task.tags.filter(tag => scopeChildren.indexOf(tag) > -1).length > 0)
if (!taskIsScoped) {
task.addTag(scopeUnknownTag)
numberOfScopedTasks = numberOfScopedTasks + 1
}
})
(new Alert("Tasks scoped", `I have assigned scope to ${numberOfScopedTasks} task(s).`)).show()
} else {
(new Alert("Cannot scope tasks", "I cannot find your 'Scope : Unknown' tag!")).show()
}
And that's it! In fact, the hardest bit is putting this into the JavaScript file you created for the plugin in the first place. This action can run whenever, so you can safely remove the action.validate
assignment.
Once you've saved your plugin, you can restart OmniFocus and should be able to select this plugin from the Automate menu. Click it, and watch it sort your tasks.
It's worth mentioning here that all this takes place within the context of an object of class
Database
. If you check out theDatabase
class in the API, all the functions and objects shown there are accessible to you by default within Omni Automation. ↩︎