Request: automate adding and removing tags to multiple tasks [Solved!]

Hello all. I am new to automation/scripting. I would like to learn how to create a script/Key Maistro/Alfred workflow to accomplish couple of simple tasks. What is the best way to do this? I started learning Apple Scripting and I think that is a little overkill to accomplish what I am trying to do.

So here is what I would like to do:

  1. Add a specific tag to a task or multiple selected tasks. (I want to implement a tag for Today, another for Tomorrow etc.)
  2. I want to be able to remove a specific tag or all tags from one or multiple tasks.

Can you please advice the best way to achieve this?

Thank you…

Did you know you could change multiple things at once?

I don’t know how to script add or taking away a tag. This conversation provides some clues: OmniFocus 3 - Script to remove “Today” tag and mark complete.

Request: Can anyone share the following AppleScript snippets:

  1. Add a tag to all selected items
  2. Remove a tage to all selected items.
  3. Add today due date to all selected items.

IF, someone is willing to do that, then you set up Keyboard Maestro in the following way:

2 Likes

Thank you so much for the response. I knew about the multiple tag editing. I would like to automate with a shortcut if possible. Thanks again and will keep an eye on the responses.

1 Like
// unlocked2412
// Set tags of selected tasks

(() => {
	'use strict';

	// main :: IO ()
	const main = () => {
		// USER DATA
		const listTags = ['Today', 'Errands', 'Home']

		const appOF = Application('OmniFocus')
		const lrSeln = ofSelectionLR()
		const ofTags = findTags(listTags)

		return isLeft(lrSeln) ? (
			lrSeln.Left
		) : lrSeln.Right.forEach(
			x => appOF.add(ofTags, {
				to: x.tags
			})
		)
	};

	// GENERIC -----------------------------------------------------------------
	// https://github.com/RobTrew/prelude-jxa
	// JS - Apps

	// findTags :: [String] -> [OF Tag]
	const findTags = xs => {
		const doc = Application('OmniFocus')
			.defaultDocument
		return rights(xs.map(x => {
			const tags = doc.flattenedTags.whose({
				name: x
			})
			return tags.length === 0 ? (
				Left('No Tags with name')
			) : Right(tags()[0])
		}));
	}

	// ofSelectionLR :: () -> Either [OF Task]
	const ofSelectionLR = () => {
		const
			appOF = Application('OmniFocus'),
			ds = appOF.documents;

		return bindLR(
			bindLR(

				// Documents ?
				ds.length === 0 ? (
					Left('No documents open')
				) : Right(ds[0].documentWindows),

				// Windows ?
				ws => ws.length === 0 ? (
					Left('No windows open')
				) : Right(ws[0].content.selectedTrees)
			),

			// Selection ?
			seln => seln.length === 0 ? (
				Left('No selection')
			) : Right(seln.value())
		)
	}

	// JS - Prelude

	// Left :: a -> Either a b
	const Left = x => ({
		type: 'Either',
		Left: x
	});

	// Right :: b -> Either a b
	const Right = x => ({
		type: 'Either',
		Right: x
	});

	// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
	const bindLR = (m, mf) =>
		m.Right !== undefined ? (
			mf(m.Right)
		) : m;

	// concatMap :: (a -> [b]) -> [a] -> [b]
	const concatMap = (f, xs) => [].concat.apply([], xs.map(f));

	// isLeft :: Either a b -> Bool
	const isLeft = lr =>
		lr.type === 'Either' && lr.Left !== undefined;

	// rights :: [Either a b] -> [b]
	const rights = xs =>
		concatMap(
			x => x.type === 'Either' && x.Right !== undefined ? (
				[x.Right]
			) : [], xs
		);

	// JXA MAIN ----------------------------------------------------------------
	return main();
})();
// unlocked2412
// Remove tags from selected tasks

