Hello everyone, I’m sharing a first success with DEVONthink that I’m quite happy with.
Thanks to ChatGPT and Claude, I managed to create a rule so that my company invoices, once placed in the Finder inbox, are automatically filed into the correct directory of the right database. It’s a very significant and appreciable time saver.
I can obviously still improve this, as for the moment, I will have to change the year of the directory every year. It’s a good start for me
Okay, here are some explanations. My goal is to be able to save my invoices in the Finder Inbox and have a rule in DEVONthink move them to the chosen folder structure based on the file title.
I should specify that my folder structure has a 2026 folder, followed by subfolders for each month to file my invoices. I have requested that the rule trigger upon import so I don’t have to think about it.
Here is the code
on performSmartRule(theRecords)
tell application id "DNtp"
set baseGroup to get record with uuid "YOUR UUID FOLDER"
repeat with theRecord in theRecords
set n to name of theRecord as string
set theMonth to my extractMonthFR(n)
if theMonth is missing value then
my addTag(theRecord, "auto:ERR-mois-non-detecte")
else
set folderName to "NAME FOLDER " & theMonth & " 2026"
set monthGroup to my findChildByName(baseGroup, folderName)
if monthGroup is missing value then
my addTag(theRecord, "auto:ERR-dossier-manquant")
else
move record theRecord to monthGroup
my addTag(theRecord, "auto:OK-" & theMonth)
end if
end if
end repeat
end tell
end performSmartRule
on findChildByName(parentGroup, childName)
tell application id "DNtp"
set kids to children of parentGroup
repeat with k in kids
if (name of k as string) is childName then return k
end repeat
end tell
return missing value
end findChildByName
on extractMonthFR(n)
set n to my toLower(n)
if n contains "janvier" then return "Janvier"
if n contains "février" or n contains "fevrier" then return "Février"
if n contains "mars" then return "Mars"
if n contains "avril" then return "Avril"
if n contains "mai" then return "Mai"
if n contains "juin" then return "Juin"
if n contains "juillet" then return "Juillet"
if n contains "août" or n contains "aout" then return "Août"
if n contains "septembre" then return "Septembre"
if n contains "octobre" then return "Octobre"
if n contains "novembre" then return "Novembre"
if n contains "décembre" or n contains "decembre" then return "Décembre"
return missing value
end extractMonthFR
on toLower(s)
return do shell script "echo " & quoted form of s & " | tr '[:upper:]' '[:lower:]'"
end toLower
on addTag(theRecord, newTag)
tell application id "DNtp"
set existingTags to tags of theRecord
if existingTags does not contain newTag then
set tags of theRecord to existingTags & {newTag}
end if
end tell
end addTag
Make sure to replace the UUID with your target directory’s ID.
“NAME FOLDER” is the name of your directory at the root level.
Did you use an AI to create that script?
Regardless:
why have set n to name of theRecord as string if name already is a string?
looping over the list of children in findChildByName is a bit awkward. You could simply do set foundChild to parentGroup's child childName. Wrap that in a try to catch a missing group.
myAddTag is slightly over-engineered. There’s no need to check if a tag exists already, you can’t have duplicate tags in DT anyway.
I’d rewrite extractMonthFR as a simple lookup, but I don’t know if that’s possible in AppleScript
Which brings me to … I’d write it in JavaScript (unsurprisingly):
const frenchMonths = {
'janvier': 'Janvier',
'fevrier': 'Fevrier',
'mars': 'Mars',
...
'aout': 'Août',
…
}
const monthsAlternation = RegExp(`(${Object.keys(frenchMonths).join('|'))}`;
function performsmartrule(records) {
const app = Application("DEVONthink");
const baseGroup = app.getRecordWithUuid('…');
records.forEach( r => {
const name = r.name();
const cleanedName = name.toLowerCase().replaceAll('é','e').replaceAll('û','u');
const matchMonth = cleanedName.match(monthsAlternation);
if (!match) {
r.tags = r.tags.concat('auto:ERR-mois-non-detecte');
return; // continue with next record
}
const folderName = `NAME FOLDER ${frenchMonths[matchMonth]} 2026`;
let monthGroup;
try {
monthGorup = baseGroup.children[folderName];
} catch {
r.tags = r.tags.concat('auto:ERR-dossier-manquant');
return;
}
app.move({record: r, to: monthGroup});
})
}
The code is not tested at all. It’s just meant as a short example how one could achieve the same goal in a more versatile language, eg avoiding a shell call only to lower case a string and using an object instead of a subroutine to normalize the month names.
An ignoring block in AppleScript would be sufficient too:
property pMonthList : {"Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"}
on extractMonthFR(recordName)
ignoring case and diacriticals
repeat with theMonth in pMonthList
if recordName contains theMonth then return theMonth
end repeat
end ignoring
return missing value
end extractMonthFR
The normalize call converts a diacritizied (diacriticalized?) character to a character followed by the diacritical (for any kind of diacritical, it appears), and the replace removes all the diacriticals.
Thank you for your feedback, but you’ve lost me. Unfortunately, I don’t know how to code, though I would like to learn one day.
I worked with ChatGPT and Claude to achieve this result, so I have no sense of what is considered good or bad practice. I confess that what interests me most is the final result.
To help me understand, what is the advantage of changing the code—for example, by using JavaScript?
First, making the code shorter makes it easier to understand, maintain and less susceptible to contain errors. For example, name as string when name already is a string is confusing and useless. The same goes for the “algorithm” to find the correct child group. It’s as ridiculous as one would expect from an AI, and as convoluted, too.
Short and concise should be a goal for code in any language. Even for AppleScript, which is chatty by design.
Take your extractMonthFR subroutine, for example, and compare it with what @cgrunenberg suggested and what I showed in the JavaScript code. Both implementations exploit the best features of the language. What your AI wrote, is a tedious to read and brute force “solution” to a fairly simple problem. I’m, btw, not even sure that it is needed – one ensure that the file names are correct in the first place.
Now, for the question which language to use… That depends on many things – taste, education, experience, to name but a few. I abhor AppleScript, which in my opinion is not even a programming language in the strict sense of the word, since it lacks a formal definition. I also find it difficult to write, because it is so verbose. Why would anyone want to write set a to b instead of a = b or a := b?
So, my arguments for JavaScript instead of AppleScript should be seen in the light of a personal preference.
It is a formally-defined language
It’s evolving, new features are occasionally added to it (about every two years, I’d say)
It’s used and useful nearly everywhere – in browsers, in servers, with macOS, Linux, and Windows
JavaScript has better object orientation and a rich type system.
It offers regular expressions and a huge set of string and array methods
Also, you can “introspect” objects, ie find out which attributes etc they contain. Try doing that with an AppleScript record.
The files always have the words “invoice, EURL, month, and year” in their names.
At the destination, we find almost the same thing, with the month and year always included.
I think AI coding is an amazing recent tool - particularly for small businesses looking to create internal tools.
Might I suggest that not only can AI write the script for you, but also it can teach you how it works if you ask?
As great as AI conding is, there are hidden gotchas that can lurk in code. Even if you do not have the time to write code, it’s a really good idea for you to learn how to read it. That’s the only way to know it’s not going to surprise you in a bad way when you least expect it.
I was trying to do exactly what you fellows are doing but I lost all programming skills… I used to program a bit with Prolog years back. I love being able to do this without scripting and have used Perplexity and one of its models to externally achieve the results you have. But it is not to my liking and would like to do without scripting the classification and subsequently the extracting of data. I have been trying other stand alone programs, but none yet do the job and i know it can be done within Devonthink. So.. Bluefrog , can you explain what you have done with no scripting..I see informacycle as under a smart rule called File invoices. Did you set up rules to achieve the results ?
Can you explain the rules ?
I know its never too late to learn, but at 83 not sure how much time i have left to learn AppleScript