Smart Rule fails only when triggered as a smart rule: (Can’t get name of «class DTcn» id 1296597 of «class DTkb» id 2.)

Hi folks! I’m getting to the pulling-hair stages of debugging a script I’ve been building.

I have an external AppleScript executing from a smart rule. It does, well, a lot… but it works perfectly when I trigger it manually. However, when it is triggered via the Smart Rule, it fails with the error:

2022-04-24, 9:11:33 PM: ~/Dropbox/Portable/Meta/Workflow scripts, templates, and resources/Workflows/DEVONthink Scripts/Smart Rules/Create new resource file for new readings and add them to my reading list file.scpt	on performSmartRule (Can’t get name of «class DTcn» id 1296597 of «class DTkb» id 2.)

The weirdest thing is that when I trigger it manually, I use:

tell application id "DNtp"
	set theSelection to get the selection
	my performSmartRule(theSelection)
end tell

So, I would have thought that my testing is completely simulating the Smart Rule trigger environment… Why is it failing?!

I’ll post the entire script below. I’ve highlighted where the script fails with -- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️. I imagine this is something simple I’m overlooking.

Thanks in advance (hopefully)!

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"

property readingListFileUUID : "0B840A80-ACC6-422E-930B-01F31EA58DE6" -- must be a UUID that points to a plain text file.
property debug : false
property import : false
property overwriteCitekey : false -- if this value is true, it will overwrite the citekeys of publications
property abbreviatedTitleLength : 6 -- the number of words to append to the citekey from the title

on performSmartRule(theRecords)
	tell application id "DNtp"
		my addToReadingListFile(theRecords)
	end tell
end performSmartRule

-- debug routine. if you set debug to true above, then run this script manually, it will test the script on the selection in DEVONthink. if debug is false, this doesn't do anything.
if debug then
	tell application id "DNtp"
		set theSelection to get the selection
		my performSmartRule(theSelection)
	end tell
end if

if not debug then
	if import then
		tell application id "DNtp"
			set theSelection to get the selection
			my addToReadingListFile(theSelection)
		end tell
	end if
end if

on addToReadingListFile(someRecords)
	tell application id "DNtp"
		log "Started the addToReadingListFile method."
		-- initialize the list of items to be added to the reading list file
		set listOfAddedLines to ""
		-- get the details from each item to be added to the reading list
		repeat with eachRecord in someRecords
			-- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
			set theName to name of eachRecord
			set theLink to reference URL of eachRecord
			log "Record name is " & theName
			set readingListItemLine to "- [ ] [[𖠫 " & theName & "]]" & return -- this is the line with reference details that will be added to the reading list file. edit it to customize what each added line looks like
			if debug then
				display dialog "Running smart rule on '" & readingListItemLine & "'"
			end if
			set listOfAddedLines to listOfAddedLines & readingListItemLine -- appends the readingListItemLine to the list of items to be added to the reading list file
			
			my createNewResourceFile(eachRecord)
		end repeat
		
		-- get the reading list file and its current text
		set theReadingListFile to get record with uuid readingListFileUUID
		set currentReadingListText to plain text of theReadingListFile
		
		-- update the text
		set newReadingListText to currentReadingListText & listOfAddedLines
		
		-- save the text into the reading list file
		set plain text of theReadingListFile to newReadingListText
		
	end tell
end addToReadingListFile