(() => {
	'use strict';

	// main :: IO ()
	const main = () => {
		// USER DATA
		const listTags = ['Home']

		const appOF = Application('OmniFocus')
		const lrSeln = ofSelectionLR()
		const ofTags = findTags(listTags)

		return isLeft(lrSeln) ? (
			lrSeln.Left
		) : lrSeln.Right.forEach(
			x => appOF.remove(ofTags, {
				from: x.tags
			})
		)
	};

	// GENERIC -----------------------------------------------------------------
	// https://github.com/RobTrew/prelude-jxa
	// JS - Apps

	// findTags :: [String] -> [OF Tag]
	const findTags = xs => {
		const doc = Application('OmniFocus')
			.defaultDocument
		return rights(xs.map(x => {
			const tags = doc.flattenedTags.whose({
				name: x
			})
			return tags.length === 0 ? (
				Left('No Tags with name')
			) : Right(tags()[0])
		}));
	}

	// ofSelectionLR :: () -> Either [OF Task]
	const ofSelectionLR = () => {
		const
			appOF = Application('OmniFocus'),
			ds = appOF.documents;

		return bindLR(
			bindLR(

				// Documents ?
				ds.length === 0 ? (
					Left('No documents open')
				) : Right(ds[0].documentWindows),

				// Windows ?
				ws => ws.length === 0 ? (
					Left('No windows open')
				) : Right(ws[0].content.selectedTrees)
			),

			// Selection ?
			seln => seln.length === 0 ? (
				Left('No selection')
			) : Right(seln.value())
		)
	}

	// JS - Prelude

	// Left :: a -> Either a b
	const Left = x => ({
		type: 'Either',
		Left: x
	});

	// Right :: b -> Either a b
	const Right = x => ({
		type: 'Either',
		Right: x
	});

	// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
	const bindLR = (m, mf) =>
		m.Right !== undefined ? (
			mf(m.Right)
		) : m;

	// concatMap :: (a -> [b]) -> [a] -> [b]
	const concatMap = (f, xs) => [].concat.apply([], xs.map(f));

	// isLeft :: Either a b -> Bool
	const isLeft = lr =>
		lr.type === 'Either' && lr.Left !== undefined;

	// rights :: [Either a b] -> [b]
	const rights = xs =>
		concatMap(
			x => x.type === 'Either' && x.Right !== undefined ? (
				[x.Right]
			) : [], xs
		);

	// JXA MAIN ----------------------------------------------------------------
	return main();
})();
// unlocked2412
// Set due date of selected tasks to today

(() => {
	'use strict';

	// main :: IO ()
	const main = () => {
		const appOF = Application('OmniFocus')
		const lrSeln = ofSelectionLR()
		const todayDate = new Date()

		return isLeft(lrSeln) ? (
			lrSeln.Left
		) : lrSeln.Right.forEach(
			x => x.dueDate = todayDate
		)
	};

	// GENERIC -----------------------------------------------------------------
	// https://github.com/RobTrew/prelude-jxa
	// JS - Apps

	// ofSelectionLR :: () -> Either [OF Task]
	const ofSelectionLR = () => {
		const
			appOF = Application('OmniFocus'),
			ds = appOF.documents;

		return bindLR(
			bindLR(

				// Documents ?
				ds.length === 0 ? (
					Left('No documents open')
				) : Right(ds[0].documentWindows),

				// Windows ?
				ws => ws.length === 0 ? (
					Left('No windows open')
				) : Right(ws[0].content.selectedTrees)
			),

			// Selection ?
			seln => seln.length === 0 ? (
				Left('No selection')
			) : Right(seln.value())
		)
	}

	// JS - Prelude

	// Left :: a -> Either a b
	const Left = x => ({
		type: 'Either',
		Left: x
	});

	// Right :: b -> Either a b
	const Right = x => ({
		type: 'Either',
		Right: x
	});

	// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
	const bindLR = (m, mf) =>
		m.Right !== undefined ? (
			mf(m.Right)
		) : m;

	// isLeft :: Either a b -> Bool
	const isLeft = lr =>
		lr.type === 'Either' && lr.Left !== undefined;

	// JXA MAIN ----------------------------------------------------------------
	return main();
})();
4 Likes

This is amazing. Thank you so much for the response. I will be trying this soon…You guys rock…

1 Like

Thank you @unlocked2412 for sharing your code.

I am new to javascript and to JXA. So I thought that before I try and understand your code, why not write my own to toggle addition and removal of “Today” tag to the task selected and also the containing project.

The code below does appear to work, however I can do with some quick help / critical comments. First two below are in the code and related to script statements.

  1. // This returns an object. Is it an object because this is supposed to return an array of objects and so it is an object even if the length of projectTags is zero?
  2. // Q. why of.add and not oDoc.add, in Applescript this would be in tell default document block?
  3. Further, will appreciate any help and welcome any critical comments on the script structure in general.

I also realized that the script will toggle “Today” tag on the project twice if two tasks are selected with the same project. Will likely work on only one task at a time so that problem we can set aside for the time being.

Thank you!

