Can you use Applescript to set a notification?

Hi,

I had a script that would automatically generate a Reminder in Apple’s Reminder program but I have recently ditched it because Applescript seems to have such difficulty accessing Reminders. Completion time is usually on the order of a minute or two (rest of the script runs in a few seconds).

I converted the script to set up a new task in Omnifocus 3, but looking through the dictionary and using Script Debugger’s ‘Explorer’ feature to grok the scripting dictionary, I can’t seem to find a way to set a notification. You can set repititions but not seeing anything for notifications. I need to use applescript to set the task to “notify me 15 minutes prior to task being due”. Is this possible?

TIA

Vince

@vinnie-bob8419 By chance, did you ever come up with a solution for this problem (re: set notifications for selected tasks)? Thanks!

What are you specifically trying to do, @jasondm007 ?

Specifically, the applescript in question submits a travel mileage reimbursement form and after it does so, sets an event in Omnifocus. When I used Reminders to do this, Applescript was also able to set a date to notify me on pay day to remember to check my paycheck to see if the reimbursement was added. Applescript grammar for reminders included explicit terminology for a ‘notification’, which tricked me into looking for the same thing in OF. It finally occurred to me that I don’t have to explicitly set a notification with OF, just need a due date. These things always become clearer…after you post a stupid question asking about it. 😂

@unlocked2412 I am incredibly sorry for the slow response. Can you even call it “slow” if it’s two years late? I don’t know how in the world I managed to miss your message for so long, but I’m really sorry.

In any case, I was looking for a way to set the notifications of selected tasks. For example, with meetings, I often like to have the same notification intervals (e.g., Before Due > 15 minutes before & 1 day, etc.). I was hoping to set the intervals quickly with a script, so I don’t have to do all that clicking. These tasks all have due dates, so the notification would relative to that date/time.

Thanks for any help you lend!

No worries :-)

Would you like to use OmniJS to accomplish it ? Are you using iOS, Mac or both ?

I see there is a way using that API. In the website, I see a promising method starting with version 3.8.

  • addNotification(info: Number or Date) → (Task.Notification) • (v3.8) Add a notification from the specification in info. Supplying a Date creates an absolute notification that will fire at that date. Supplying a Double (number) will create a due-relative notification. Specifying a due relative notification when this task’s effectiveDueDate is not set will result in an error.

@unlocked2412 Yeah, I use both the iOS and Mac versions - though I’d say I probably use the Mac version about 95% of the time.

I usually rely on AppleScripts for these things, but I couldn’t find anything about notifications in the OmniFocus library. However, I can run scripts just about any way, since I always use Alfred to trigger them.

I’m no programmer, but I’m guessing that OmniJS is some kind of javascript version for OmniFocus? Thanks!

First sketch of a OmniJS (cross-platform) PlugIn that adds a notification 15 minutes before the due date of selected tasks. Is this what you had in mind, @jasondm007 ?

