Javascript: Given a tag as a string, how to assign it?

I can’t work out how to find a tag using Javascript in DEVONthink.

Imagine, if you will, you were to have the text of a tag you know exists in your database.

How do you find this tag, using JavaScript?

Were you to then have a record with which you wished to associate this tag - which it may already have - how would this be achieved?

I’d say I’m very close to having a plugin that will find matching tags in a DT record, present the user with suggestions of which to link with the record, and assigning them, but really I’ve just written a few lines of code around Houthakker’s contributions. :slight_smile:

However, this stumps me - I can’t see any way to find a record/tag based on a name, short of walking the whole database.

“matching Tags”? Are you meaning it finds words in the text that are also existing Tags in a database? Are you trying to create some kind of “auto-Tagging” mechanism?

Hi Jim,

Yes, kind-of - more an ‘assisted naïf’ tagging system, working against my own specified taxonomy. I appreciate how hit-and-miss a text-based tag matching system is, but also don’t want to start using the complexity and overhead of systems like opencalais (I also don’t want my data leaving my machine!)

I find tags really useful to quickly catch key themes in a document, rather than the soup searching of free text. It’s also nice in conjunction with Ammonite, which makes it easy to tie together queries and relationships between my defined tags. Manual tagging is very time consuming, though, so anything that can help automate this will be a real time-saver, even if it is simplistic.

The things in my taxonomy are locations, human names, sources, and high-level themes. It’s nice to have these pieces of content collected into their related tag groups as well, and hierarchical groups/tags makes discovery particularly exploratory and interesting.

In AppleScript it looks like this:

tell application id "DNtp" to return item 1 of ((tag groups of current database whose name is "name of the tag") as list)

Thanks, cgrunenberg!

Thinking about this - what I actually mean in DEVONthinkese, on reflection, is ‘how do I replicate this record into a group?’ - I think this is a more formal structure, whereby it will inherit the group hierarchy as a set of nested tags (with groups as tags enabled for the hierarchy) - which is what I want.

Is that the same as adding a tag, under the hood? There’s a certain amount of voodoo to how DEVONthink is actually working.

You don’t need to replicate a record into a Tag. Applying a Tag automatically creates the Replicant. Also, you wouldn’t replicate it to all the Tags in a hierarchy as they inherit the parent Tags.

Thanks for the tips, cgrunenberg and bluefrog!

It’s horrible trying to debug in the Script Editor, everything is so opaque and you can’t look at what’s being returned easily, if at all.