function run() {
    const of = Application("OmniFocus");
    const oDoc = of .defaultDocument;
    const oWin = oDoc.documentWindows[0];
    const seln = oWin.content.selectedTrees.value();

    const validItems = seln.filter(x => {
        return ObjectSpecifier.classOf(x) == 'project' ||
            ObjectSpecifier.classOf(x) == 'task' || ObjectSpecifier.classOf(x) == 'inboxTask'
    });

    numberSelected = validItems.length;
    if (!numberSelected) {
        console.log("No valid task(s) selected");
        throw new Error("Nothing selected");
    };

    todayTags = oDoc.flattenedTags.whose({
        name: {
            _contains: 'Today'
        }
    })(); // this will result in an array of tag objects
    todayTag = todayTags[0]; //choosing the first element of array, to access the id

    for (myTask of validItems) {
        console.log(myTask.name());
        if (myTask.containingProject() && myTask.containingProject().id() != myTask.id()) {
            myProject = myTask.containingProject();
            console.log("Project is " + myProject.name() + " and id is: " + myProject.id() + " and myTask.id is: " + myTask.id());
            projectTags = myProject.tags(); // This returns an object. Is it an object because this is supposed to return an array of objects and so it is an object even if the length of projectTags is zero?
            console.log("Number of tags are: " + projectTags.length)

            projectTaggedWithToday = projectTags.filter(item => item.id() == todayTag.id());
            console.log(projectTaggedWithToday.length);

            if (!projectTaggedWithToday.length) {
                console.log("About to add Today to project.");
                of.add(todayTags, { // Q. why of.add and not oDoc.add, in Applescript this would be in tell default document block?
                    to: myProject.tags
                })
            } else {
                console.log("About to remove Today from project.");
                of.remove(todayTags, {
                    from: myProject.tags
                })
            }
        } else {
            console.log(myTask.name() + " does not have a project assigned.");
        }


        taskTags = myTask.tags(); // returns an array of tags
        // is task tagged with Today tag?
        taskTaggedWithToday = taskTags.filter(item => item.id() == todayTag.id());
        if (!taskTaggedWithToday.length) {
            console.log("About to add Today to task.");
            of.add(todayTags, {
                to: myTask.tags
            })
        } else {
            console.log("About to remove Today from task.");
            of.remove(todayTags, {
                from: myTask.tags
            })
        }

    };
}

@unlocked2412

Also, I have been working to understand your code but am unable to understand. Searched the Internet for Hoogle naming convention and rad a little about Haskell. Appears that naming convention seems to refer to type of input and type of output etc, but not sure why a and b used here for naming.

Was trying to follow your code where const lrSeln = ofSelectionLR() and then I go to ofSelectionLR() and I lose the flow at bindLR in the function ofSelectionLR. Specifically, I can see how ds is being referred as ds is defined, but ws is not defined but being referred to in your code.

Not asking you to hand hold, but if there is any pointers as to what one can read to understand your code and to leverage prelude-JXA that you have shared, would be great. Thanks!!

Yes, it returns an object; namely, the empty list [].

Both work in Applescript. I can’t give you a technical answer here. Perhaps, some scoping rule is taking place there. But we see that add is an application method. So, Application(‘OmniFocus’).add would be correct.

I see good ideas on your script. Perhaps useful would be to:

  • Separate construction of values from effects (console.log, displaying dialogs, …).
  • Whenever there is a need to apply some transformation to a collection, I.e. a list, trying to think what would be the transformation on one item and then generalize it.

Also, I would suggest making a script to add tags to selected tasks and then, when new concepts are settling down, try to experiment also with the objects obtained in .containingProject property.
So, in a functional way, I would approach the problem of adding a specific tag to selected tasks:

What is the effect I am after? The addition of a list of tag objects to a single task.

addTag :: [OF Tag] -> [OF Task]
const addTag = (oTag, oTask) => Application
    .('OmniFocus')
    .add(oTags, {
        to: oTask.tags
    })

What are the values I need to obtain? A filtered list of tasks.

onlyTasks :: [OF Item] -> [OF Task]
const onlyTasks = seln => 
    seln.filter(x => 
        ObjectSpecifier.classOf(x) == 'task'
    )

Now, I need to apply i.e. — map — that transformation — addTag — to my filtered list of tasks.

onlyTasks(seln).map(addTag)
1 Like

In Haskell, a type signature like map :: (a -> b) -> [a] -> [b] carries an implicit Universal Quantifier. Making it explicit,

map :: forall a b. (a -> b) -> [a] -> [b]

a and b are type variables. In this signature, it means map definition will hold for all a and b types.

Instantiating, for example, a as String and b as Int, we have:

map :: (String -> Int) -> [String] -> [Int]

Good question. In ES6, => denote anonymous or arrow functions and, in this case, ws is bound to that function. Specifically,

ws => ...

This tutorial is an interesting resource: [http://learnyouahaskell.com/chapters]

1 Like

You can find the complete library here:

https://github.com/RobTrew/prelude-jxa/tree/master/JS%20Prelude%20MD

1 Like

First - thank you and echoing it a few times. Sometimes a feedback on actual work is worth many tutorials. :)

So, is the best way to assess whether the item is empty to assess its length? In python, an empty list of any kind was “Falsy” and therefore could directly be used in an if block.

So assign the value to variable and only then use them in effects. Hmm - I generally like that but it leads to more lines and experts seem to not write like that :). With that said, got it and will do more of this separation.

