Automating DT with JavaScript: Basics

Although AppleScript is prevalent in the DT forum, one can also use JavaScript for automation (JXA) to script it. It might have some advantages in the areas of string and date processing. In the following, I try to collect some patterns and approaches that might help in this context.

Getting the application instance

AppleScript uses tell application ... to set the application context in which method calls are executed. The JavaScript alternative is

const app = Application("DEVONthink 3");
app.includeStandardAdditions = true;

The second line is only necessary if you want to use UI methods like displayAlert. But it does no harm to include it anyway.

Using methods and getting properties

Once you have an application object, you can call its methods:

const selectedRecords = app.selectedRecords();

This returns an array containing the currently selected records from DT. The parenthesis are JXA lingo for get the value(s). If you don’t need the records themselves but for example their UUIDs, you can use

const UUIDs = app.selectedRecords.uuid();

This is a lot faster than iterating over the selectedRecords to get at the UUIDs.

Setting properties

To get a property of (for example) a record, you use

const prop = record.propertyName();

To set a property, you leave out the parenthesis:

record.propertyName = value;

The different approaches are due to the fact that record.propertyName does not refer to the property itself but to an “Object Specifier” in Apple parlance. Appending () calls this object’s get method, whereas using the property name on the left hand side of an assignment refers to the object itself. Or something like that.

Iterating over arrays (aka “lists”)

To iterate over the currently selected records, you can use forEach:

const recs = app.selectedRecords();
recs.forEach(r => {
  /* do something with r */
})

For example, to add a stylesheet to the beginning of all currently selected markdown files, you’d use

