I’d like to make an AppleScript that allows me to go through all tasks in OmniFocus, check if they are repeating, and then tag those that are repeating with the tag “Repeating”.
This will be useful for daily planning. I will use a perspective to look at all repeating actions across all of my projects to choose those that I certainly want to do tomorrow.
I’ve been trying to figure out how to do it myself but I’m new to AppleScript and at a loss.
Not that this is directly responsive to your question, but I solve for this by having a separate “repeating” project for each “normal” project, and sorting that project ABOVE the “normal” project in the projects list, so that with the same due date, importance, flagging, etc, they sort to the top of any perspective that has a mixture of one-time and repeating.
I also have daily, weekly and monthly repeating review meta-tasks that have sequential or parallel subtasks based on use, which reschedule themselves to the next day, or out a fixed period of time.
If you did that, you could create a perspective that only had the repeating task projects, which is kind of an interesting idea, actually.
Hi lainsw, thanks for your comment. I have this on top of my task list as well but I find that I often have repeating tasks threaded throughout my various projects as well—tasks that I’m not inclined to pull out and store at the top of my task list. This is why I want an AppleScript that lets me tag all repeating tasks, so that I can catch those that are inside different projects.
Others will know the interface better, but it looks as if this kind of thing might work:
(copy all the way down to mReturn)
-- Repeaters tagged
on run
set tagName to "repeater"
tell application "OmniFocus"
tell front document
if name of its tags does not contain tagName then
set oTag to make new tag with properties {name:tagName}
else
set oTag to first tag whose name is tagName
end if
script go
on |λ|(x)
set refTags to a reference to tags of x
if (name of refTags) does not contain tagName then
add oTag to end of refTags
end if
end |λ|
end script
my map(go, flattened tasks where its repetition rule is not missing value)
end tell
end tell
end run
-------------------- GENERIC FUNCTIONS --------------------
-- https://github.com/RobTrew/prelude-applescript
-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
-- The list obtained by applying f
-- to each element of xs.
tell mReturn(f)
set lng to length of xs
set lst to {}
repeat with i from 1 to lng
set end of lst to |λ|(item i of xs, i, xs)
end repeat
return lst
end tell
end map
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
-- 2nd class handler function lifted into 1st class script wrapper.
if script is class of f then
f
else
script
property |λ| : f
end script
end if
end mReturn
I suppose this is a framework you used to write the script so quickly?
Can you add check at the end where it goes through all “repeater” (variable name) tasks and then checks to make sure they’re all actually repeating tasks, and if not, removes the tag? That way, when I run it, I’m sure the list is up-to-date, without having to manually go check.
FYI, this took about 5-10 minutes to go through my entire (rather large) database. Is that expected?
How does it treat repeating subprojects that are set to “Complete with last action” that don’t have repeating tasks within them? Does it tag that repeating subproject? If so, will that show up in the tagged items? Does it tag every non-repeating task within it (going back now, it doesn’t seem to)? Can this be made a preference in the script?
It just finished running and gave me this result with commas in AppleScript. Is this expected?
Just a bunch of generic functions – it does make scripting faster.
make sure they’re all actually repeating tasks, and if not, removes the tag?
You could certainly do that. The simplest approach might just be to remove all such tags at the start (by deleting that tag type)
5-10 minutes to go through my entire (rather large) database. Is that expected?
I don’t have any recent experience of scripting OmniFocus, so I have no idea, but I imagine you could make it very much faster if you experiment with the omniJS interface, rather than using the much slower and older AppleScript interface.
How does it treat …
Your Q4 – I have no idea : -) I don’t use OmniFocus, so I would defer to expert opinion.
gave me this result with commas
The version below makes two changes.
Strips out all repeat tags at the beginning, and builds them afresh
Aims to give a more intelligible message at the end.
-- Repeaters tagged, with simple report
on run
set tagName to "repeater"
tell application "OmniFocus"
tell front document
if name of its tags contains tagName then
delete tag named tagName
end if
set oTag to make new tag with properties {name:tagName}
script go
on |λ|(a, x)
set refTags to a reference to tags of x
if (name of refTags) does not contain tagName then
add oTag to end of refTags
1 + a
else
a
end if
end |λ|
end script
set xs to flattened tasks where its repetition rule is not missing value
set intAdded to my foldl(go, 0, xs)
("Added " & intAdded as string) & space & tagName & " tags." & linefeed & ¬
"(Total of " & length of xs & " repeating tasks)"
end tell
end tell
end run
-------------------- GENERIC FUNCTIONS --------------------
-- https://github.com/RobTrew/prelude-applescript
-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
tell mReturn(f)
set v to startValue
set lng to length of xs
repeat with i from 1 to lng
set v to |λ|(v, item i of xs, i, xs)
end repeat
return v
end tell
end foldl
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
-- 2nd class handler function lifted into 1st class script wrapper.
if script is class of f then
f
else
script
property |λ| : f
end script
end if
end mReturn
I think I’ve probably reached the limits of my knowledge of OmniFocus – and I have to use someone else’s machine to experiment with, but perhaps others can comment on how that might be approached without slowing things down too much.
PS I don’t have a database to test with, but it’s possible that this (less cautious) version may be fractionally faster:
-- Repeaters tagged, with simple report, skipping a check
on run
set tagName to "repeater"
tell application "OmniFocus"
tell front document
if name of its tags contains tagName then ¬
delete tag named tagName
set oTag to make new tag with properties {name:tagName}
script go
on |λ|(a, x)
add oTag to end of tags of x
1 + a
end |λ|
end script
set xs to flattened tasks where its repetition rule is not missing value
set intAdded to my foldl(go, 0, xs)
("Added " & intAdded as string) & space & tagName & " tags." & linefeed & ¬
"(Total of " & length of xs & " repeating tasks)"
end tell
end tell
end run
-------------------- GENERIC FUNCTIONS --------------------
-- https://github.com/RobTrew/prelude-applescript
-- foldl :: (a -> b -> a) -> a -> [b] -> a
on foldl(f, startValue, xs)
tell mReturn(f)
set v to startValue
set lng to length of xs
repeat with i from 1 to lng
set v to |λ|(v, item i of xs, i, xs)
end repeat
return v
end tell
end foldl
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
-- 2nd class handler function lifted into 1st class script wrapper.
if script is class of f then
f
else
script
property |λ| : f
end script
end if
end mReturn
An alternative to @draft8 excellent Applescript, would be a cross-platform script OmniFocus OmniJS interface. If you have 3.8 version (public test starting today), you can try it.
You can run it from Script Editor (language tab at top left set to JavaScript). As OmniJS is available on iOS, it’s possible to execute the code inside omniJSContext in iPhone and/or iPad pasting it in Automation Console or it could be made a standalone plugin.
This version handles that specific request.
(() => {
'use strict';
// OMNI JS CODE ---------------------------------------
const omniJSContext = () => {
// main :: IO ()
const main = () => {
const strTag = 'repeater'
const
oldTag = tagNamed(strTag);
// Delete Tag from Database
if (oldTag !== null) {
deleteObject(oldTag)
}
const
oTag = new Tag(strTag)
// Tag every repeating task
return compose(
map(x => x.name),
flatten,
map(
tagTaskAndDescendants(
oTag
)(true)
),
filter(x => x.repetitionRule !== null)
)(flattenedTasks)
};
// OMNIFOCUS FUNCTIONS ------------------------------------
// addTag :: Tag Object -> OFItem -> OFItem
const addTag = oTag => item => {
item.addTag(oTag)
return item
}
// tagFoundOrCreated ::
const tagFoundOrCreated = strTag =>
tagNamed(strTag) || new Tag(strTag)
// tagTaskAndDescendants ::
const tagTaskAndDescendants = oTag => blnDesc => task =>
[task, ...(blnDesc ? task.flattenedChildren : [])]
.flatMap(addTag(oTag))
// GENERIC FUNCTIONS --------------------------------------------
// https://github.com/RobTrew/prelude-jxa
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
x => fs.reduceRight((a, f) => f(a), x);
// filter :: (a -> Bool) -> [a] -> [a]
const filter = f => xs => xs.filter(f);
// flatten :: NestedList a -> [a]
const flatten = nest => nest.flat(Infinity);
// ------------------------------------------------------------
// map :: (a -> b) -> [a] -> [b]
const map = f =>
// The list obtained by applying f
// to each element of xs.
// (The image of xs under f).
xs => (
Array.isArray(xs) ? (
xs
) : xs.split('')
).map(f);
// MAIN -----------------------------------------
return main()
};
// readFile :: FilePath -> IO String
const readFile = fp => {
const
e = $(),
ns = $.NSString.stringWithContentsOfFileEncodingError(
$(fp).stringByStandardizingPath,
$.NSUTF8StringEncoding,
e
);
return ObjC.unwrap(
ns.isNil() ? (
e.localizedDescription
) : ns
);
};
// omnifocusOmniJSWithArgs :: [FilePath] -> Function -> [...OptionalArgs] -> a
function omnifocusOmniJSWithoutLibs(f) {
return Application('OmniFocus')
.evaluateJavascript(
`(() => {
'use strict';
return (${f})(${Array.from(arguments)
.slice(2).map(JSON.stringify)});
})();`
);
};
// OmniJS Context Evaluation------------------------------------------------
return omnifocusOmniJSWithoutLibs(
omniJSContext
)
})();
As I said in the post, you need OmniFocus 3.8. The reason: .evaluateJavascript (used in the script) wasn’t included in previous versions. Also, .repetitionRule method is required to accomplish what you want.
@unlocked2412 Sorry about that. I missed that detail when reading as I assumed the beta release was for iOS versions only and that OmniJS was something that already existed on the current macOS version.
I just downloaded 3.8 Beta (public release) and got this error when running the script. Any ideas what might be happening?