For once I don’t have a question but a script I’d like to share. A Smart Rule script that distributes items by tag.
A Use Case for the Script
First I’ll explain how I am working with tags. I use, so to speak, two different kind of tags with two different purposes.
The first kind is the common one, the »topic tag«. These tags connect items of any kind by a topic, say “19th century novel”. A search for this tag shows every item tagged with it, wether they are in the same group, database, or maybe even outside of DEVONthink. These tags are (meant to be) permanent.
The second kind is the “destination tag”. I tag items outside from DEVONthink with these tags to tell DT where they belong as soon as they get into it. When the file is coming from macOS or iOS/iPadOS they can be tagged directly if the app they are created in supports the OS tags (I try to avoid apps that don’t).
If you use DT’s Convert Hashtags to Tags feature, either generally or in a Smart Rule, you could use tags even with files from non-Apple systems, at least when they contain text. So tags cover items with quite a large number of origins and kinds. Destination tags are non-permanent, they get deleted right after the item has reached its destination.
I use a central collecting place for items from outside of DEVONthink, which naturally is the Global Inbox as it can be accessed from the outside.
A Smart Rule watches the Global Inbox and uses the script to distribute the incoming items to my databases.
The Script
-- Distribute by Tag
on performSmartRule(theRecords)
tell application id "DNtp"
set theDestinationTags to {"A Group", "A Sub-Group", "Naughty"}
set theDestinationPaths to {"/A Group", "/A Group/A Sub-Group", "/Notebooks/My Secret Diary"}
set theDestinationDatabases to {"Database 1", "Database 1", "Highly Encrypted Database"}
set theDefaultDestination to root of database "Database 1" -- or inbox, if you prefer
repeat with theRecord in theRecords
set theTags to the tags of theRecord
set duplicated to false
repeat with theTag in theTags
set theDestinationPosition to my getPositionOfItemInList((theTag as string), theDestinationTags)
if (theDestinationPosition is not 0) then
set theDestinationPath to item theDestinationPosition of theDestinationPaths
set theDestinationDatabase to item theDestinationPosition of theDestinationDatabases
set theDestination to create location theDestinationPath in database theDestinationDatabase
set theTags to my removeItemfromList((theTag as string), theTags)
set theDuplicatesTags to theTags
repeat with theDestinationTag in theDestinationTags
set theDuplicatesTags to my removeItemfromList((theDestinationTag as string), theDuplicatesTags)
end repeat
repeat with theDestinationTag in theDestinationDatabases
set theDuplicatesTags to my removeItemfromList((theDestinationTag as string), theDuplicatesTags)
end repeat
set the tags of theRecord to theDuplicatesTags
duplicate record theRecord to theDestination
set duplicated to true
set the tags of theRecord to theTags
else
if theDestinationDatabases contains theTag then
set theDestinationDatabase to theTag
set theTags to my removeItemfromList((theTag as string), theTags)
set theDuplicatesTags to theTags
repeat with theDestinationTag in theDestinationTags
set theDuplicatesTags to my removeItemfromList((theDestinationTag as string), theDuplicatesTags)
end repeat
repeat with theDestinationTag in theDestinationDatabases
set theDuplicatesTags to my removeItemfromList((theDestinationTag as string), theDuplicatesTags)
end repeat
set the tags of theRecord to theDuplicatesTags
duplicate record theRecord to root of database theDestinationDatabase -- or inbox, if you prefer
set duplicated to true
set the tags of theRecord to theTags
end if
end if
end repeat
if duplicated then
delete record theRecord
else
move record theRecord to theDefaultDestination -- optional
end if
end repeat
end tell
end performSmartRule
on getPositionOfItemInList(theItem, theList)
repeat with n from 1 to count of theList
if item n of theList is theItem then return n
end repeat
return 0
end getPositionOfItemInList
on removeItemfromList(theItem, theList)
set theNewList to {}
repeat with n from 1 to count of theList
set theNewItem to item n of theList
if theNewItem is not theItem then set theNewList to theNewList & theNewItem
end repeat
return theNewList
end removeItemfromList
What Does the Script Do and How to Customize It?
The Basics
There is a list, theDestinationTags
, which contains every destination tag.
There is a corresponding list, theDestinationPaths
, which contains every POSIX path of the destination.
There is a second corresponding list, theDestinationDatabases
, which contains every destination database.
Meaning: If an item is tagged with the first item of theDestinationTags
, it will be moved to the first item in theDestinationPaths
in the first item of theDestinationDatabases
. And so forth.
TheDefaultDestination
is the destination for every item that has no destination tag.
Just expand and customize these lists to your needs.
The Actions
When an item has arrived the script checks it for destination tags and moves it to the respective destinations. Yes, plural. The script will send an item to more than one destination if it has more than one destination tag.
It does also look for tags named after destination databases. If there are any the script will send the item to the root folder of those databases. If you prefer the inbox you can change that of course.
In the last step all destination and database tags get removed. All “normal” a.k.a. topic tags of course are kept untouched.
If an item has no destination tag it will be moved to theDefaultDestination
. This is of course optional. If you prefer to keep items without destination tags in their incoming group to later move them manually, why not?
But if so when you are using an interval trigger for the Smart Rule you should consider marking these items for exclusion from the rule, maybe by flagging them, after the script has been executed on them once. If not it will be iterating through their tags every time the Smart Rule kicks in. That would cause unnecessary activity.
Why This Script Or: Can’t Smart Rules With Simple Move Actions Provide This Too?
I have no doubt they can. But using a number of sequenced move action Smart Rules instead of one Smart Rule with this script can get fiddly at least in two ways:
The Proper Sequencing of the Move Action Smart Rules
When you use a fallback Smart Rule, i. e. the Smart Rule that moves an item to the default destination if none of the other move action Smart Rules have detected their conditional destination tags, the fallback Smart Rule must by all means be executed at last. Depending on the number of sequenced move action Smart Rules and on the kind of their triggers the intended execution order of Smart Rules might get broken.
Unless … the fallback Smart Rule comes with a safety net by checking the items for all the destination tags first: If the item is not tagged with destination tag 1, is not tagged with destination tag 2, &c. … only then move the item to the default destination.
Can be done but leads us straight to:
Maintenance
When you allow only one destination tag at a time it’s simple: Tag X is detected, item gets moved to the respective destination, Tag X gets removed.
When you allow multiple destination tags—like this script does—every single move action Smart Rule would not only have to remove its own destination tag but all of them. Plus the database tags.
Now imagine what you have to do when you want to expand your list of destinations:
- You have to set up a new move action Smart Rule
- You have to take care of the proper sequencing of all the move action Smart Rules.
- You have to add the new destination tag to every other move Action Smart Rule for removal.
- You have to add the new destination tag to the fallback move action Smart Rule
Whereas in the script you just have to add the new destination tag, its destination path and its destination database once to their respective lists and that’s it.
In Short
When you only have a small number of destinations move action Smart Rules probably will suffice. When it comes to a future proof heavy lifting in my opinion this script provides a more comfortable alternative. Which is why I wrote it in the first place.