const style = 'h1: { color: blue; }';
const recs = app.selectedRecords();
recs.forEach(r => {
  /* get the text of the MD document */
  if (r.type() === "markdown") {
	const pt = r.plainText();
	r.plainText = `<style>${style}</style>
${pt}`;

Note that it is not necessary to write the record back or anything: Simply setting its plainText property will change the content. Therefore you might want to experiment with a copy of your records before running your script on the whole database.

Iteration for non-arrays

As you’ve seen, forEach iterates over all elements of an array returning the current element in each iteration. Sometimes you’re not dealing with arrays but e.g. a Set or a Map. Those objects do not provide the forEach method, but for … of works well:

const recs = app.selectedRecords();
for (let r of recs) {
/* do something with r */
}

Getting the database

JXA allows to access objects directly by name:

const db = app.databases["MyDatabase"];

gives you the Object Specifier of the database called “MyDatabase”. To print all groups in a database to the console, you’d use

let db = app.databases["MyDatabase"];
db.records().forEach( r =>  {
if (r.type() === "group") {
    console.log(r.name());
  }
})

Alternatively, you should be able to use the whose methode to get at the groups and then append the name property to the query to get an array of names:

/* THIS DOES NOT WORK! */
let db = app.databases["MyDatabase"];
let names = db.records.whose({type: "group"}).name();
console.log(names);

Unfortunately, using type with whose does NOT work currently. I suppose there’s something going on under the hood in DTs scripting implementation, but who knows…

Creating groups and records

To create a group in DT, you use createLocation. If the group exists already, it will simply return it. Otherwise, it will create the group and return it. So it is not possible to create a second group with the same name in the same database using createLocation. But that’s probably not something you’d want to do anyway.

const db = app.databases["MyDatabase"]
const dbGroup =  app.createLocation("/path/to/group", {in: db});

This will create “path” as top level group in “MyDatabases”, “to” as subgroup in “path” and finally “group” in “to”. Of course, if this hierarchy exists already, createLocation will simply return the Object Identifier of “group”.

MyDatabase
|
--- path
    |
	--- to
	    |
		--- group

Once you have the Object Specifier of a group, you can create a record in this group:

const myRecord = app.createRecordWith(
  {type: "markdown", 
   name: "My Markdown Document"}, 
    {in: dbGroup});

Note that some record types require you to enter more properties in the first object, for example a richText. So some experimentation might be in order.

Handling sheets in DT

DT can handle classic CSV documents (comma separated values) you can export from most any spreadsheet program. It also offers its own flavour in the form of “TSV” documents (tab separated values). To read and write those records, you have to access their cells property: const cells = record.cells();. This call returns an array of rows, each row being an array of cells. So to get at the second column in every row, you’d use

const cells = record.cells();
cells.forEach( r => {
  console.log(r[1]); /* print second element of row */
})

Note that in JavaScript array indices start with 0!

Now suppose you wanted to find the document where the 4th cell of the 3rd row contains the word “Bingo”. So you’d first get all sheets from DT and then iterate over them to check for this word in the appropriate place:

const recs = app.search("type: sheet");
recs.forEach(r => {
  if (r.cells()[2][3] === "Bingo") {
	console.log(`Found in ${r.name()} in database ${r.database.name()}`)
  }
}

Working with Markdown files

Questions about modifying markdown files are fairly frequent in the forum. In order to do so, you must access the plainText property of the record(s):
const pt = rec.plainText(); To append to it, use rec.plainText = pt + '\n\nNew text;. There’s no need to write back the document, setting its plainText property already changes and persists the content.

Suppose you have links in your Markdown document that refer to the domain example.com which you want to change to otherdomain.org:

const oldDomain = 'example.com';
/*
  Build a regular expression to find all links to documents on example.com
  Opening parenthesis '\\('
  followed by anything up to oldDomain '(.*?)${olddomain}
	   "anything up to" is captured in group 1 (i.e. the first pair of parentesis
*/
const RE = new RegExp(`\\((.*?)${oldDomain}`, 'g'); 
const newDomain = 'otherdomain.org';
var pt = rec.plainText();
/*
  Replace everything matched by the regular expression with
	Opening parenthesis '('
	first matching group '$1' (i.e. the part _before example.com_
	newDomain
*/
rec.plainText = pt.replaceAll(RE, `($1${newDomain}`);

There’s a more complex and fully documented example on adding markdown links to DT documents available in the forum.

14 Likes

Very nice post. Thanks for the time and effort. :slight_smile:

Thanks for the nice words. It was my pleasure.

A not-insignificant portion of education is the process of demystifying concepts and technology. Posts like this go a long way toward planting a seed that someone, like oh I don’t know, ME, can read and digest and slowly get around to giving javascript a shot.
If there was a Nobel Prize for voluntary forum contributions I’d throw your name in there.

2 Likes

As this is quite an old Post I am not sure if its ok to reply, but I give it a try hence its related to the basics listed above.

First of all thanks for posting these hints to help people like me, who are not professionals but try to learn to get around with Javascript and Devonthink. I also had a look at your site.

I just started with scripting and try to get some smartrules up and running in combination with Devonthink 4.

The only thing I cannot sort out, is listing the groups of a database. I started filing data in Devonthink 3 a while ago and created groups I would love to adress with smartrules. But for that I need to know how those are names especially with subgroups.

The script above to list groups does not work for me.

This is what I created:

(() => {
  const app = Application("DEVONthink");
app.includeStandardAdditions = true;
let db = app.databases["Business"];
db.records().forEach(r => {
if (r.type() === "group") {
console.log(r.name());
    }
  })
})()

Running it, I get the error:

execution error: Error: Error: Fehler in der AppleEvent-Routine. (-10000)

either something has changed with the transition to DT 4 or I am doing something wrong.
A console.log(db.records.length) returns 22, which is weird as there are above 40 PDFs within 13 groups.

Please let me know if I doing something wrong here, or something has changed. It also seems the Apple Scripts which should return a group list, do not work anymore.

The type property has been renamed to record type in version 4 and only AppleScript is able to use the synonym type. Therefore this should be r.recordType()

A new post would’ve been better, but since we’re at it …

Things have changed a bit between DT3 and DT4, and not only the parameter to Application. You’ve stumbled upon the disappearance of records, which was available in DT3.

Another thing is that type has been changed to recordType. But in createRecord, you have to use "record type" in the object you pass in.

I don’t understand what you’re trying to say there.

This code gets you the names of all groups in a database

(() => {
  const app = Application("DEVONthink");
  const db = app.databases["Test"];
  const groups = db.parents;
  groups().forEach(g => {
    if (g.recordType() === "group") {
      console.log(g.name());
    } else {
      console.log(`not a group: ${g.name()}`);
    }
  })
})()

Note that it also finds smart groups!
Alternatively, use
groups().filter(g => g.recordType() === 'group').forEach(g => ...)
to only get the groups.

If you have additional issues, please open a new thread.

The issue here seems to be with records(). While db.records.length returns something useful, db.records() throws an error:

app = Application("DEVONthink")
	app.databases.byName("Test").records()
Ergebnis:
Error -10000: Fehler in der AppleEvent-Routine.

But records is not listed as an element of database, anyway.

Thank you both! I will give it a try.

regarding this, I wanted to see what I would need to pass into your script to move the record into the correct group (while having several subgroups).

Well, what is “the correct group” then? And where does “the record” come from? Some more context would be appreciated.