/*{
	"type": "action"
}*/
// Twitter: @unlocked2412
(() => Object.assign(
    new PlugIn.Action(selection => {

        // OMNI JS CODE ---------------------------------------
        const omniJSContext = () => {
            // main :: IO ()
            const main = () => {
                const
                    ts = selection.tasks,
                    mins = 15 * 60 * 1000,
                    dates = ts.map(x => new Date(
                        x.dueDate - mins
                    ));
                return zipWith(task => dte => {
                    return (
                        task.addNotification(dte),
                        task
                    )
                })(ts)(dates)
            };


            // FUNCTIONS --
            // JS Prelude --------------------------------------------------
            // Just :: a -> Maybe a
            const Just = x => ({
                type: 'Maybe',
                Nothing: false,
                Just: x
            });

            // Nothing :: Maybe a
            const Nothing = () => ({
                type: 'Maybe',
                Nothing: true,
            });

            // Tuple (,) :: a -> b -> (a, b)
            const Tuple = a =>
                b => ({
                    type: 'Tuple',
                    '0': a,
                    '1': b,
                    length: 2
                });

            // apply ($) :: (a -> b) -> a -> b
            const apply = f =>
                // Application operator.
                x => f(x);

            // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
            const compose = (...fs) =>
                // A function defined by the right-to-left
                // composition of all the functions in fs.
                fs.reduce(
                    (f, g) => x => f(g(x)),
                    x => x
                );

            // fst :: (a, b) -> a
            const fst = tpl =>
                // First member of a pair.
                tpl[0];

            // length :: [a] -> Int
            const length = xs =>
                // Returns Infinity over objects without finite
                // length. This enables zip and zipWith to choose
                // the shorter argument when one is non-finite,
                // like cycle, repeat etc
                'GeneratorFunction' !== xs.constructor.constructor.name ? (
                    xs.length
                ) : Infinity;

            // list :: StringOrArrayLike b => b -> [a]
            const list = xs =>
                // xs itself, if it is an Array,
                // or an Array derived from xs.
                Array.isArray(xs) ? (
                    xs
                ) : Array.from(xs || []);

            // 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 => [...xs].map(f);

            // min :: Ord a => a -> a -> a
            const min = a => b => b < a ? b : a;

            // snd :: (a, b) -> b
            const snd = tpl =>
                // Second member of a pair.
                tpl[1];

            // take :: Int -> [a] -> [a]
            // take :: Int -> String -> String
            const take = n =>
                // The first n elements of a list,
                // string of characters, or stream.
                xs => 'GeneratorFunction' !== xs
                .constructor.constructor.name ? (
                    xs.slice(0, n)
                ) : [].concat.apply([], Array.from({
                    length: n
                }, () => {
                    const x = xs.next();
                    return x.done ? [] : [x.value];
                }));

            // uncons :: [a] -> Maybe (a, [a])
            const uncons = xs => {
                // Just a tuple of the head of xs and its tail, 
                // Or Nothing if xs is an empty list.
                const lng = length(xs);
                return (0 < lng) ? (
                    Infinity > lng ? (
                        Just(Tuple(xs[0])(xs.slice(1))) // Finite list
                    ) : (() => {
                        const nxt = take(1)(xs);
                        return 0 < nxt.length ? (
                            Just(Tuple(nxt[0])(xs))
                        ) : Nothing();
                    })() // Lazy generator
                ) : Nothing();
            };

            // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
            const zipWith = f =>
                // Use of `take` and `length` here allows zipping with non-finite lists
                // i.e. generators like cycle, repeat, iterate.
                xs => ys => {
                    const n = Math.min(length(xs), length(ys));
                    return Infinity > n ? (
                        (([as, bs]) => Array.from({
                            length: n
                        }, (_, i) => f(as[i])(
                            bs[i]
                        )))([xs, ys].map(
                            compose(take(n), list)
                        ))
                    ) : zipWithGen(f)(xs)(ys);
                };

            // zipWithGen :: (a -> b -> c) -> 
            // Gen [a] -> Gen [b] -> Gen [c]
            const zipWithGen = f => ga => gb => {
                function* go(ma, mb) {
                    let
                        a = ma,
                        b = mb;
                    while (!a.Nothing && !b.Nothing) {
                        let
                            ta = a.Just,
                            tb = b.Just
                        yield(f(fst(ta))(fst(tb)));
                        a = uncons(snd(ta));
                        b = uncons(snd(tb));
                    }
                }
                return go(uncons(ga), uncons(gb));
            };
            return main();
        };

        return omniJSContext()

    }), {
        validate: selection => 
            selection.tasks.every(
                x => null !== x.dueDate
            )
    }
))();
1 Like

You can paste it in a text editor and save it with .omnijs file extension. Then, place it in your OmniFocus iCloud Folder, for example.

It is the native API for OmniFocus. As you say, it is based in the JavaScript language. It is faster than Applescript and works on iOS and macOS.

1 Like

@unlocked2412 Wow, this is amazing! Thanks a ton!! While I don’t understand much of the language, your script, of course, works like a charm!

Two quick (and likely stupid) questions:

(1) Is it possible to make the notifications relative? While I don’t do it ton, if the user changes the due date, then they have to redo the notifications. It’s not that big of deal, but I was curious.

