JXA error with app.move()

I’m creating a smart rule that performs a series of checks on incoming PDF files into the inbox and processes them accordingly. I’m writing a JXA script to do the processing.

One of the tasks is moving files to a group. I’ve reduced the TC to the following:

function performSmartRule(records) { 
    let app = Application('DEVONthink 3');

    let group = app.createLocation('/Test', app.inbox);

    records.forEach(function(record) {
        app.logMessage('Record: ' + record.name());
        app.move({record: record, to: group});
        app.logMessage('Moved: ' + record.name());
    });
}

When the smart rule is applied, the on performSmartRule (Error: Error: Can't convert types.) error is logged. The offending line is the one with the app.move() call.

Now the curious thing…

If I run the above as a standalone script (with osascript) it works like a charm.

I’ve spent many-many hours already trying to figure out what could be wrong but sincerely I’m out of ideas. This here is my cry for help.

Thanks in advance!

The current JXA interface is a bit peculiar with record parameters. Interesting, that the script runs with osascript. Could you post its code?

Yes, that’s the “depressing” part of trying to figure what’s going on.

I’ve adapted it so you can run it directly:

function performsmartrule(records) { 
    let app = Application('DEVONthink 3');

    let group = app.createLocation('/Test', app.inbox());

    records.forEach(function(record) {
        app.logMessage('Record: ' + record.name());
        app.move({record: record, to: group});
        app.logMessage('Moved: ' + record.name());
    });
}

// Set to false to run as a smart rule
if (true) {
    let app = Application('DEVONthink 3');

    // Get all records in the inbox
    let records = app.inbox().root().children().filter(function(record) {
        return record.kind() != 'Group';
    });

    performsmartrule(records);
}

Ive just confirmed that it works when running with osascript -l JavaScript script.jxa but not inside DT as a smart rule.

Thanks for the help.

Hm. A slightly modified version of the script runs ok in script editor, too:

function performsmartrule(records) { 
    let app = Application('DEVONthink 3');

    let group = app.createLocation('/Test', app.inbox());

    records.forEach(function(record) {
        app.logMessage('Record: ' + record.name());
        app.move({record: record, to: group});
        app.logMessage('Moved: ' + record.name());
    });
}

// Set to false to run as a smart rule
if (true) {
    let app = Application('DEVONthink 3');
    const db = app.databases['Test'];
	const group = db.incomingGroup;
    // Get all records in the inbox of database "Test"
    let records = group.children().filter(r => r.type() !== 'group');
    performsmartrule(records);
}

For me, it looks as if your script contains some subtle differences.

  • dereferencing inbox and root as in inbox() and root() seems to be unnecessary (though that doesn’t seem to throw an error). And you’re using inbox in your first script (the performsmartrule function), but inbox() in your second. So, not the same code for osascript and DT…
  • your call of createLocation works, but only coincidentally. The second parameter of this method is an object of the form {in: database}, while you pass in the database (i.e. the global Inbox) outside an object. That might work if Inbox is the current database.

Also, this very simplistic script runs fine in Script Editor

(() => {
  const app = Application("DEVONthink 3");
  const r = app.selectedRecords[0];
  const group = app.databases['Test'].incomingGroup;
  app.move({record: r, to: group});
})()

That seems to indicate that move works ok now in DT with JXA. I suggest that you fix the inconsistencies in your script and try again. Perhaps the Can't convert types messages is not related to the move itself, but to its group parameter…

1 Like

Thank you for all the observations. It is a big guess work to understand the JXA API of DT. But I’m getting used to the dictionary documentation now (although I still don’t know when to use dereferencing).

I applied your suggestions and came out with the following:

function performsmartrule(records) { 
    let app = Application('DEVONthink 3');

    let group = app.createLocation('/Test', {in: app.inbox});

    app.logMessage('Number of records: ' + records.length);

    records.forEach(function(record) {
        app.logMessage('Record: ' + record.name());
        app.move({record: record, to: group});
        app.logMessage('Moved: ' + record.name());
    });
}

// Set to false to run as a smart rule
if (true) {
    let app = Application('DEVONthink 3');

    // Get all records in the inbox
    let records = app.inbox.root.children().filter(function(record) {
        return record.kind() != 'Group';
    });

    performsmartrule(records);
}

I noticed there there is a bit of confusion around osascript so I’m rephrasing things here:

  1. The above script runs well when doing “play” in Script Editor (this is basically the same as running it with osascript).
  2. Now set the if check to “false” and save the script in the “Smart Rules” script folder of DT.
  3. Create a smart rules for the Inbox on files of kind “Any Document” that runs on demand and executes the saved script.
  4. Be sure to have some files in the inbox.
  5. Apply the rule.

For me, the “Test” group is created. The log shows 3 entries: the proper number of records, the “Record:” message for the first document and the error.

I’n other words, this issue happens only when the script runs inside DT itself.

Thanks you a lot for the amazing help!

To be clearer, this is my definition to the test Smart Rule:

PS: “FCK” are just my initials… had confusion with that in the past :slight_smile:

1 Like

LOL
Glad it’s not just an angry smart rule :wink:

By the way, what are you specifically trying to accomplish here? Moving documents to where, based on what?

1 Like

The script I showed here is just a reduced TC focused on the issue I’m having.

I’ve been spending the last 3 days learning and coding a script to be run as a Smart Rule in my inbox.

The script parses the text of PDF files that land into the inbox. I have a Smart Rule sending the list of new records (on import or move). The script retrieves the text of each record and send it to processors (6 of them so far) that use regex to sniff, add comments, rename and, finally, move the files to a group inside the inbox itself (different group, depending on the processor assigned for a specific file).

Anyway, that’s just the beginning. I hope to do much more in the future. But if the scripts don’t run as Smart Rules the whole concept (and work done so far) go to trash :frowning:

Scripting is not the only action available in a smart rule. In fact, smart rules are made for non-programmers to be able to automate some things without needing to be able to code.

Why don’t you explore the other smart rule actions, working with one “processor”, to try to accomplish your goal?

Well, in that case your initials might also describe your feelings towards the smart rule I guess :wink:

Yes, osascript and Script Editor should be basically the same. But so should DT’s JXA scripting. Or, in other words, DT is using the same infrastructure as osascript and Script Editor. Therefore, an error occurring in DT and not in Script Editor/osascript is probably not directly related to DT.
Now, when I add your script as an internal script (that is, selecting “JavaScript” instead of “External”), it works just fine over here (with a minor amendment, see below).

You might want to look in the forum for sample scripts, for example using “performsmartrule” as a search term or “Application(” (don’t know if the latter works to find only JXA scripts.

When I said “dereferencing”, I was a bit off the usual terminology. JXA (as AppleScript) mostly works with “Object Specifiers”. Those are funny beasts. Usually, you don’t have to do anything about them if you just want to pass them on to JXA methods again. However, if you want to use them in JavaScript proper, things become interesting. JS doesn’t know sh*t (not my initials) about Object Specifiers. But it can (kind of) get at (some of) their internals by using function syntax on them. That’s what I called “dereferencing” before.
So, database.records is an Object Specifier, and you could (for example) use the JXA method whose with it like database.records.whose({name: "myname"}). You can also do database.records.length because length works with Object Specifiers. And even databases['Name of DB'], which looks like pure JS, works in JXA although databases is an Object Specifier.

But if you want to apply real JavaScript methods like forEach, you have to somehow™ convert the ObjectSpecifier to a JS object. That’s what the function call syntax does: you can do database.records().forEach(…), but database.records.forEach(…) will throw an error.

This conversion must also be applied to strings (console.log(record.name) will give you an error, while console.log(record.name()) prints the name.

It’s all a bit messy, yes. Rule of thumb: If you stay inside the JXA environment, i.e. you pass around things between apps or between methods of an app, do not “dereference”. If you want to work with something coming from JXA in JS, do “dereference”.

And then there are those border cases like the parameter to performsmartrule: That’s a list of Object Specifiers, aka known as Array in JavaScript. Therefore, you can do a records.forEach(r => ...). But then, each element of this Array is an Object Specifier. So, you have to write records.forEach(r => console.log(r.name())) to print out the name of every record.

You’ll find more on JXA on my website Scripting with JXA | JavaScript for Automation (JXA), also some DT examples

Now to the tiny amendment to your script. For my DT scripts, I tend to do this:

function performsmartrule(records) {
…
}
(() => {
  const appName = app.currentApplication.properties()['name'];
  if (appName !== 'DEVONthink 3') {
     performsmartrule(Application('DEVONthink 3').selectedRecords());
  }
})()

Rationale:

  • I don’t have to modify the source, the script figures out the environment it’s running in itself.
  • As the condition appName !== 'DEVONthink 3' is always true in a testing environment (aka Script Editor), the performsmartrule function is run with the currently selected record(s) in that context. That is, in my opinion, a lot more flexible than hard-coding locations etc.
1 Like

Christian, Jim, and others, first of all, I’m amazed by the level of passion and dedication you demonstrate here. For that, I thank you from the bottom of my FCK heart :slight_smile:

Being a software developer myself, I’m never satisfied with workarounds or the “let it go and do it some other way” approach. If a feature (like JXA automation) is made available, it must work (and have bugs, since it’s software). So, I’ll keep digging until I see that there is no chance for it, and eventually, and sadly, accept the need to “let it go and do it some other way.”

So let me continue trying…

You said this:

Yes, osascript and Script Editor should be basically the same. But so should DT’s JXA scripting. Or, in other words, DT is using the same infrastructure as osascript and Script Editor.

… which opened my eyes to something.

Although it is true that DT uses the same infrastructure, there’s an important difference that we didn’t pay attention to. In the case of Smart Rules, DT is the one preparing the list of records and sending it to performsmartrule(records). So, I intuitively thought that maybe the list built by DT is different than the one my script builds. And voila, the issue has been located.

The TC is simple. Here’s my original code, which doesn’t work:

function performsmartrule(records) { 
    let app = Application('DEVONthink 3');

    var group = app.createLocation('/Test', {in: app.inbox});

    records.forEach(function(record) {
        // This line throws an error
        app.move({record: record, to: group});
    });
}

I then tried something else, to not use the records sent by DT and build my own list instead, and everything works:

function performsmartrule() { 
    let app = Application('DEVONthink 3');

    // Get all records in the inbox,
    // instead of relying on the records sent by DT
    let records = app.inbox.root.children().filter(function(record) {
        return record.kind() != 'Group';
    });

    var group = app.createLocation('/Test', {in: app.inbox});

    records.forEach(function(record) {
        // This line now works
        app.move({record: record, to: group});
    });
}

So, “theoretically” we have similar records lists, but there is something in the internals that messes things up. I tried to figure out what, with no success, certainly because of my limited knowledge of JXA and DT.

It is important to note that this issue happens when calling this code from a Smart Rule in DT, so DT will build the records list itself.

I hope this will help those who know more to understand and eventually fix this issue.

The nice outcome of this research is that a “reasonable” workaround could be found. It basically involves rebuilding the records list within the script:

function performsmartrule(records) { 
    let app = Application('DEVONthink 3');

    // Rebuild the record list since the one passed by DT can cause issues
    records = records.map(function(record) {
        return app.getRecordWithUuid(record.uuid());
    });

    var group = app.createLocation('/Test', {in: app.inbox});

    records.forEach(function(record) {
        // This line now works like a charm
        app.move({record: record, to: group});
    });
}

I assume the above will become part of my default template when developing Smart Rule scripts in the future… at least for the time being :slight_smile:

2 Likes

Good that it works now. Not so good that you’re working around the whole idea of smart rules.

I’d suggest you have a look at the output of your original script again. What’s special about the record causing the error? As I said before: the original code runs just fine here. Which makes me doubt that it’s a DT issue (unless we’re using different versions – mine is 3.9.4).

Edit I just replicated your approach by saving the script in the Smart rules folder of DT’s script folder. Now I see the same error as you do:

03.02.24, 12:55:19: Number of records: 28
03.02.24, 12:55:19: Record: ScanPro 18.05.2020 18.12
03.02.24, 12:55:19: ~/Library/Application Scripts/com.devon-technologies.think3/Smart Rules/Copy to subgroup.scpt	
on performSmartRule (Error: Error: Typen können nicht konvertiert werden.)

@cgrunenberg: Shouldn’t external and internal scripts behave identically? Here’s the code:

function performsmartrule(records) { 
    let app = Application('DEVONthink 3');

    let group = app.createLocation('/Test', {in: app.inbox});

    app.logMessage('Number of records: ' + records.length);

    records.forEach(function(record) {
        app.logMessage('Record: ' + record.name());
        app.move({record: record, to: group});
        app.logMessage('Moved: ' + record.name());
    });
}

(Working on all documents in the global inbox that are of type “any document”).
If run as an internal JS script, it works ok. As an external script, it utters the first two log messages and then throws the error “Can’t convert types”.

2 Likes

Theoretically they should. But AppleScript & JXA have some weird peculiarities :wink: Just like running a script in the Script Editor.app is not fully identical to running it via the Scripts menu of DEVONthink or the global Scripts menu extra.

Anyway, a future version should fix the inconsistent handling of internal/external scripts and definitely will fix the original move issue.

3 Likes

I have encountered the same issue with the JXA method getCustomMetaData. The following script …

function performsmartrule (records) {
	const app = Application ('DEVONthink 3');
	app.includeStandardAdditions = true;
	records.forEach(record => {
		const x = app.getCustomMetaData({for: 'date', from: record});
		app.displayAlert('success', {message: x});
	})
}

function run () {
	const app = Application ('DEVONthink 3');
	app.includeStandardAdditions = true;
	const records = app.selectedRecords();
	records.forEach(record => {
		const x = app.getCustomMetaData({for: 'date', from: record});
		app.displayAlert('success', {message: x});
	})
}

… runs fine (1) in the Script Editor, (2) from DT’s scripts menu, and (3) In a smart rule via Apply Script > JavaScript. It does not work only when saved in the Smart Rules folder and run through Apply Script > External. Using other metadata identifier keys does not change the outcome.

It happens that the same error message is given by Script Editor when I change record to undefined

app.getCustomMetaData({for: 'date', from: undefined})

Update: I tested the workaround (to “reconstruct” the list of records) suggested by @fredck . It works. Thanks a lot!