Thank you for the example functions. Need to make and use more of these little primary routines. They look beautiful aside from working well.

While I will read the link you shared, can I ask what the arrows in this line denote? So broadly I understand that the arrows likely denote a transformation, but then there should one be one arrow isn’t it? So the first parentheses contains the inputs to the function ok, then what is the arrow doing there, the arrow after parens denotes the transformation - ok, and after that is the function output but what is the arrow doing in the output? Not sure if the question makes sense - I hope it does and I am sure once I understand I will be embarrassed that I did not understand the first time.

I will be sure to read it and try to understand. I have heard about functional programming and it appears that you use a lot of functional programming principles, given the number of reusable functions in your JXA-prelude. Intuitively sounds very clean, but I will admit after reading your most recent comments, I went back to your script where you used ws arrow function, and still could not understand what bindLR does and then there is the use of the Right function and so on. But. I will be working to understand your library and your code. After reading the document you linked to.

Big thank you!

This Haskell is very interesting so far. Am at lists and reading. Need to defer it just a little as it is a rabbit hole :)

But it seems so different from javascript / python that I wonder if it is a good idea to try and write javascript scrips or perhaps python scripts with Haskell principles (or functional programming principles). But you seem to do that in at least your javascript scripts and in how created the library for JXA. So I guess there is merit.

It seems the document may answer my arrow related question at a later state in the document.

I am at list comprehensions and there is some reference to how x <- [1…20] refers to x in numbers ranging from 1…20. May be this arrow is different from the nomenclature.

You’re welcome.

Yes. Another way would be to check equality with the empty list, i.e. === []
Python is implicitly coercing the empty list to a boolean, but would be best to not rely on that.

Most functional programming languages are based upon the lambda calculus, where every function has only one parameter.

even :: Int -> Bool
even n = n `rem` 2 == 0

Takes a value of type Int and return a value of type Bool.

add :: Int -> Int -> Int
add x y = x + y

Apparently, add takes two parameters of type Int and return a value of type Int. But, in reality, if we make parentheses explicit…

add :: Int -> (Int -> Int)
add x y =  x + y    

Takes one value of type Int and returns a type (Int -> Int), i.e. a function that takes an Int and return a value of type Int.

If we look at its definition in Data.Either, we see:

data Either a b = Left a | Right b

The Either type represents values with two possibilities: a value of type Either a b is either Left a or Right b.

bindLR takes an argument of type Either a and returns a function taking:

  • one function of type (a -> Either b), and
  • returns a value of type Either b.

I found that functional programming suites automation scripting very well. It depends on what we are trying to accomplish. Haskell training pays off when programming in an imperative language, also. But, it takes some time to become familiar with it.

Yes, apart from being an arrow pointing to the left, they have completely different meanings and uses.

x <- [1..20] is a generator and, as you say, x is going to take every value in that list.

P.S. Robin Trew created the wonderful prelude-JXA library.

1 Like

Thank you for explaining. And I am still working on the Haskell tutorial - it will be a longer journey that you got me started on here to understand functional programming and use it where it is most suitable.

In the meantime, shouldn’t the comment for the math function be as below,

add :: Int Int -> Int

where the int int refers to two integers as input and then one -> (arrow) to indicate transformation and then one int to indicate what one gets back from the function?

If you think that there is no shortcut to understanding at least the comment notation, then that is fine, and I will just wait till I have completed the Haskell document.

Thank you! You rock.


Edit: You can ignore the above comment / question. I will try to follow from the book as the books seems to begin to talk about type declarations for functions. I thought these were just comments above the code, but in Haskell it seems these can be actual type declarations for functions, in javascript, I guess they are there for comments and for explaining what the function does.

Not an easy one. Starting to get a sense of those concepts takes some time.

Perhaps, you are thinking about a Tuple. In that case, add would take one argument (a Tuple consisting of two values of type Int) and return a value of type Int. So, its type signature would be:

add :: (Int, Int) -> Int

Remember, every function takes one argument. In Haskell, every function is automatically curried. It is useful to make specialised versions of function. For example,

add :: Int -> Int -> Int
add x y = x + y

add5 :: Int -> Int
add5 = add 5

It is not necessary to fill all the gaps of a function. We can partially apply them. That’s the case in add5.

Yes, in Haskell, the compiler uses type signatures for typechecking. Comments are --
We write those same type signatures in JS for reading purposes.

1 Like

Very true. Not easy as I am still not able to understand your script above. But am determined to. Will come back to it after reading the Haskell stuff. Since the week is beginning, it make take some time.

Thanks for the add and add5 example. Starting to make some sense.

Will be back and thanks a bunch again.

1 Like

Thanks! I was way overthinking this ! Much appreciate the cool video