TL;DR – Tag files, Hazel sorts them into your Archive folder in a hierarchy of subfolders by tag.

I use the Desktop as a place for working files. Projects that I’m working on, files that I will need this week etc. Once they are no longer needed, I will delete them or file them away. With the help of Hazel and a couple of AppleScripts, I can achieve the latter simply by tagging them. For example, this years tax return might be tagged: “Finances”, “Tax”, “2017”, “Archive”.

Hazel sees the file, moves it into the Archived folder and then into subfolders Finances > Tax > 2017.

Here’s how to set it up:

First step is to move files that have been tagged “Archive” (I use the grey colour label for this). However, you want that file to have more tags than just “Archive” so the file must first pass the embedded AppleScript as below:

-- SET THIS TO "OFF" TO GET RESULT AS APPLESCRIPT LIST -- (USEFUL FOR USING TAGS WITH OTHER APPLICATIONS) set text_List to "OFF" -- SET THIS TO "OFF" TO REMOVE DIALOG BOX FROM TEXT LIST RESULT set display_ON to "OFF" (* ====================================== // PROPERTIES (USE CAUTION WHEN CHANGING) ====================================== *) set tagItems to {} (* ====================================== // MAIN PROGRAM ====================================== *) --RESET TEXT ITEM DELIMITERS set AppleScript 's text item delimiters to "" --GET THE FILE set thePath to quoted form of ( POSIX path of theFile ) as string -- GET THE TAG VALUES VIA SHELL SCRIPT set userTags to ( do shell script "mdls -name kMDItemUserTags " & thePath ) as text -- SET UP THE DELIMITER VALUES TO PULL OUT TAG VALUES… set theDelim to "(" set finalDelim to ")" -- SAVE THE OLD TEXT ITEM DELIMITERS set oldDelim to AppleScript 's text item delimiters -- PROCESS THE KEY INTO A LIST OF TAGS set AppleScript 's text item delimiters to theDelim set userTags to ( userTags as text ) set preProc to ( every word of userTags ) as string set procItems to words 3 thru end of preProc set finalLine to last item of procItems set AppleScript 's text item delimiters to finalDelim set finalProc to first text item of finalLine set last item of procItems to finalProc set AppleScript 's text item delimiters to oldDelim -- CREATE A COMMA-DELIMITED TAG STRING if text_List is "ON" then set AppleScript 's text item delimiters to ", " set procItems to ( procItems as text ) set AppleScript 's text item delimiters to oldDelim -- DISPLAY AS DIALOG, IF SELECTED if display_ON is "ON" then tell application "System Events" display dialog procItems end tell end if else -- … OR CREATE A CLEAN LIST OF TAGS -- REMOVE THE COMMAS… repeat with procItem in procItems set the_String to procItem if ( the last character of ( the_String ) is "," ) then set newItem to ( text 1 thru (( length of the_String ) - 1 )) of the_String as string copy newItem to the end of tagItems else set newItem to ( text 1 thru ( length of the_String )) of the_String as string copy newItem to the end of tagItems end if end repeat -- FINAL LIST ----return tagItems --PASSES APPLESCRIPT FOR HAZEL if ( count of tagItems ) is greater than 1 then return true end if if ( count of tagItems ) is 1 then return false end if end if

(Hat tip to Veritrope for the get tags script)

2. Now, we’ll need to create rules in the “Archived” folder.

You will need to save the following AppleScript somewhere and refer to it in the rules (it won’t work as an embedded script due to presence of handlers). Now before you scroll through this monstrosity let me explain what it’s doing: the end result is it exports Hazel tokens for the second “sort into subfolder” rule for Hazel to do it’s sorting business.

However, you may already have an existing hierarchy (e.g. “Computer” > “AppleScript” > “Countdown_Scripts”) and if you tag out of order or omit one of the tags (e.g. “Countdown_Scripts”; “Computer”), you would get a big mess of folders.

What the script attempts to do is check the hierarchy and adjust the order of the exported tags to match the existing file structure. If it finds a new tag, it will generally put it at the end of the order; if the tag comes up multiple times in the hierarchy the script will return the export tags in the same order as you entered them.

It’s a bit convoluted, let me know in the comments below if that doesn’t make sense. If you don’t care about the order of the tags you could cut that section out I suppose – let me know in the comments below if you’d like the less complicated version of the script.

You’ll need to change the three “/path/to/your/Archived folder/” lines, to where your folder is.

--variables for later... global thePrimaryFolderList global theSecondaryFolderList global theTertiaryFolderList global theSpecialCaseList global theFullFolderList on hazelProcessFile ( theFile ) (* ====================================== // USER SWITCHES (YOU CAN CHANGE THESE!) ====================================== *) -- SET THIS TO "OFF" TO GET RESULT AS APPLESCRIPT LIST -- (USEFUL FOR USING TAGS WITH OTHER APPLICATIONS) set text_List to "OFF" -- SET THIS TO "OFF" TO REMOVE DIALOG BOX FROM TEXT LIST RESULT set display_ON to "OFF" (* ====================================== // PROPERTIES (USE CAUTION WHEN CHANGING) ====================================== *) set tagItems to {} (* ====================================== // MAIN PROGRAM ====================================== *) --RESET TEXT ITEM DELIMITERS set AppleScript 's text item delimiters to "" --GET THE FILE set thePath to quoted form of ( POSIX path of theFile ) as string -- GET THE TAG VALUES VIA SHELL SCRIPT set userTags to ( do shell script "mdls -name kMDItemUserTags " & thePath ) as text -- SET UP THE DELIMITER VALUES TO PULL OUT TAG VALUES… set theDelim to "(" set finalDelim to ")" -- SAVE THE OLD TEXT ITEM DELIMITERS set oldDelim to AppleScript 's text item delimiters -- PROCESS THE KEY INTO A LIST OF TAGS set AppleScript 's text item delimiters to theDelim set userTags to ( userTags as text ) set preProc to ( every word of userTags ) as string set procItems to words 3 thru end of preProc set finalLine to last item of procItems set AppleScript 's text item delimiters to finalDelim set finalProc to first text item of finalLine set last item of procItems to finalProc set AppleScript 's text item delimiters to oldDelim -------------------------------------------------------------------------------- -- CREATE A COMMA-DELIMITED TAG STRING if text_List is "ON" then set AppleScript 's text item delimiters to ", " set procItems to ( procItems as text ) set AppleScript 's text item delimiters to oldDelim -- DISPLAY AS DIALOG, IF SELECTED if display_ON is "ON" then tell application "System Events" display dialog procItems end tell end if else -- … OR CREATE A CLEAN LIST OF TAGS -- REMOVE THE COMMAS… repeat with procItem in procItems set the_String to procItem if ( the last character of ( the_String ) is "," ) then set newItem to ( text 1 thru (( length of the_String ) - 1 )) of the_String as string copy newItem to the end of tagItems else set newItem to ( text 1 thru ( length of the_String )) of the_String as string copy newItem to the end of tagItems end if end repeat -- FINAL LIST --return tagItems -------------------------------------------------------------------------------- --get rid of "Archive" from the tag list set cleanTagList to {} repeat with i from 1 to count of tagItems set theItem to item i of tagItems if theItem is not "Archive" then copy theItem to end of cleanTagList end if end repeat --get a list of all primary directories do shell script "ls -d /path/to/your/Archived folder/*/" --transform into AppleScript compatible list set theFullList1 to paragraphs of result --get a list of all secondary directories do shell script "ls -d /path/to/your/Archived folder/*/*/" --transform into AppleScript compatible list set theFullList2 to paragraphs of result --get a list of all tertiary directories do shell script "ls -d /path/to/your/Archived folder/*/*/*/" --transform into AppleScript compatible list set theFullList3 to paragraphs of result --make some empty lists for later... set theCatchList1 to {} set theCatchList2 to {} set theCatchList3 to {} set thePrimaryFolderList to {} set theSecondaryFolderList to {} set theTertiaryFolderList to {} set theFullFolderList to {} set theSpecialCaseList to {} repeat with eachLine in theFullList1 set theOffset to offset of "Archived/" in eachLine set theLength to (( length of eachLine ) - 1 ) set theNewText to text theOffset through theLength of eachLine copy theNewText to the end of theCatchList1 end repeat --parse the "Archived/*/" into individual items {"Archived", "*"} set AppleScript 's text item delimiters to "/" repeat with eachItem in theCatchList1 set theFolders to text items of eachItem set thePrimaryFolder to item 2 of theFolders copy thePrimaryFolder to end of thePrimaryFolderList end repeat repeat with eachLine in theFullList2 set theOffset to offset of "Archived/" in eachLine set theLength to (( length of eachLine ) - 1 ) set theNewText to text theOffset through theLength of eachLine copy theNewText to the end of theCatchList2 end repeat --parse the "Archived/*/*/" into individual items {"Archived", "*", "*"} repeat with eachItem in theCatchList2 set theFolders to text items of eachItem set theSecondaryFolder to item 3 of theFolders --creat a list of "Special Case" folders, which exist as both primary and secondary directories if theSecondaryFolder is in thePrimaryFolderList then if theSecondaryFolder is not in theSpecialCaseList then copy theSecondaryFolder to end of theSpecialCaseList end if end if copy theSecondaryFolder to end of theSecondaryFolderList end repeat repeat with eachLine in theFullList3 set theOffset to offset of "Archived/" in eachLine set theLength to (( length of eachLine ) - 1 ) set theNewText to text theOffset through theLength of eachLine copy theNewText to the end of theCatchList3 end repeat --parse the "Archived/*/*/*" into individual items {"Archived", "*", "*", "*"} repeat with eachItem in theCatchList3 set theFolders to text items of eachItem set thePrimaryFolder to item 2 of theFolders copy thePrimaryFolder to end of theFullFolderList set theSecondaryFolder to item 3 of theFolders copy theSecondaryFolder to end of theFullFolderList set theTertiaryFolder to item 4 of theFolders copy theTertiaryFolder to end of theTertiaryFolderList copy theTertiaryFolder to end of theFullFolderList --creat a list of "Special Case" folders, which exist as both secondary and tertiary directories if theTertiaryFolder is in theSecondaryFolderList then if theTertiaryFolder is not in theSpecialCaseList then copy theTertiaryFolder to end of theSpecialCaseList end if end if end repeat set AppleScript 's text item delimiters to "" -------------------------------------------------------------------------------- --get names of all these damned tags set theCount to count of cleanTagList if theCount is 1 then set theTag to item 1 of cleanTagList as text end if if theCount is 2 then set theTag to item 1 of cleanTagList as text set theTag2 to item 2 of cleanTagList as text end if if theCount is 3 then set theTag to item 1 of cleanTagList as text set theTag2 to item 2 of cleanTagList as text set theTag3 to item 3 of cleanTagList as text end if -------------------------------------------------------------------------------- --get the order of these tags set the123List to {} repeat with eachItem in cleanTagList copy whatkindofTagisit ( eachItem ) to end of the123List end repeat set the123ListLength to length of the123List if the123List contains 4 then if the123ListLength is 1 then return { hazelExportTokens :{ Tag : theTag }} end if if the123ListLength is 2 then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 }} end if if the123ListLength is 3 then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 , Tag3 : theTag3 }} end if end if ----------------------------------[1 tag]--------------------------------------- if the123List is { 1 } then return { hazelExportTokens :{ Tag : theTag }} end if if the123List is { 2 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag }} end if if the123List is { 3 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList set theGrandParent to item ( theTagPosition - 2 ) of theFullFolderList return { hazelExportTokens :{ Tag : theGrandParent , Tag2 : theParent , Tag3 : theTag }} end if if the123List is { 0 } then return { hazelExportTokens :{ Tag : theTag }} end if ---------------------------------[2 tags]--------------------------------------- if the123List is { 1 , 2 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 }} end if if the123List is { 1 , 3 } then set theTagPosition to list_position ( theTag2 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theTag , Tag2 : theParent , Tag3 : theTag2 }} end if if the123List is { 1 , 0 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 }} end if if the123List is { 2 , 1 } then return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theTag }} end if if the123List is { 2 , 3 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag , Tag3 : theTag2 }} end if if the123List is { 2 , 0 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag , Tag3 : theTag2 }} end if if the123List is { 3 , 1 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theParent , Tag3 : theTag }} end if if the123List is { 3 , 2 } then set theTagPosition to list_position ( theTag2 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag2 , Tag3 : theTag }} end if if the123List is { 3 , 0 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList set theGrandParent to item ( theTagPosition - 2 ) of theFullFolderList return { hazelExportTokens :{ Tag : theGrandParent , Tag2 : theParent , Tag3 : theTag , Tag4 : theTag2 }} end if if the123List is { 0 , 1 } then return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theTag }} end if if the123List is { 0 , 2 } then set theTagPosition to list_position ( theTag2 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag2 , Tag3 : theTag }} end if if the123List is { 0 , 3 } then set theTagPosition to list_position ( theTag2 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList set theGrandParent to item ( theTagPosition - 2 ) of theFullFolderList return { hazelExportTokens :{ Tag : theGrandParent , Tag2 : theParent , Tag3 : theTag2 , Tag4 : theTag }} end if if the123List is { 0 , 0 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 }} end if ---------------------------------[3 tags]--------------------------------------- if the123List is { 1 , 2 , 3 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 , Tag3 : theTag3 }} end if if the123List is { 1 , 2 , 0 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 , Tag3 : theTag3 }} end if if the123List is { 1 , 3 , 2 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag3 , Tag3 : theTag2 }} end if if the123List is { 1 , 3 , 0 } then set theTagPosition to list_position ( theTag2 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theTag1 , Tag2 : theParent , Tag3 : theTag2 , Tag4 : theTag3 }} end if if the123List is { 1 , 0 , 2 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 , Tag3 : theTag3 }} end if if the123List is { 1 , 0 , 3 } then set theTagPosition to list_position ( theTag3 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theTag , Tag2 : theParent , Tag3 : theTag3 , Tag4 : theTag2 }} end if if the123List is { 2 , 1 , 3 } then return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theTag , Tag3 : theTag3 }} end if if the123List is { 2 , 1 , 0 } then return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theTag , Tag3 : theTag3 }} end if if the123List is { 2 , 0 , 3 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag , Tag3 : theTag3 , Tag4 : theTag2 }} end if if the123List is { 2 , 0 , 1 } then return { hazelExportTokens :{ Tag : theTag3 , Tag2 : theTag , Tag3 : theTag2 }} end if if the123List is { 2 , 3 , 0 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag , Tag3 : theTag2 , Tag4 : theTag3 }} end if if the123List is { 2 , 3 , 1 } then return { hazelExportTokens :{ Tag : theTag3 , Tag2 : theTag , Tag3 : theTag2 }} end if if the123List is { 3 , 1 , 0 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theParent , Tag3 : theTag , Tag4 : theTag3 }} end if if the123List is { 3 , 1 , 2 } then return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theTag3 , Tag3 : theTag }} end if if the123List is { 3 , 2 , 0 } then set theTagPosition to list_position ( theTag2 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag2 , Tag3 : theTag , Tag4 : theTag3 }} end if if the123List is { 3 , 2 , 1 } then return { hazelExportTokens :{ Tag : theTag3 , Tag2 : theTag2 , Tag3 : theTag }} end if if the123List is { 3 , 0 , 1 } then set theTagPosition to list_position ( theTag , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theTag3 , Tag2 : theParent , Tag3 : theTag , Tag4 : theTag2 }} end if if the123List is { 3 , 0 , 2 } then set theTagPosition to list_position ( theTag3 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag3 , Tag3 : theTag , Tag4 : theTag2 }} end if if the123List is { 0 , 1 , 2 } then return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theTag3 , Tag3 : theTag }} end if if the123List is { 0 , 1 , 3 } then set theTagPosition to list_position ( theTag3 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theTag2 , Tag2 : theParent , Tag3 : theTag3 , Tag4 : theTag }} end if if the123List is { 0 , 2 , 1 } then return { hazelExportTokens :{ Tag : theTag3 , Tag2 : theTag2 , Tag3 : theTag }} end if if the123List is { 0 , 2 , 3 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 , Tag3 : theTag3 }} end if if the123List is { 0 , 3 , 1 } then set theTagPosition to list_position ( theTag2 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theTag3 , Tag2 : theParent , Tag3 : theTag2 , Tag4 : theTag }} end if if the123List is { 0 , 3 , 2 } then set theTagPosition to list_position ( theTag3 , theFullFolderList ) set theParent to item ( theTagPosition - 1 ) of theFullFolderList return { hazelExportTokens :{ Tag : theParent , Tag2 : theTag3 , Tag3 : theTag2 , Tag4 : theTag }} end if if the123List is { 0 , 0 , 0 } then return { hazelExportTokens :{ Tag : theTag , Tag2 : theTag2 , Tag3 : theTag3 }} end if end if end hazelProcessFile -------------------------------------------------------------------------------- --Functions for later... on whatkindofTagisit ( theTest ) --returns 1 if primary folder --returns 2 if secondary folder --returns 3 if tertiary folder --returns 4 if the folder is found at multiple levels --returns 0 if unknown folder if theTest is in theSpecialCaseList then return 4 else if theTest is in thePrimaryFolderList then return 1 else if theTest is in theSecondaryFolderList then return 2 else if theTest is in theTertiaryFolderList then return 3 else return 0 end if end whatkindofTagisit --gets position of item in the list on list_position ( this_item , this_list ) repeat with i from 1 to the count of this_list if item i of this_list is this_item then return i end repeat return 0 end list_position --------------------------------------------------------------------------------

Update 2017-07-15:

If you would like to get a notification of where your file landed, just add an action at the end with an embedded shell script:

exec = 'open -R ' /usr/local/bin/terminal-notifier -message "$1" -title 'your file landed here:' -execute "$exec$1"