Help: Tag all repeating tasks "Repeating" using AppleScript

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.

Does anybody think they can help me?

1 Like

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
1 Like

Amazing, thank you @draft8

Some questions:

  1. I suppose this is a framework you used to write the script so quickly?

  2. 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.

  3. FYI, this took about 5-10 minutes to go through my entire (rather large) database. Is that expected?

  4. 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?

  5. 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

1 Like

@draft8 Thank you for the update! This is very useful and I will use it during my weekly reviews to make sure that my recurring tasks are tagged.

The only thing it’s missing is tagging the tasks that are not themselves repeating but are sub-tasks to a repeating sub-project (see below).

Do you think you could help with that too?

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.

1 Like

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

@draft8 Alright, fair enough. Thank you. Maybe someone else can help.

Thanks for this. I will test it on a copy of my database.

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
	)
})();
2 Likes

@unlocked2412 Thank you. I did receive this error when trying to run it. Am I missing something?

I cannot reproduce that error. What OS and OF version are you using ?

Catalina 10.15.4
OmniFocus 3.6.4

I just copy/pasted your script into Script Editor and set it for JavaScript.

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.

2 Likes

That’s excellent – I just downloaded a test copy of OmniFocus 3.8 from https://omnistaging.omnigroup.com/omnifocus/ and tried your omniJS script with a toy database.

Much faster than AppleScript …

(and reaches parts which the AppleScript wasn’t able to)

2 Likes

@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?

No problem, @kud.

Sorry. I made an edit to the post. I will update it in just a minute.

1 Like

Just updated it. Tell me if it works for you.

1 Like

@unlocked2412 Worked! Incredible. Fast and does everything I wanted. Thank you for taking the time to help me.

P.S. How do I learn this power?

2 Likes