For other people coming across this I found this documentation for JXA. Seems to be a pretty good source for info. There’s lots of people out there complaining about how Apple hasn’t kept up their documentation so this should help.
<marketing blurb>
There’s a more thorough and recent JXA site out there, too
</marketing blurb>
You can also replace Hazel with the Folder Actions Service built in macOS to run scripts upon certain events.
const groupFound = app.search(`name: ${group} kind:group kind:!tag`, {in: app.databases[db]});
I’m using the same group name multiple times (e.g. I have the subgroup Documents in each project group).
Is there a way to restrict the search for a group when using the same group name multiple times? Can I include the parent group in the search?
I found a solution. I instead of using app.search() for finding a group I can use app.getRecordAt():
const group = "/Dokumente";
const db = app.getCustomMetaData({for: "Datenbank", from: record});
if (!db) {
return;
}
const groupRecord = app.getRecordAt(`${group}`, {in: app.databases[db]});
You could also use createLocation, I think. But I’m away from my Mac, so can’t check.
I know this is an old thread, but you referenced it more recently in 2024, so i’m guessing that it is still relevant.
I am interested in automatically filing bills into various groups in DEVONthink and would like to study your script as a starting point.
Unfortunately the link to download your script is broken (404).
is that intentional? Perhaps there’s a more current version?
thanks in advance.
No, it is not intentional. I modified the website in the meantime. Sorry for that.
You can download the script here. And there’s a bit more explanation on it here. I also fixed the link in the original post.
Thanks for the quick response, the script, and the very helpful info on your website.
I am moving your code over, piece by piece, and modifying it for my environment.
I have set up a new database: “JXA Test” and copied over a representative pdf for each type of bill / invoice / … that I would like to auto-file so that I can start the experimentation phase.
Unfortunately, I am running into a problem with your original code in your script that is doing a search of DEVONthink using the search command. The problem is the format of the query with an optional in parameter.
Here is line 75 of your script:
const groupFound = app.search(name: ${group} kind:group kind:!tag, {in: app.databases[db]});
I get an error from Script Editor:
app = Application(“DEVONthink”)
app.search(“name: Output kind:group kind:!tag”, {in:app.databases.byName(“JXA Test”)})
→ Error -50: Parameter error.
Result://Error -50: Parameter error.
The dictionary isn’t precise about how to specify the optional parameter(s), and maybe it’s common knowledge that I don’t have, so I experimented:
// Attempt 1
// moving the optional “in:” argument inside the main query,
// instead of as a second argument works for me:
const groupFound = app.search(name: ${group} kind:group kind:!tag in: app.databases[db]);
// but only in the sense of not getting a runtime error, the search still fails
// Attempt 2
// If I mimic the format inside the DEVONthink search box and use scope instead of in:
const groupFound = app.search(name: ${group} kind:group kind:!tag scope: app.databases[db]);
// the code runs and finds the group this time
// - but it is actually searching all of my databases,
// not just "JXA Test", so the scope: keyword isn't working either.
I’m hoping that you might be able to shed some light on how to correctly use the search command in DEVONthink from JXA.
Hi,
First off: Make sure that you copy/paste the code as correctly as possible. You have “smart” quotes (curly ones) in the code you posted here. I know that you do not have these wrong quotes in your original code because then it wouldn’t even compile. But with the wrong quotes here, copy/pasting the code into script editor (or wherever) will produce a syntax error.
Then there’s indeed a bug in my code:
should be
const groupFound = app.search(`name: ${group} kind:group kind:!tag`, {in: app.databases[db].root});
because app.databases[db] returns a database, while the in parameter in search expects a record. The root property of a database is a record, kind of the top level one. So that’s that, and thanks for pointing it out.
The dictionary has been invented (by Apple) for AppleScript, and for people using that language, it might be obvious what the parameters mean and how to pass them. For JavaScript, not so much…
In this case, you use
search("search string", {in:…, comparison:…, excludeSubgroups:…})
where the object and each of it’s components are optional.
In general, optional named parameters are denoted by square brackets and have to be passed in an optional object (those that you need in a single object). Required named parameters (those in bold face and not in square brackets) must also be passed in a single object.
In some cases, there may be an unnamed first parameter (like with search). This one appears as first parameter in the scripting dictionary. It’s denoted with the same name as the method and written in bold face. Another example for this is deleteRowAt (where you pass the window as first parameter) and doJavaScript, where you pass the script code as first parameter.
Yes, that’s messy, but you’ll get used to it. And the scripting dictionary now contains examples which might help.
Note that app.databases["Test"] is the same as app,databases.byNames("Test").
I’m not sure how you worked all this out, but I am extremely grateful that you did, and then take the time to help teach others.
I have written quite a lot of AppleScript but never feel comfortable just typing away with the expectation that my code is correct. I have written less JavaScript, but can sit and type confidently without constantly asking myself how to do something.
So far all my JavaScript has been script actions in Shortcuts using Scriptable, or script steps in Drafts, so this is a new frontier for me, and thanks again.
… and it is likely (always?) required even if empty, i.e. a blank string.
DEVONthink 4 introduced an optional markdown parameter for the getMetadataOf method. I was excited to test it, but figuring out the syntax took me a little while:
(() => {
const app = Application("DEVONthink");
const r = app.contentRecord();
const mMD = app.getMetadataOf("", { markdown: r.plainText() });
// --> JavaScript object
)}()
You must include the empty string as the first parameter for it to work.
If there’s a first parameter named like the method as in this case, it is required.
getMetaDataOf is a bit peculiar, since (as you pointed out) the first parameter can be empty if you pass something in the markdown field of the optional parameter object. An interesting way to avoid using a flag, imo. I’d have written something like
getMetadatOf(text, isMD, baseURL) where text and isMD (boolean) are required and baseURL is not.
But that would hit the limitations of AppleScript. What I called a required parameter is what Apple in its abysmal “documentation” of their language seems to dub a “direct parameter”.
There can be only one of these, it is not named, and it must be the first. So, we are limited to one unnamed required parameter appearing first in a function/handler/method/whatever call if we want to have named parameters at all.
Alternatively, one could use positional parameters only, but then all of them are required. And it would be a PITA to specify something like the call to createRecordWith.
To sum it up: DT goes with named parameters, to make it easier for us. Which OTOH limits it to one mandatory parameter in the first position and might lead to peculiar situations like the one you described.