(2) Is it possible to add a loop - or whatever the equivalent of that is called in this language - so that it will add more than one notification (e.g., 15 minutes out, 1 day out, etc)? Or is it advisable to just create more than one of these plugins (w. different intervals)?

Thanks again for all of your help! This is amazing.

You’re welcome, @jasondm007.

Do you want to change the notifications based on an event — i.e. the user modifies the due date. Is that is the case, it is not currently possible. I heard it is planned, though.

Yes, it is possible. Does this script accomplish it ? Or, perhaps you had something else in mind ?

/*{
	"type": "action"
}*/
// Twitter: @unlocked2412
(() => Object.assign(
    new PlugIn.Action(selection => {

        // OMNI JS CODE ---------------------------------------
        const omniJSContext = () => {
            // main :: IO ()
            const main = () => {
                const
                    ts = selection.tasks,
                    xs = [15 * 60 * 1000, 24 * 60 * 60 * 1000],
                    dates = ts.map(x => x.dueDate),
                    notifs = dates.map(
                        dte => xs.map(x => new Date(dte - x))
                    );
                return zipWith(task => dts => {
                    return (
                        dts.map(x => task.addNotification(x)),
                        task
                    )
                })(ts)(notifs)
            };


            // FUNCTIONS --
            // JS Prelude --------------------------------------------------
            // Just :: a -> Maybe a
            const Just = x => ({
                type: 'Maybe',
                Nothing: false,
                Just: x
            });

            // Nothing :: Maybe a
            const Nothing = () => ({
                type: 'Maybe',
                Nothing: true,
            });

            // Tuple (,) :: a -> b -> (a, b)
            const Tuple = a =>
                b => ({
                    type: 'Tuple',
                    '0': a,
                    '1': b,
                    length: 2
                });

            // apply ($) :: (a -> b) -> a -> b
            const apply = f =>
                // Application operator.
                x => f(x);

            // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
            const compose = (...fs) =>
                // A function defined by the right-to-left
                // composition of all the functions in fs.
                fs.reduce(
                    (f, g) => x => f(g(x)),
                    x => x
                );

            // fst :: (a, b) -> a
            const fst = tpl =>
                // First member of a pair.
                tpl[0];

            // length :: [a] -> Int
            const length = xs =>
                // Returns Infinity over objects without finite
                // length. This enables zip and zipWith to choose
                // the shorter argument when one is non-finite,
                // like cycle, repeat etc
                'GeneratorFunction' !== xs.constructor.constructor.name ? (
                    xs.length
                ) : Infinity;

            // list :: StringOrArrayLike b => b -> [a]
            const list = xs =>
                // xs itself, if it is an Array,
                // or an Array derived from xs.
                Array.isArray(xs) ? (
                    xs
                ) : Array.from(xs || []);

            // 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 => [...xs].map(f);

            // min :: Ord a => a -> a -> a
            const min = a => b => b < a ? b : a;

            // snd :: (a, b) -> b
            const snd = tpl =>
                // Second member of a pair.
                tpl[1];

            // take :: Int -> [a] -> [a]
            // take :: Int -> String -> String
            const take = n =>
                // The first n elements of a list,
                // string of characters, or stream.
                xs => 'GeneratorFunction' !== xs
                .constructor.constructor.name ? (
                    xs.slice(0, n)
                ) : [].concat.apply([], Array.from({
                    length: n
                }, () => {
                    const x = xs.next();
                    return x.done ? [] : [x.value];
                }));

            // uncons :: [a] -> Maybe (a, [a])
            const uncons = xs => {
                // Just a tuple of the head of xs and its tail, 
                // Or Nothing if xs is an empty list.
                const lng = length(xs);
                return (0 < lng) ? (
                    Infinity > lng ? (
                        Just(Tuple(xs[0])(xs.slice(1))) // Finite list
                    ) : (() => {
                        const nxt = take(1)(xs);
                        return 0 < nxt.length ? (
                            Just(Tuple(nxt[0])(xs))
                        ) : Nothing();
                    })() // Lazy generator
                ) : Nothing();
            };

            // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
            const zipWith = f =>
                // Use of `take` and `length` here allows zipping with non-finite lists
                // i.e. generators like cycle, repeat, iterate.
                xs => ys => {
                    const n = Math.min(length(xs), length(ys));
                    return Infinity > n ? (
                        (([as, bs]) => Array.from({
                            length: n
                        }, (_, i) => f(as[i])(
                            bs[i]
                        )))([xs, ys].map(
                            compose(take(n), list)
                        ))
                    ) : zipWithGen(f)(xs)(ys);
                };

            // zipWithGen :: (a -> b -> c) -> 
            // Gen [a] -> Gen [b] -> Gen [c]
            const zipWithGen = f => ga => gb => {
                function* go(ma, mb) {
                    let
                        a = ma,
                        b = mb;
                    while (!a.Nothing && !b.Nothing) {
                        let
                            ta = a.Just,
                            tb = b.Just
                        yield(f(fst(ta))(fst(tb)));
                        a = uncons(snd(ta));
                        b = uncons(snd(tb));
                    }
                }
                return go(uncons(ga), uncons(gb));
            };
            return main();
        };

        return omniJSContext()

    }), {
        validate: selection => 
            selection.tasks.every(
                x => null !== x.dueDate
            )
    }
))();
1 Like