I’m hoping I’m not too far away from a solution with this:

    // ADD TAGS --------------------------------------------------------------
	function addTagsToEntry(tags) {
		// tags is an array of strings, each string corresponding with the name of a group/tag in DT
		for(var i in tags) {
			var tagNameToAdd = tags[i];
			var tagToAdd = Application('DevonThink Pro').currentDatabase.tagGroups({name:tagNameToAdd});
			Application('DevonThink Pro').content().tags.append(tagToAdd);

but natch, there are opaque errors somewhere along those chains, and I’m not sure where they are. Any insight hugely appreciated (and very close to a reasonably useful autotagging script contribution here!)

  • Dave

The extent to which this works depends a bit on the macOS version, but worth experimenting with using the Safari debugger:

Also, I find that Atom provides a more pleasant editing environment, from which .scpt files can be run using

(and, of course, linting, code formatting etc for JS can be set up with other Atom packages)

Thanks - that Develop→«mymachinename»→*JSContents menu tip was really helpful.

So I had a poke around with Application().tagsGroups() and got nowhere, and realised I needed to do

Application('DevonThink Pro').currentDatabase().records()[0].properties()

which will get me a list of items I can filter on who may be kind: “Group” with a name: property I’m looking for.

Application('DevonThink Pro').currentDatabase().records()[0].properties()

Somewhere online I got a sense I could filter this list with some probably completely unrelated code, but clutching at straws, I tried it:

Application('DevonThink Pro').currentDatabase().records({match: [ObjectSpecifier().name, 'Uber']})[0].properties()

only to get nothing. And in fact, it just seems to point to the first group in my database anyway, not one of the things I’m trying to use at all.

Coming from another angle, I tried this:

Application('DevonThink Pro').contentRecord().tags()

which at least returned me a JS Array of strings, which felt less threatening to me, code-wise. Maybe, I thought, I could just append the tag as a string, and DEVONthink would step in and do the right thing for me?

Application('DevonThink Pro').contentRecord().tags().length // gives me a number of tags I'd expect

Application('DevonThink Pro').contentRecord().tags().append("Uber")
Application('DevonThink Pro').contentRecord().tags() // does *not* contain "Uber"!

So again, I’m kind of stumped. I’ve tried a few ways to implement bit of cgrunenberg’s suggestion as JS, but in a naïf way, and inevitably tripped.

I’m slowly getting there, but would welcome any suggestions - fully appreciating, now, how undocumented the JXA interfaces are (I can’t call some Applescript from JavaScript, can I? Is that crossing the beams?)

I can see the tags in the current record with:

Application('DevonThink Pro').contentRecord().tags().entries()

and there even seems to be a push() method in there

Application('DevonThink Pro').contentRecord().tags().push("Uber")

which returns the count of tags I’d expect to see on a successful push, but it makes no change.

Am I thinking of the word immutable for some reason? Do I have to recreate and reassign the tags property somehow?

Just a side note: This could easily be accomplished in ApleScript with…

tell thisRecord to set tags to (tags & "Uber")


I’m meeting you half-way, Jim, with a horrific JavaScript-then-terminal-then-AppleScript solution. I hope you’re pleased with yourself :slight_smile:

		for(var i in tags) {
			var tagNameToAdd = tags[i];
			var appleScriptToRun = 'tell application "DEVONthink Pro"¬	set thisRecord to the selection¬	set theTags to the tags of item 1 of thisRecord & "'+tagNameToAdd+'"¬	set tags of item 1 of thisRecord to theTags¬end tell¬';
			var osaCommandToRun = "osascript -l AppleScript -e '"+appleScriptToRun+"'";
			dt.doShellScript( osaCommandToRun );

Knowing that the code in ‘appleScriptToRun’ is a valid piece of AppleScript, which has executed successfully in the Script Editor (natch using newlines for ¬ and swapping out a legitiamte tag string for ‘+tagNameToAdd+’,

tell application "DEVONthink Pro"
	set thisRecord to the selection
	set theTags to the tags of item 1 of thisRecord & "Browser"
	set tags of item 1 of thisRecord to theTags
end tell

and which came from the terminal command to execute

 osascript -l AppleScript -e 'tell application "DEVONthink Pro"¬	set thisRecord to the selection¬	set theTags to the tags of item 1 of thisRecord & "Browser"¬	set tags of item 1 of thisRecord to theTags¬end tell¬'

I’m getting Error -1708: Message not understood. from the Script Editor, when I execute this wobbly tower of jelly.

Yet, if I open a terminal and paste the following in (newlines and all, I place it in the clipboard as multiple lines and paste directly into the terminal) - other than a few ‘dunk’ sounds - it works fine, and does what I want it to:

osascript -l AppleScript -e 'tell application "DEVONthink Pro" 
	set thisRecord to the selection 
	set theTags to the tags of item 1 of thisRecord & "Browser" 
	set tags of item 1 of thisRecord to theTags 
end tell'

help! I’m so close. If I can crack this last issue, of setting tags, I’m happy (or at least, I can start finding the new bugs this exposes…!)

…and yes, I realise I’ve exchanged my JavaScript bug for a terminal problem, via an AppleScript workaround. I’m very aware of that. I’m trying to be pragmatic, for certain values of pragmatism, as they say. teases me with more information!

error	16:41:47.556247 +0100	sandboxd	Sandbox: mds(84) deny file-write-xattr /Users/davem/Library/Application Support/DEVONthink Pro 2/Scripts/stagger.scpt
Violation:       deny file-write-xattr /Users/davem/Library/Application Support/DEVONthink Pro 2/Scripts/stagger.scpt 
MetaData: {"build":"Mac OS X 10.13 (17A405)","action":"deny","target":["Users","davem","Library","Application Support","DEVONthink Pro 2","Scripts","stagger.scpt"],"hardware":"Mac","platform_binary":"yes","profile":"unknown","process":"mds","op":"file-write-xattr"}

Process:         mds [84]
Path:            /System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Support/mds
Load Address:    0x109ba2000
Identifier:      mds
Version:         ??? (???)
Code Type:       x86_64 (Native)
Parent Process:  launchd [1]
Responsible:     /System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Support/mds [84]
User ID:         0

Date/Time:       2017-10-08 16:41:47.547 GMT+1
OS Version:      Mac OS X 10.13 (17A405)
Report Version:  8

Thread 0 (id: 595):
0   libsystem_kernel.dylib        	0x00007fff6b21ce76 mach_msg_trap + 10
1   CoreFoundation                	0x00007fff43b70475 __CFRunLoopServiceMachPort + 341
2   CoreFoundation                	0x00007fff43b6f7c7 __CFRunLoopRun + 1783
3   CoreFoundation                	0x00007fff43b6ee43 CFRunLoopRunSpecific + 483
4   mds                           	0x0000000109bae365
5   libdyld.dylib                 	0x00007fff6b0d6145 start + 1
6   mds                           	0x0000000000000001

Thread 1 (id: 2397):
0   libsystem_kernel.dylib        	0x00007fff6b227592 read + 10
1   libsystem_pthread.dylib       	0x00007fff6b3606c1 _pthread_body + 340
2   libsystem_pthread.dylib       	0x00007fff6b36056d _pthread_body + 0
3   libsystem_pthread.dylib       	0x00007fff6b35fc5d thread_start + 13

Thread 2 (id: 2472):
0   libsystem_kernel.dylib        	0x00007fff6b21ce76 mach_msg_trap + 10
1   SpotlightIndex                	0x00007fff65d26117 _handleExceptions + 111
2   libsystem_pthread.dylib       	0x00007fff6b3606c1 _pthread_body + 340
3   libsystem_pthread.dylib       	0x00007fff6b36056d _pthread_body + 0
4   libsystem_pthread.dylib       	0x00007fff6b35fc5d thread_start + 13

Thread 3 (id: 4634106):
0   libsystem_pthread.dylib       	0x00007fff6b35fc40 start_wqthread + 0

Thread 4 (id: 4635885):
0   libsystem_kernel.dylib        	0x00007fff6b226c1a fsetxattr + 10
1   CoreFoundation                	0x00007fff43b8f3de -[__NSDictionaryM __apply:context:] + 94
2   Metadata                      	0x00007fff45190de8 _MDSetExtendedAttributes + 53
3   mds                           	0x0000000109cc0503
4   SpotlightServerKit            	0x00007fff65e25db8 mds_dispatch_nocopy_wrapper + 92
5   SpotlightServerKit            	0x00007fff65e26038 mds_dispatch_wrapper + 14
6   libdispatch.dylib             	0x00007fff6b09cf64 _dispatch_client_callout + 8
7   libdispatch.dylib             	0x00007fff6b0b142b _dispatch_queue_serial_drain + 635
8   libdispatch.dylib             	0x00007fff6b0a430e _dispatch_queue_invoke + 373
9   libdispatch.dylib             	0x00007fff6b0b2117 _dispatch_root_queue_drain_deferred_wlh + 332
10  libdispatch.dylib             	0x00007fff6b0b5ef0 _dispatch_workloop_worker_thread + 880
11  libsystem_pthread.dylib       	0x00007fff6b360033 _pthread_wqthread + 980
12  libsystem_pthread.dylib       	0x00007fff6b35fc4d start_wqthread + 13
13                                	0x0000000300000048

Thread 5 (id: 4637826):
0   libsystem_kernel.dylib        	0x00007fff6b2266da __workq_kernreturn + 10
1   libsystem_pthread.dylib       	0x00007fff6b35fc4d start_wqthread + 13
2                                 	0x7265506b63656843

Binary Images:
       0x109ba2000 -        0x109d51ff7  mds (1191) <2ab9e150-475e-3909-8706-53cfdf066288> /System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Support/mds
    0x7fff43aea000 -     0x7fff43f89fff (6.9 - 1443.13) <2881430b-73e5-32c1-b62d-7ceb68a616f5> /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation
    0x7fff4516c000 -     0x7fff4521bff3 (10.7.0 - 1191) <32c57f7b-dac3-3f12-8dea-1cb5a062dc02> /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/Metadata.framework/Versions/A/Metadata
    0x7fff65bdd000 -     0x7fff65e18ff3 (10.7.0 - 1191) <7f436ff6-135e-322e-9834-44cf7ed1699a> /System/Library/PrivateFrameworks/SpotlightIndex.framework/Versions/A/SpotlightIndex
    0x7fff65e1d000 -     0x7fff65e2cfff (1.0 - 1191) <f408c4ff-8213-307c-beb9-b3f5854712ab> /System/Library/PrivateFrameworks/SpotlightServerKit.framework/Versions/A/SpotlightServerKit
    0x7fff6b09b000 -     0x7fff6b0d4ff7  libdispatch.dylib (913.1.6) <0dd78497-6a2a-350a-99ef-15bf41ea07dd> /usr/lib/system/libdispatch.dylib
    0x7fff6b0d5000 -     0x7fff6b0f2ff7  libdyld.dylib (519.2.1) <2597d818-42d2-3375-bd9d-451d5942a6ba> /usr/lib/system/libdyld.dylib
    0x7fff6b20a000 -     0x7fff6b22fff7  libsystem_kernel.dylib (4570.1.46) <71ba15cb-3056-3cbd-a5f5-ee61566eea0c> /usr/lib/system/libsystem_kernel.dylib
    0x7fff6b35d000 -     0x7fff6b368ff7  libsystem_pthread.dylib (301.1.6) <6d0b0110-2b44-3d3c-b672-bd08fe46378a> /usr/lib/system/libsystem_pthread.dylib

This is triggered when I launch a terminal command to use osascript to send a command to DEVONthink to update the tag for an item (and I guess that makes DEVONthink hit the item’s metadata on disk, which is where the violation is coming from).

So I suspect that it works from the terminal as it trusts me as a human user, but when I invoke it through scripts from DEVONthink or Script Editor, it sandboxes me and assumes I’m acting maliciously.

How do I work around this? It’s so close!

Actually I’m terrified, like the villagers confronted by Frankenstein’s monster!! :mrgreen:

Not sure I can help on this as JXA is not my primary language, is indeed poorly documented in general, and a mélange such as this feels fragile or bolted together. I am having a play about with it but perhaps houthakker has done such an operation in DTPO with JXA.

This actually a very apropros metaphor. I’m sorry! :slight_smile:

It is brittle, but it almost works for now, and from a working foundation may come some experience and a less fragile re-write. TBH the main reason I went JS with it was for regexes, and what seemed to be a clearer data model exposed. AppleScript is very friendly for quick-hits - your examples are exemplars of brevity - but for more complex data structures, it looks a little unwieldy.

It seems it’s one of those weirdly unexpected errors that’s sprung up from a corner you didn’t realise existed - I suspect it’s a sandboxing issue, perhaps, whereby the terminal invocation of osascript to call methods on DTPo loses connection to some permissions or authorisations, which is causing the failure. It may be as simple as a running the terminal command as a different user, or preserving the running environment, or it may be a house built on quicksand, in a floodplain, on a rainy day.

But until the storm comes, I’ve almost got the door wedged in!

I actually use do shell script a lot and the shell rocks! (bash, is my girlfriend :smiley:)
AppleScript with bash rolled in makes for some pretty amazing stuff, honestly.

After a side conversation with houthakker, I have been messing about with JXA as it does have far better array handling methods (and AppleScript is known for mediocre list processing). But I still assert that Javascript itself is a terse and mysterious language for beginners (note I didn’t say bad or lacking power). I believe there are people who intuit Javascript, but I believe they are the minority (and not in a bad way) or they have come from similar languages. AppleScript is far easier to teach the complete noob. :smiley:

From horse-back (forgive me for not reading a long thread carefully, and so perhaps for missing the point)

Is this what you are after ?

(() => {
    'use strict';

    const rec =  Application('DEVONthink Pro').selection()[0];

    rec.tags = rec.tags().concat('Delta');

    return rec.tags();

Or if you are using a pre Sierra system, then in ES5 JS

(function () {
    'use strict';

    var rec = Application('DEVONthink Pro').selection()[0];

    rec.tags = rec.tags().concat('Delta');

    return rec.tags();

i.e. the get function ```


and the set property ```


``` expects an Array of Strings.

PS the core

rec.tags = rec.tags().concat(strTag)

can, of course, be variously wrapped.

Here, for example, we map a (curried) tagAdded function over all the selected records:

(() => {
    'use strict';

    // curry :: Function -> Function
    const curry = f => x => y => f(x, y);

    // tagAdded :: String -> DT Record -> DT Record
    const tagAdded = curry((strTag, rec) => (
        rec.tags = rec.tags()

    // TEST --------------------------------------------

    const strTag = 'extraTag';

    // Tagging mapped over all selected records
    return Application('DEVONThink Pro')

Applescript can also map curried functions, but it gets a bit out of breath:

-- tagAdded :: String -> DT Record -> DT Record
on tagAdded(strTag, rec)
    using terms from application "DEVONthink Pro"
        set tags of rec to ((tags of rec) & strTag)
        return rec
    end using terms from
end tagAdded

-- TEST ------------------------------------------------------------
on run
    tell application "DEVONthink Pro" to ¬
        set lstSeln to selection
    set strTag to "extraTag"
    map(curry(tagAdded)'s |λ|(strTag), lstSeln)
end run

-- GENERIC FUNCTIONS -----------------------------------------
-- curry :: (Script|Handler) -> Script
on curry(f)
        on |λ|(a)
                on |λ|(b)
                    |λ|(a, b) of mReturn(f)
                end |λ|
            end script
        end |λ|
    end script
end curry

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- Lift 2nd class handler function into 1st class script wrapper 
-- mReturn :: Handler -> Script
on mReturn(f)
    if class of f is script then
            property |λ| : f
        end script
    end if
end mReturn

Alternatively, assuming the same generic functions:

-- TEST ------------------------------------------------------------
on run
	tell application "DEVONthink Pro"
		set lstSeln to selection
		-- tagAdded :: String -> DT Record -> DT Record
		script tagAdded
			on |λ|(strTag, rec)
				set tags of rec to ((tags of rec) & strTag)
			end |λ|
		end script
	end tell
	set addExtra to curry(tagAdded)'s |λ|("extraTag")
	map(addExtra, lstSeln)
end run

houthakker, stop it!

Thanks so much for your code. I still have to sit down to read it, and trying to pull parts out to re-purpose leaves me short of breath, and none the wiser.

In the meantime, may I present the first draft of Stagger, the stupid tagger? It’s pretty much all your code anway, with a couple of lines of glue from me.

My initial tests have show it’s saved me almost minutes of trying to remember my tag hierarchy, helped me avoid missing dozens of relevant tags, and helped Ammonite step up its game no end. It’s put the Al in False Positive.

It’s very naïf in its implementation, which is partly by design, but fits some of my thinking in a way that DEVONthink only partly addresses. I’d like to think of it as the least bad implementation of this approach, currently.