Auto-Categorize Files Moved Into A Folder

Note: best use BetterTouchTool 5.373 or higher for this.

There are probably many other ways to achieve the same, but I'd like to share how to setup auto-categorization of files when moving them to some folder.

In this example I want to categorize files moved into the ~/Downloads folder.
Pdfs shall be moved into a subfolder ~/Downloads/PDFS
Zip files shall be moved into ~/Downloads/Archives

You can of course extend / change this however you want.

You can download the configured example here:
downloads_folder_changed_example.bttpreset (5.4 KB)

1.) Create a "File or Folder Did Change" trigger in the "Automations & Named & Other Triggers" section. Make sure to set it to "File Renamed / File Moved"

2.) Assign a "Run Real Java Script" action to it and assign this simple script. Adapt the "moveFile" function to your needs (just change the paths and your username)

async function moveFile() {
    // adapt the username
    let yourUsername = "andi";

    // define which files should go into which subfolder
    let moveToFolder = {
        ".zip": `/Users/${yourUsername}/Downloads/Archives/`,
        ".pdf": `/Users/${yourUsername}/Downloads/PDFS/`,
        ".mov": `/Users/${yourUsername}/Downloads/Videos/`,
        ".txt": `/Users/${yourUsername}/Downloads/Text/`,
        ".docx": `/Users/${yourUsername}/Downloads/Text/`
        // you can add as many mappings as you want
    }

    let movedTo = await reallyMoveFileWithMapping(moveToFolder);
    return `moved to ${movedTo}`;
}

// you don't need to change this
async function reallyMoveFileWithMapping(moveToFolder) {
    let filePath = await get_string_variable("BTTLastChangedFileOrFolder");

    // get the file extension
    const idx = filePath.lastIndexOf('.');
    const ext = idx > -1 ? filePath.slice(idx).toLowerCase() : '';

    // get the target path for the extension
    const target = moveToFolder[ext];
    if (!target) {
        return `no mapping for extension “${ext}”`;
    }

    if (filePath.startsWith(target)) {
        return "already in target folder";
    }

    // make sure the target folder exists
    await runShellScript({ script: `mkdir -p "${target}"` });
    // move the file
    await runShellScript({ script: `mv "${filePath}" ${target}`});
 
    return target;
}

Thanks for sharing! Additional idea would be to combine this with an LLM (preferably running locally using Ollama or LM Studio) to add additional file organization functionality.

At least for me, removing a path from the File Paths (the folders that are monitored) does not take effect until BTT is restarted. (BTT Version: 5.396 & Sequoia 15.5).

edit:
Also adding a path to the File Paths does not take effect until BTT is restarted.

edit2:
Tested also with 5.406 Alpha.