@unlocked2412 You rock! Honestly, I can’t thank you enough for taking the time to put this together. That’s incredibly kind of you.

The script works great! And, you’ve done such a great job annotating everything that even a programming Luddite, like myself, can tinker with it. Thanks!

Yeah, I’m not sure if I described it correctly. I guess it’s easiest for me to describe based on what I see in the inspector panel. Instead of assigning a specific date and time, I was just wondering if it was possible to assign a “Before Due” notification. So, for example, if you wanted a 5 minute out notification, the inspector displays “Before Due: 5 minutes” - which will be 5 minutes out, irrespective of whether the user changes the due date. With this script, however, it assigns a specific date and time. Honestly, it’s not that big of a deal, however. I don’t normally change the due dates of tasks that I assign notifications for, and I imagine that’s probably true for others, too.

Can’t thank you enough!! You’re the best.

You’re very welcome, @jasondm007. Glad it is useful for you.

Yes, of course. Something like this ?

Setting this via automation is possible. See:

  • DueRelative (Task.Notification.Kind r/o) • This notification fires at a time relative to its task’s due date.

I will look into this, @jasondm007.

Try this. Does it solve your problem ?

/*{
	"type": "action"
}*/
// Twitter: @unlocked2412
(() => Object.assign(
    new PlugIn.Action(selection => {

        // OMNI JS CODE ---------------------------------------
        const omniJSContext = () => {
            const main = () => {
                const
                    ts = document.windows[0].selection.tasks,
                    relativeOffsets = [-(15*60), -(24 * 60 * 60)];
                return map(task => {
                    return (
                        relativeOffsets.map(x => task.addNotification(x)),
                        task
                    )
                })(ts)
            };

            // FUNCTIONS --
            // JS Prelude --------------------------------------------------
            // Just :: a -> Maybe a
            const Just = x => ({
                type: 'Maybe',
                Nothing: false,
                Just: x
            });

            // Nothing :: Maybe a
            const Nothing = () => ({
                type: 'Maybe',
                Nothing: true,
            });

            // Tuple (,) :: a -> b -> (a, b)
            const Tuple = a =>
                b => ({
                    type: 'Tuple',
                    '0': a,
                    '1': b,
                    length: 2
                });

            // apply ($) :: (a -> b) -> a -> b
            const apply = f =>
                // Application operator.
                x => f(x);

            // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
            const compose = (...fs) =>
                // A function defined by the right-to-left
                // composition of all the functions in fs.
                fs.reduce(
                    (f, g) => x => f(g(x)),
                    x => x
                );

            // fst :: (a, b) -> a
            const fst = tpl =>
                // First member of a pair.
                tpl[0];

            // length :: [a] -> Int
            const length = xs =>
                // Returns Infinity over objects without finite
                // length. This enables zip and zipWith to choose
                // the shorter argument when one is non-finite,
                // like cycle, repeat etc
                'GeneratorFunction' !== xs.constructor.constructor.name ? (
                    xs.length
                ) : Infinity;

            // list :: StringOrArrayLike b => b -> [a]
            const list = xs =>
                // xs itself, if it is an Array,
                // or an Array derived from xs.
                Array.isArray(xs) ? (
                    xs
                ) : Array.from(xs || []);

            // 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 => [...xs].map(f);

            // min :: Ord a => a -> a -> a
            const min = a => b => b < a ? b : a;

            // snd :: (a, b) -> b
            const snd = tpl =>
                // Second member of a pair.
                tpl[1];

            // take :: Int -> [a] -> [a]
            // take :: Int -> String -> String
            const take = n =>
                // The first n elements of a list,
                // string of characters, or stream.
                xs => 'GeneratorFunction' !== xs
                .constructor.constructor.name ? (
                    xs.slice(0, n)
                ) : [].concat.apply([], Array.from({
                    length: n
                }, () => {
                    const x = xs.next();
                    return x.done ? [] : [x.value];
                }));

            // uncons :: [a] -> Maybe (a, [a])
            const uncons = xs => {
                // Just a tuple of the head of xs and its tail, 
                // Or Nothing if xs is an empty list.
                const lng = length(xs);
                return (0 < lng) ? (
                    Infinity > lng ? (
                        Just(Tuple(xs[0])(xs.slice(1))) // Finite list
                    ) : (() => {
                        const nxt = take(1)(xs);
                        return 0 < nxt.length ? (
                            Just(Tuple(nxt[0])(xs))
                        ) : Nothing();
                    })() // Lazy generator
                ) : Nothing();
            };
            return main();
        };

        return omniJSContext()

    }), {
        validate: selection => 
            selection.tasks.every(
                x => null !== x.dueDate
            )
    }
))();
1 Like

