Automating the +1 for defer dates with Omni Automation

I have a few ideas I think would be easy to do with the new JavaScript automation but I don’t know where to start. My current favorite idea for an automation would take tasks with the defer date as the current date (or older) and tag them Today so that I don’t have to keep +1-ing them to see them in the Forecast day after day. Ideally the same automation would remove the tag if I rescheduled the defer date to a day in the future.

Another basic idea I have would include assigning tasks that have a defer/due date to a ‘Misc’. project if I don’t assign one.

Where would someone like me start if I wanted to build this? I don’t even know enough about the Omni implementation of JS to know if the scripts need to be triggered or if that can run at certain times of day, etc…

I have read the Big Picture at Omni-Automation.com and it helped a ton but it appears that the page specific to OmniFocus is in development. Any guidance on what’s next is appreciated.

Here is a rough draft that I think accomplishes what you want.

(() => {
    'use strict';

    // OMNI JS CODE ---------------------------------------
    const omniJSContext = () => {
        // main :: IO ()
        const main = () => {
            const
                todayTag = 'today',
                oTag = tagFoundOrCreated(todayTag);

            const p = strTag => x =>
                (isTodayOrPast(x.deferDate) && !hasTag(strTag)(x)) ||
                (!isTodayOrPast(x.deferDate) && hasTag(strTag)(x))

            return compose(
                splitArrow(
                    map(removeTag(oTag))
                )(map(addTag(oTag))),
                partition(x => !isTodayOrPast(x.deferDate)),
                filter(p(todayTag)),
                filter(x => x.deferDate !== null)
            )(flattenedTasks)
        };

        // OMNIFOCUS FUNCTIONS ----------------------------------------------
        // isTodayOrPast :: Date -> Bool
        const isTodayOrPast = date => {
            const todayDate = new Date()
            return (
                todayDate.setHours(0),
                todayDate.setMinutes(0),
                todayDate.setSeconds(0),
                todayDate.setDate(todayDate.getDate() + parseInt(1)),
                date <= todayDate
            )
        }

        // hasTag :: Tag Name -> OFItem -> Bool
        const hasTag = strTag => task =>
            elem(strTag)(
                map(
                    x => x.name
                )(task.tags)
            )

        // addTag :: Tag Object -> OFItem -> OFItem
        const addTag = oTag => item => {
            item.addTag(oTag)
            return item
        }

        // removeTag :: Tag Object -> OFItem -> OFItem
        const removeTag = oTag => item => {
            item.removeTag(oTag)
            return item
        }

        // tagFoundOrCreated :: Tag Name -> Tag Object
        const tagFoundOrCreated = strTag =>
            tagNamed(strTag) || new Tag(strTag)

        // GENERIC FUNCTIONS --------------------------------------------
        // https://github.com/RobTrew/prelude-jxa
        // Tuple (,) :: a -> b -> (a, b)
        const Tuple = a =>
            b => ({
                type: 'Tuple',
                '0': a,
                '1': b,
                length: 2
            });

        // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
        const compose = (...fs) =>
            x => fs.reduceRight((a, f) => f(a), x);

        // elem :: Eq a => a -> [a] -> Bool
        // elem :: Char -> String -> Bool
        const elem = x =>
            xs => {
                const t = xs.constructor.name;
                return 'Array' !== t ? (
                    xs['Set' !== t ? 'includes' : 'has'](x)
                ) : xs.some(eq(x));
            };

        // eq (==) :: Eq a => a -> a -> Bool
        const eq = a =>
            // True when a and b are equivalent in the terms
            // defined below for their shared data type.
            b => {
                const t = typeof a;
                return t !== typeof b ? (
                    false
                ) : 'object' !== t ? (
                    'function' !== t ? (
                        a === b
                    ) : a.toString() === b.toString()
                ) : (() => {
                    const kvs = Object.entries(a);
                    return kvs.length !== Object.keys(b).length ? (
                        false
                    ) : kvs.every(([k, v]) => eq(v)(b[k]));
                })();
            };

        // filter :: (a -> Bool) -> [a] -> [a]
        const filter = f => xs => xs.filter(f);

        // 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
                ) : Array.from(xs)
            ).map(f);

        // partition :: (a -> Bool) -> [a] -> ([a], [a])
        const partition = p => xs =>
            xs.reduce(
                (a, x) =>
                p(x) ? (
                    Tuple(a[0].concat(x))(a[1])
                ) : Tuple(a[0])(a[1].concat(x)),
                Tuple([])([])
            );

        // splitArrow (***) :: (a -> b) -> (c -> d) -> ((a, c) -> (b, d))
        const splitArrow = f =>
            // The functions f and g combined in a single function
            // from a tuple (x, y) to a tuple of (f(x), g(y))
            g => tpl => Tuple(f(tpl[0]))(
                g(tpl[1])
            );

        // MAIN -----------------------------------------
        return main()
    };

    // 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

