Hazel and DEVONthink

Hazel and DEVONthink

Hazel is a nifty tool to watch over folders and handle incoming files. It is (still) offering better functions to rename files depending on file content or metadata. DEVONthink 3 (DT) on the other hand is very good in storing and finding documents. Linking the two together lets Hazel adjust the file names, for example for bank statements or recurring bills and send them to DEVONthink with a JXA script.

This script is executed by Hazel as last part of the renaming rule. It must define a function hazelProcessFile which receives the file name as first parameter and an array of additional parameters that you define in the Hazel rule.

Assuming that you have two private and three business accounts and corresponding groups in the DT databases private and business. You could then setup a JavaScript object like so

  const targetGroups = {
    accNo1: { db: "private",  group: "account 1"},
    accNo2: { db: "private",  group: "account 2"},
    accNo3: { db: "business", group: "account 1"},
    accNo4: { db: "business", group: "account 2"},
    accNo5: { db: "business", group: "account 3"},

Passing in the account number as additional parameter to hazelProcessFile permits you to find the correct database and group with targetGroups[accountNo].db ( line 9) and targetGroups[accountNo].group ( line 10).

function hazelProcessFile(theFile, inputAttributes) {
  const targetGroups = { … }
  const app = Application('DEVONthink 3');

  const accountNo = inputAttributes[0];
  const target = targetGroups[accountNo];
  const dbName = target.db;
  const groupName = target.group;

  const db = app.databases[dbName];
  const g = app.search(`name:${groupName} kind:group kind:!tag`,
     {in: db})[0];

  const rec = app.import(theFile.toString(),  {to: g});

You might be wondering why the code to find the correct group uses the search method ( line 13) instead of the more obvious db.groups[groupName]. This is due to DT’s object model: A database contains several collections of records (for historical reasons), and groups is none of them. Using search here is in fact more convenient then figuring out a contrived whose() call.

The whole setup assumes, of course, that group names are unique in a database. Which seems to be reasonable in this context.

This approach is not limited to account statements. You could for example add telCo1: {db: "private", group: "invoices"} to your targetGroups object and have Hazel pass in “telCo1” as first element of inputAttributes.

If you’re using tags in DT, you can add an additional field tags to every element of targetGroups and have the script set the tags of the record after importing the file. This approach is explored in more detail here.