You’re the best, @unlocked2412!! This script/plugin is amazing! I can’t thank you enough. You’ve saved me so much mouse clicking!!!

YouRock

Good ! Glad to know this is useful for you, @jasondm007.

1 Like

Hi

First of all, thank you for a great script, but how to write several notifications in one script.

Notification before:
1 hour before
30 min before
Notification after:
1 hour after
3 hours after

is it possible for the script to run automatically or only manual press?

THANKS

To accomplish this, use a positive offset.

Quoting omni-automation.com:

Notifications Timed Relative to Task Due Date

To set a notification relative to the due data (before or after), provide a positive or negative integer indicating the number of seconds before (negative) or after (positive) to due date/time for the notification is to occur.

That is not currently possible, I think.

I’m very sorry, I probably didn’t explain clearly. Bad English))

How to make multiple notifications in 1.omnijs file? For example, before 30 minutes and before 1 hour …?

I tried to copy multiple notifications to the same code in a 1.omnijs file, but only 1 notification is running.

If I understand correctly, you would like to set 4 notifications on a specific manner:

Screenshot 2021-02-09 at 12.27.44

Perhaps, something like this ?

Update:

This Plug-In appears in the menu only if every selected task has a due date.

Source code (omniJS Plug-In):

/*{
	"type": "action"
}*/
// Twitter: @unlocked2412
(() =>
    Object.assign(
        new PlugIn.Action(selection => {
                // OMNI JS CODE ---------------------------------------
                const omniJSContext = () => {
                    // main :: IO ()
                    const main = () => {
                        const
                            ts = document.windows[0].selection.tasks,
                            min = 60,
                            hour = 60 * min,
                            day = 24 * hour,
                            relativeOffsets = [-(15 * min), -(1 * hour), 1 * hour, 3 * hour];
                        return ts.map(task => {
                            return (
                                relativeOffsets.map(x => task.addNotification(x)),
                                task
                            )
                        })
                    };

                    // FUNCTIONS --

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

                return omniJSContext()
            }

        ), {
            validate: selection => selection.tasks.every(
                x => null !== x.dueDate
            )
        })
)();
1 Like