Wow. Thank you so much for this. Looking through it, I can see how some the logic and Omni-flavored syntax are working, but I don’t think I would be able to build this on my own in any reasonable amount of time!

I went to the console and set up a new action and saved the script to the OmniFocus iCloud Drive folder. I copied and pasted your script to it and titled it ‘Snooze Deferred.’ The script shows up in iCloud Drive but the option from the Automation menu bar is greyed out and the console repeatedly gives me these messages:

TypeError: Application is not a function. (In ‘Application(‘OmniFocus’)’, ‘Application’ is an instance of CallbackObject) /Users/me/Library/Mobile Documents/iCloud~com~omnigroup~OmniFocus/Documents/Plug-Ins/Snooze Deferred.omnijs:162:27

at /Users/me/Library/Mobile Documents/iCloud~com~omnigroup~OmniFocus/Documents/Plug-Ins/Snooze Deferred.omnijs:162:27

Any idea where I went wrong?

Yes, the issue is the code you are pasting is meant to be executed in JS Context, whereas the plugin is being executing inside OmniJS Context. If you paste the function I called omniJSContext inside this function:

var action = new PlugIn.Action(function(selection) {
    // CODE GOES HERE
}

and call it with trailing parenthesis, as:

var action = new PlugIn.Action(function(selection) {
    // CODE GOES HERE
}
omniJSContext();

I polished default template a little bit. You can paste this code into a text editor, save it with .omnijs extension and move it to you Plug-Ins folder inside OmniFocus iCloud Drive folder.

Plug-In code:

/*{
    "type": "action",
    "label": "Script Name for Automation Menu"

}*/
(() => Object.assign(
    new PlugIn.Action(selection => {

        // main :: IO ()
        const main = () => {
            // OMNI JS CODE ---------------------------------------
            const omniJSContext = () => {
                // main :: IO ()
                const main = () => {
                    const
                        todayTag = 'today',
                        oTag = tagFoundOrCreated(todayTag);

                    const p = strTag => x =>
                        (isTodayOrPast(x.deferDate) && !hasTag(strTag)(x)) ||
                        (!isTodayOrPast(x.deferDate) && hasTag(strTag)(x))

                    return compose(
                        splitArrow(
                            map(removeTag(oTag))
                        )(map(addTag(oTag))),
                        partition(x => !isTodayOrPast(x.deferDate)),
                        filter(p(todayTag)),
                        filter(x => x.deferDate !== null)
                    )(flattenedTasks)
                };

                // OMNIFOCUS FUNCTIONS ----------------------------------------------
                // isTodayOrPast :: Date -> Bool
                const isTodayOrPast = date => {
                    const todayDate = new Date()
                    return (
                        todayDate.setHours(0),
                        todayDate.setMinutes(0),
                        todayDate.setSeconds(0),
                        todayDate.setDate(todayDate.getDate() + parseInt(1)),
                        date <= todayDate
                    )
                }

                // hasTag :: Tag Name -> OFItem -> Bool
                const hasTag = strTag => task =>
                    elem(strTag)(
                        map(
                            x => x.name
                        )(task.tags)
                    )

                // addTag :: Tag Object -> OFItem -> OFItem
                const addTag = oTag => item => {
                    item.addTag(oTag)
                    return item
                }

                // removeTag :: Tag Object -> OFItem -> OFItem
                const removeTag = oTag => item => {
                    item.removeTag(oTag)
                    return item
                }

                // tagFoundOrCreated :: Tag Name -> Tag Object
                const tagFoundOrCreated = strTag =>
                    tagNamed(strTag) || new Tag(strTag)

                // GENERIC FUNCTIONS --------------------------------------------
                // https://github.com/RobTrew/prelude-jxa
                // Tuple (,) :: a -> b -> (a, b)
                const Tuple = a =>
                    b => ({
                        type: 'Tuple',
                        '0': a,
                        '1': b,
                        length: 2
                    });

                // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
                const compose = (...fs) =>
                    x => fs.reduceRight((a, f) => f(a), x);

                // elem :: Eq a => a -> [a] -> Bool
                // elem :: Char -> String -> Bool
                const elem = x =>
                    xs => {
                        const t = xs.constructor.name;
                        return 'Array' !== t ? (
                            xs['Set' !== t ? 'includes' : 'has'](x)
                        ) : xs.some(eq(x));
                    };

                // eq (==) :: Eq a => a -> a -> Bool
                const eq = a =>
                    // True when a and b are equivalent in the terms
                    // defined below for their shared data type.
                    b => {
                        const t = typeof a;
                        return t !== typeof b ? (
                            false
                        ) : 'object' !== t ? (
                            'function' !== t ? (
                                a === b
                            ) : a.toString() === b.toString()
                        ) : (() => {
                            const kvs = Object.entries(a);
                            return kvs.length !== Object.keys(b).length ? (
                                false
                            ) : kvs.every(([k, v]) => eq(v)(b[k]));
                        })();
                    };

                // filter :: (a -> Bool) -> [a] -> [a]
                const filter = f => xs => xs.filter(f);

                // 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
                        ) : Array.from(xs)
                    ).map(f);

                // partition :: (a -> Bool) -> [a] -> ([a], [a])
                const partition = p => xs =>
                    xs.reduce(
                        (a, x) =>
                        p(x) ? (
                            Tuple(a[0].concat(x))(a[1])
                        ) : Tuple(a[0])(a[1].concat(x)),
                        Tuple([])([])
                    );

                // splitArrow (***) :: (a -> b) -> (c -> d) -> ((a, c) -> (b, d))
                const splitArrow = f =>
                    // The functions f and g combined in a single function
                    // from a tuple (x, y) to a tuple of (f(x), g(y))
                    g => tpl => Tuple(f(tpl[0]))(
                        g(tpl[1])
                    );

                // MAIN -----------------------------------------
                return main()
            };
            omniJSContext()
        };

        return main()
        
        }, {
            validate: selection => true
        })
))();

Tell me if it works for you.

1 Like

Any idea where I went wrong?

And if you would like to test the first (JXA context) version, the thing is just to paste it into macOS Script Editor (with the language selector at top left set to JavaScript), and run it from there (or perhaps from an Execute JavaScript for Automation action in a Keyboard Maestro macro)

(Rather than from the OF Automation console)

2 Likes

Thank you! This helps me understand a little bit more about the distinction between the syntax and procedure of a standard JS and the Omni implementation.

This is brilliant. I set up some defer dates yesterday, today, and tomorrow, and it worked on the first try. There isn’t a way to have it happen automatically during sync or cleanup, is there?

I cannot thank you enough. This will be a game changer for my workflow. I sincerely appreciate it and hope that it did not take you long.

You’re very welcome.

That is not currently possible.

You could set up a JavaScript For Automation Action and assign a shortcut to it, if you use Keyboard Maestro. There are other options for assigning a shortcut, though.

1 Like