on createNewResourceFile(someRecord)
	
	set indentationCharacter to tab -- tab is a reserved keyword in DEVONthink scripting, so I'm instantiating it here as the character we'll use to indent some text in variables below
	
	
	tell application id "DNtp"
		set YAMLdelimiter to "---"
		set recordName to someRecord's name without extension
		set citekeyAlias to ""
		set citekeyYAML to "citekey: "
		-- get the citekey from Bookends for the resource file's alias
		tell application "Bookends"
			try
				set thePublications to (sql search "attachments REGEX '" & recordName & "'")
				if (thePublications is not {}) then -- the item _is_ in Bookends
					set thePublication to the first item in thePublications -- assume it's only attached to one publication
					if debug then
						display dialog ("'" & thePublication's title as text) & "' citekey is: " & thePublication's citekey as text
					end if
					if thePublication's citekey as text is "" then -- empty citekey
						-- # the below items are commented out in favour of a title-appended citekey approach
						-- set citekeyPromptMessage to ("Set the citekey for \"" & thePublication's title & "\" by :" & return & thePublication's authors & ".") -- prompt the user for a citekey
						-- set publicationAuthors to thePublication's author names
						-- set firstAuthor to the first item in publicationAuthors
						-- try
						-- set firstAuthorLastName to the first item in (my splitText(firstAuthor, ","))
						--on error
						--	set firstAuthorLastName to firstAuthor
						--end try
						--set defaultCitekey to (firstAuthorLastName & thePublication's publication date string)
						--set citekeyPrompt to display dialog citekeyPromptMessage default answer defaultCitekey buttons {"Cancel", "Set citekey"} default button "Set citekey"
						-- set publicationCitekey to my checkForRedundantCitekeys(text returned of citekeyPrompt)
						if debug then
							display dialog "Citekey is blank. Generating new citekey."
						end if
						set publicationCitekey to my generateNiceCitekey(thePublication)
						if debug then
							display dialog "New citekey: " & publicationCitekey
						end if
						set citekeyAlias to publicationCitekey
					else -- citekey already exists
						if overwriteCitekey then
							set publicationCitekey to my generateNiceCitekey(thePublication)
							
						end if
						set citekeyAlias to publicationCitekey
					end if
				end if
			end try
		end tell
		
		if citekeyAlias is not "" then
			set aliasYAML to "alias:" & return & "  - " & citekeyAlias
			set citekeyYAML to citekeyYAML & citekeyAlias
		else
			set aliasYAML to "alias:"
		end if
		
		set devonthinkYAML to "devonthink-link: x-devonthink-item://" & someRecord's uuid
		set theTagsAsString to ""
		set recordTags to someRecord's tags
		if (count of someRecord's tags) is 2 then
			set theTagsAsString to "  - " & the second item of recordTags as text
		else
			set firstTag to the second item in recordTags as text -- the first tag is always `.presets`, a utility tag that doesn't tell us anything.
			set remainingTags to items 3 through (count of recordTags) of recordTags
			set theTagsAsString to "  - " & firstTag
			repeat with eachTag in remainingTags
				if eachTag as text does not contain "preset" then
					set theTagsAsString to theTagsAsString & return & "  - " & eachTag as text
				end if
			end repeat
		end if
		set tagsYAML to "tags:" & return & theTagsAsString
		set wordCount to word count of someRecord
		set estimatedReadingMinutes to wordCount / 200
		set readtimeYAML to "minutes_to_read: " & estimatedReadingMinutes
		set summaryYAML to "summary: "
		set potentialImpactYAML to "potential_impact: "
		set YAML to YAMLdelimiter & return & aliasYAML & return & tagsYAML & return & devonthinkYAML & return & citekeyYAML & return & readtimeYAML & return & summaryYAML & return & potentialImpactYAML & return & YAMLdelimiter & return
		
		-- a Dataview Query for returning any annotation stream files
		set annotationNotesDataviewQuery to "```dataviewjs" & return & "let summaryPages = dv.pages('\"Notes\"')" & return & indentationCharacter & ".where(p => p.file.name.includes(\"∎ " & name without extension of someRecord & "\"))" & return & indentationCharacter & ".map(p => [p.file.link, p.annotation_status])" & return & return & "dv.table([\"Annotation Summary\", \"Status\"], summaryPages)" & return & "```"
		
		-- set the text of the resource file
		set resourceFileText to YAML & return & return & "# [" & someRecord's name without extension & "](" & someRecord's reference URL & ")" & return & return & "![[" & someRecord's filename & "]]" & return & return & "## Annotations" & return & return & annotationNotesDataviewQuery
		
		-- get the group of the resource file
		set destinationGroup to someRecord's location group
		set newRecord to create record with {name:"𖠫 " & name without extension of someRecord, content:resourceFileText, type:markdown} in destinationGroup
		if debug then
			set theWindow to open window for record newRecord
			activate
		end if
	end tell
end createNewResourceFile

-- Generates and sets a citekey for the publication
on generateNiceCitekey(somePublication)
	tell application "Bookends"
		tell front library window
			
			set thePublication to somePublication
			
			if not overwriteCitekey then -- don't overwrite citekeys if they already exist
				if thePublication's citekey is not "" then
					return thePublication's citekey
				end if
			end if
			
			
			-- Otherwise, generate a new citekey
			set thePublication's citekey to "" as text
			set theTitle to title of thePublication
			
			set cleanedTitle to my cleanText(theTitle)
			set titleAsList to my splitText(cleanedTitle, " ")
			try
				set abbreviatedTitle to my getFirstFiveWords(titleAsList) -- convert theTitle into its first five words
			on error
				set abbreviatedTitle to the first item of titleAsList & the second item of titleAsList & the third item of titleAsList
			end try
			if debug then
				display dialog abbreviatedTitle as text
			end if
			set publicationDate to thePublication's publication date string
			if debug then
				display dialog abbreviatedTitle
				display dialog publicationDate
			end if
			if length of publicationDate is 4 then
				try
					set theYear to publicationDate as number -- if this succeeds, the date string is a four-digit number, thereby likely a year.
				end try
			else --length of the date string is not 4, so let's try to get a year out of it
				try
					set theYear to my dateStringToYear(publicationDate) -- parse for year
				on error
					display notification "There was an issue extracting the publication's year. Using date added instead."
					set dateAdded to thePublication's date added
					set theYear to my dateStringToYear(dateAdded)
				end try
				if theYear is "" then -- deal with no year in the date string
					set dateAdded to thePublication's date added
					set theYear to my dateStringToYear(dateAdded)
				end if
			end if
			
			
			set authorField to authors of thePublication
			set firstAuthor to first item in my splitText(authorField, "
")
			if firstAuthor contains "," then
				set firstAuthor to first item in my splitText(firstAuthor, ",")
			end if
			
			
			set firstCitekey to firstAuthor & theYear & abbreviatedTitle
			set newCitekey to my (checkForRedundantCitekeys(firstCitekey))
			set thePublication's citekey to newCitekey
			display notification "Generated a new citekey for '" & theTitle & "' in Bookends: " & newCitekey as text
			return thePublication's citekey
		end tell
	end tell
end generateNiceCitekey

-- check for used citekeys
on checkForRedundantCitekeys(someCitekey)
	tell application "Bookends"
		set redundantCitekeyPublications to (sql search "user1 REGEX '" & someCitekey & "'")
		if ((count of redundantCitekeyPublications) = 0) then
			return someCitekey
		else
			set citekeyPromptMessage to ("The citekey " & someCitekey & " is already in use for the publication titled: " & return & return & "'" & title of first item of redundantCitekeyPublications & "'" & return & return & "Please provide a different citekey:")
			set newTextPrompt to display dialog citekeyPromptMessage default answer "" buttons {"Cancel", "Try this citekey"} default button "Try this citekey"
			set newCitekey to text returned of newTextPrompt
			return my checkForRedundantCitekeys(newCitekey)
		end if
	end tell
end checkForRedundantCitekeys

on cleanText(someString)
	set illegalCharacters to {"-", "/", "\\", ":", "|", "?", "'", ".", "&", "–", "_", "—", ","}
	set newString to someString
	repeat with illegalCharacter in illegalCharacters
		if newString contains illegalCharacter then
			if (illegalCharacter is "?") then
				set newString to my findAndReplaceInText(newString, illegalCharacter, "")
			else if (illegalCharacter is "-") then
				set newString to my findAndReplaceInText(newString, illegalCharacter, " ")
			else if (illegalCharacter is "'") then
				set newString to my findAndReplaceInText(newString, illegalCharacter, "")
			else if (illegalCharacter is "\"") then
				set newString to my findAndReplaceInText(newString, illegalCharacter, "")
			else if (illegalCharacter is ".") then
				set newString to my findAndReplaceInText(newString, illegalCharacter, "")
			else if (illegalCharacter is "&") then
				set newString to my findAndReplaceInText(newString, illegalCharacter, "and")
			else
				set newString to my findAndReplaceInText(newString, illegalCharacter, " ")
			end if
		end if
	end repeat
	return newString
end cleanText


on getFirstFiveWords(someListOfWords)
	--	set stopWords to {'all', 'just', 'being', 'over', 'both', 'through', 'yourselves', 'its', 'before', 'herself', 'had', 'should', 'to', 'only', 'under', 'ours', 'has', 'do', 'them', 'his', 'very', 'they', 'not', 'during', 'now', 'him', 'nor', 'did', 'this', 'she', 'each', 'further', 'where', 'few', 'because', 'doing', 'some', 'are', 'our', 'ourselves', 'out', 'what', 'for', 'while', 'does', 'above', 'between', 't', 'be', 'we', 'who', 'were', 'here', 'hers', 'by', 'on', 'about', 'of', 'against', 's', 'or', 'own', 'into', 'yourself', 'down', 'your', 'from', 'her', 'their', 'there', 'been', 'whom', 'too', 'themselves', 'was', 'until', 'more', 'himself', 'that', 'but', 'don', 'with', 'than', 'those', 'he', 'me', 'myself', 'these', 'up', 'will', 'below', 'can', 'theirs', 'my', 'and', 'then', 'is', 'am', 'it', 'an', 'as', 'itself', 'at', 'have', 'in', 'any', 'if', 'again', 'no', 'when', 'same', 'how', 'other', 'which', 'you', 'after', 'most', 'such', 'why', 'a', 'off', 'i', 'yours', 'so', 'the', 'having', 'once'}
	set stopWords to {"of", "the", "and", "a", "A", "on", "is"}
	
	set maxLength to length of someListOfWords
	set wordIterator to 1
	set abbreviatedTitleList to {}
	-- iterate through someListOfWords until five words are obtained (removing common words) or the iterator reaches the end of the list
	repeat while (wordIterator ≠ maxLength) -- for the entire possible word list
		set theWord to item wordIterator of someListOfWords
		if theWord is not in stopWords then
			-- convert word case to first-letter-capitalized
			try
				set theWord to my changeCaseOfFirstCharacter(theWord, "upper")
			end try
			
			set abbreviatedTitleList to abbreviatedTitleList & theWord
		end if
		if length of abbreviatedTitleList is abbreviatedTitleLength then
			
			return abbreviatedTitleList as string
		end if
		set wordIterator to wordIterator + 1
	end repeat
	return abbreviatedTitleList as string
end getFirstFiveWords




-- Utility functions from Apple

on changeCaseOfFirstCharacter(theText)
	set uppercaseCharacters to "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	set lowercaseCharacters to "abcdefghijklmnopqrstuvwxyz"
	set firstCharacter to character 1 of theText
	set capitalizedWord to ""
	
	-- capitalize first character
	set theOffset to offset of firstCharacter in lowercaseCharacters
	if theOffset is not 0 then -- the character was found in lowercaseCharacters, so it should be uppercased
		set capitalizedWord to (capitalizedWord & character theOffset of uppercaseCharacters) as string
	else -- character is already the correct case, just add it as-is
		set capitalizedWord to (capitalizedWord & aCharacter) as string
	end if
	
	
	-- lowercase the rest of the word
	set restOfWord to characters 2 thru (length of theText) of theText
	repeat with aCharacter in restOfWord
		set theOffset to offset of aCharacter in uppercaseCharacters
		if theOffset is not 0 then -- the character was found in uppercaseCharacters, so it should be lowercased
			set capitalizedWord to (capitalizedWord & character theOffset of lowercaseCharacters) as string
		else -- character is already the correct case, just add it as-is
			set capitalizedWord to (capitalizedWord & aCharacter) as string
		end if
	end repeat
	return capitalizedWord
end changeCaseOfFirstCharacter

on splitText(theText, theDelimiter)
	set AppleScript's text item delimiters to theDelimiter
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to ""
	return theTextItems
end splitText

on findAndReplaceInText(theText, theSearchString, theReplacementString)
	set AppleScript's text item delimiters to theSearchString
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to theReplacementString
	set theText to theTextItems as string
	set AppleScript's text item delimiters to ""
	return theText
end findAndReplaceInText

(* this script works on these formats: 
"Sunday 10th September 2017", "Sunday 10 September 2017", "Sunday 10 September 17", "September 10th, 2017", "September 10th 2017", "September 10 2017"
 "10th September 2017", "10 September 2017", "10 Sep 17"
 also work with the abbreviation ( e.g. Sep instead of September and Sun instead of Sunday) 
 also work with the localized name of (the month and the day)

"09/10/2017", "09.10.2017", "09-10-2017" : month_day_year only,  or "2016/05/22", "2016-05-22", "2016.05.22" : year_month_day only 
( month and days could be one or two digits)
*)

on dateStringToYear(thisString) -- 
	tell current application
		-- **  finds all the matches for a date, the result is a NSArray ** 
		set m to its ((NSDataDetector's dataDetectorWithTypes:(its NSTextCheckingTypeDate) |error|:(missing value))'s matchesInString:thisString options:0 range:{0, length of thisString})
		if (count m) > 0 then
			set d to (item 1 of m)'s |date|() -- get the NSDate of the first item 
			set df to its NSDateFormatter's new() -- create a NSDateFormatter 
			df's setDateFormat:"yyyy" -- a specified output format: "day/month/year" (day and month = two digits, year = 4 digits)
			return (df's stringFromDate:d) as text
		end if
	end tell
	return "" -- no match in this string
end dateStringToYear
1 Like

I can’t be sure, as I’ve never done anything similar. But it looks like a referencing issue: ‘eachRecord’
is a pointer to a list item, not the item itself, and somehow it is not resolved. If that’s the case the error should go away when you add this line to the top of the loop:

set eachRecord to contents of eachRecord
1 Like

As a first step, I’d reduce the script to the absolute mininum code that reproduces the error. I.e. remove all the if debug and if not debug (why not use an else here, btw?) stuff.

Then, it would be interesting to know if that error appears for all selected records or only for some of them (which?).

But I see @cgrunenberg is at the keyboard right now – he’ll probably have the right idea.

1 Like

Did you try dragging & dropping the same selection which was used for testing onto the smart rule, does this fail too? In addition, is this necessary?

use scripting additions
1 Like

I’ve seen this happen too with scripts using the Foundation framework. Does the script work as inline script instead of an external script?

1 Like

Wow, thanks for the speedy and thoughtful replies!

I think @mdbraber and @cgrunenberg got it—I didn’t actually need the use statements:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"

Now that I’ve removed them, it seems to work splendidly. Finally!

I tend to write scripts like a stampede on a fine china shopping spree. “Oh I could use that shiny thing, and that shiny thing, and that shiny thing!” Meanwhile, behind me, there’s a lot of broken porcelain that I’m not cleaning up. :upside_down_face:

And your dateStringToYear handler works without the use Foundation statement. Amazing.

Even more amazing is that a simple use something should derail an equally simple repeat ... selected records. AppleScript never fails to surprise me.

While I can understand that, it helps neither you nor other people who are trying to understand what your code is doing (which is also made more difficult by the utter lack of comments). Especially if you’re encountering errors, it is helpful to clean up the code. It helps to clarify the logic and (occasionally) to detect flaws.

1 Like

In case anyone’s curious, here’s what the script does.

I index all incoming readings in a single bottleneck Organizer group. I have a Smart Rule that nags me until I tag those items from a preset selection of tags. Once they’re tagged, the Smart Rule moves them to a new folder (I organize all readings by the semester I first downloaded them) and adds a .to-organize tag (which I should probably rename to .just-organized…).

The script here fires on all files tagged with .to-organize. It does three seemingly-simple things:

  1. it creates a new “Resource File,” which is sort-of a Zettelkasten literature note,
  2. it makes the reading’s annotation note the newly-created resource note, and
  3. it adds a link to that literature note to a task list in a Reading List file.

The Resource File captures a few important things about the reading in YAML metadata format. In particular, though, it checks to see if the reading is stored as an attachment in my reference manager, Bookends.

If an associated reference is found, it adds the citekey for the resource as both a citekey field and as a YAML alias for the resource file. However, if no citekey exists, the script generates a new citekey of the format firstAuthorLastNameYEARFirstFiveWordsInTitle.

A bunch of these features of the resource note have affordances in Obsidian, hence why I’m using them.

I wonder which statement actually broke the script.

1 Like

And why would including a framework or the scripting additions break an otherwise correct script?

Me too, hah. I removed one at first—I can’t recall if it was "Foundation" or scripting additions—but then the entire script stopped compiling, failing on display dialog.

Which was unexpected.

So I cut all three statements to see if it would compile, and it did, so I ran it, and it worked… which was also unexpected, but it worked!

I’ll try figuring out which one it was and report back.

1 Like

No. Display dialog is part of scripting additions.

If I remove use AppleScript version "2.4" -- Yosemite (10.10) or later, leaving only:

use scripting additions
use framework "Foundation"

I get the error I originally reported in this thread.

If I also remove use framework "Foundation", leaving only use scripting additions, the script starts to fail on a different issue—converting a tag to text.

If I remove use scripting additions and leave use framework "Foundation" and use AppleScript version "2.4" or later, the whole script fails to compile.

Fair enough. It must come bundled in AppleScript versions shipped with recent versions of macOS? I don’t need to add use scripting additions in a brand-new AppleScript in order to invoke display dialog. This works as a complete script:

display dialog "hi"

Actually I never needed this, at least not since the first scriptable betas of DEVONthink Pro 1.0.

1 Like

My bad. I mixed up scripting additions and standard additions. And I was speaking from my experience with JXA – you certainly can’t use displayDialog there without including standard additions. I can imagine that Apple did something magic behind the scenes to automagically include them in AppleScript scripts – in any case, display dialog is defined in standard additions, so they must somehow™ be included in a script that wants to use this method.

All this, however, does not explain why useing a perfectly fine framework would cause a runtime error in a part of the script that does not even use this framework. Nor how the aforementioned handler can run without the Foundation framework being included, given that it references classes defined in Foundation only.

1 Like

Are you using Script Debugger?

Yes! I only just got the hang of it, but I started using it for all scripting last week.

I figured as much.
Those are boilerplate items in Script Debugger, as it anticipates you may use ASOC commands.

Scripting Additions are enabled by default so things like display dialog work out of the box. You’d only need that use scripting additions if you’re using ASOC as well. None of those three commands are needed for pure AppleScript.

PS: I have found the vanilla Script Editor from Apple to be perfectly fine for non-ASOC scripting for many, many years.

1 Like

You’re apparently hard to disappoint :wink: Regardless: What is the problem of using Script Editor when writing AS-ObjC code?