-- TAG DELETION SCRIPT (User-Specified Count)
-- Define variables
set tagsToDelete to {}
set tagNamesToDelete to {}
set targetCount to 0
try
-- === STEP 1: GET USER INPUT ===
repeat until targetCount > 0
try
set dialogResult to display dialog "Enter the exact number of items a tag must have to be deleted:" default answer "1" buttons {"Cancel", "Continue"} default button "Continue"
set userInputText to text returned of dialogResult
set targetCount to userInputText as integer
if targetCount < 1 then
display alert "Invalid Number" message "Please enter a whole number greater than 0."
set targetCount to 0
end if
on error number errNum
if errNum is -128 then error "User cancelled the operation." number -128
display alert "Invalid Input" message "Please enter a valid whole number (e.g., 1, 2, 3)."
end try
end repeat
set pluralSuffix to "s"
if targetCount is 1 then set pluralSuffix to ""
-- === STEP 2: FINDING (The Robust Method) ===
tell application id "DNtp"
set theDatabase to current database
if not (exists theDatabase) then error "No database is open."
set theTagsGroup to get record at "/Tags" in theDatabase
set allTagRecords to children of theTagsGroup
set totalTagCount to count of allTagRecords
if totalTagCount is 0 then
error "No tags found in the database to check."
end if
show progress indicator "Finding tags with " & targetCount & " item" & pluralSuffix & "..." steps totalTagCount
-- This loop is reliable. For each tag, we ask DEVONthink for its usage count.
repeat with aTagRecord in allTagRecords
set theTagName to name of aTagRecord
step progress indicator "Checking: " & theTagName
set foundRecords to lookup records with tags {theTagName} in theDatabase
-- The condition uses the user-provided 'targetCount' variable.
if (count of foundRecords) is targetCount then
set end of tagsToDelete to aTagRecord
set end of tagNamesToDelete to theTagName
end if
end repeat
hide progress indicator
end tell
set deletionCount to count of tagsToDelete
if deletionCount is 0 then
display dialog "Process complete. No tags were found with exactly " & targetCount & " item" & pluralSuffix & "."
return
end if
-- === STEP 3: CONFIRMATION DIALOG ===
set AppleScript's text item delimiters to ", "
set tagListForDisplay to tagNamesToDelete as text
set AppleScript's text item delimiters to ""
display dialog "Found " & deletionCount & " tags with exactly " & targetCount & " item" & pluralSuffix & " each." & return & return & "Are you sure you want to permanently delete these " & deletionCount & " tags in a single batch?" & return & return & "Example Tags: " & tagListForDisplay buttons {"Cancel", "Delete All"} default button "Cancel" cancel button "Cancel"
-- === STEP 4: EFFICIENT DELETING ===
tell application id "DNtp"
show progress indicator "Deleting " & deletionCount & " tags in a single batch..." steps -1
delete record tagsToDelete
hide progress indicator
end tell
-- === STEP 5: FINAL REPORT ===
display dialog "Deletion Complete!" & return & return & "Successfully deleted " & deletionCount & " tags that had " & targetCount & " item" & pluralSuffix & ":" & return & return & tagListForDisplay
on error errMsg number errNum
if errNum is -128 then return
try
tell application id "DNtp" to hide progress indicator
end try
display alert "An error occurred:" & return & "Error " & errNum & ": " & errMsg
end try
I suppose that one is AI-generated.
and?
It does what it says, doesnât it?
And no, Iâm not thinking of javascript
Comments. Nobody except AI comment so profusely.
Anyway, yesterday Gemini did me one script for a Smart Rule that worked at first try. Without that, I could not have been able to do it by myself. Not in 100 years of JS study.
And so uselessly. Instead of explaining the overall structure of the code, it verbalizes the obvious, like testing for a condition that is there for everybody to see.
And, since it has no idea what it is doing, it puts the steps in the wrong order â why asking someone for the number of tags to delete if thereâs no database open in the first place?
I doubt that.
Iâd rather not post anything. People can use AI on their own.
Since thereâs code, we can discuss the code and how to improve it. For example, in omitting a useless step indicator
in the context of batch deletion. Or in not creating the same string twice, once as tagListForDisplay
and once as finalReportText
And just for educational purposes, hereâs a JS version of the script. Considerably shorter and less cruft. Purely for educational purposes.
(() => {
const app = Application("DEVONthink");
app.includeStandardAdditions = true;
const database = app.currentDatabase();
if (!database) {
app.displayAlert('No database selected');
// No need to continue here
return;
}
let tagCount = 0;
while (tagCount < 1) {
const dialogResult = app.displayDialog('Enter number of items a tag must have to be deleted', {defaultAnswer: 1, buttons: ['Cancel', 'OK'], defaultButton: 'OK'});
tagCount = +dialogResult.textReturned;
}
const pluralForm = tagCount > 1 ? "s" : "";
// Get the current database's tags group
const tagsGroup = database.tagsGroup();
// get all children of the tags group and bail out if there are none
const tagRecords = tagsGroup.children();
if (!tagRecords.length) {
app.displayAlert(`No tags found in database "${database.name()}"`);
return;
}
// matchingRecords stores all tags with exactly `tagCount` tags
const matchingRecords = [];
tagsGroup.children().forEach(tagRecord => {
const foundRecords = app.lookupRecordsWithTags([tagRecord.name()]);
if (foundRecords.length === tagCount) {
matchingRecords.push(tagRecord);
}
})
// Bail out if no tags with exactly `tagCount` children found
const deletionCount = matchingRecords.length;
if (! deletionCount) {
app.displayAlert(`Process complete. No tags were found with exactly ${tagCount} item${pluralForm}.`);
return
}
// Ask for user confirmation before deletion
const tagListForDisplay = matchingRecords.map(r => r.name()).join(', ');
app.displayDialog(`Found ${deletionCount} tags with exactly ${tagCount} item${pluralForm} each.
Are you sure you want to permanently delete these ${deletionCount} tags in a single batch?
Example Tags: ${tagListForDisplay}`,
{buttons: ["Cancel", "Delete All"], defaultButton: "Cancel", cancelButton: "Cancel"});
// Delete records. Note: This code will not run if the user cancels the dialog above
app.delete({record: matchingRecords});
// One last information for the user
app.displayAlert(`Deletion Complete!
Successfully deleted ${deletionCount} tags that had ${tagCount} item${pluralForm} :
${tagListForDisplay}`)
})()
A script that does what?
I use to print to PDF from RSS entry (CMD+P, CMD+D -custom shortcut). This ends a âFeeds - Whateverâ in Global Inbox. The script removes prefix and postfix.
-- DEVONthink Smart Rule Script to Clean Feed Titles
-- This script removes a specific prefix and a trailing quote from the name of a record.
-- Example Input: Feeds â "This is the title"
-- Example Output: This is the title
on performSmartRule(theRecords)
tell application id "DNtp"
try
-- Loop through all records passed to the smart rule
repeat with eachRecord in theRecords
-- Get the current name of the record
set currentName to name of eachRecord
-- Define the prefix to be removed
set prefixToRemove to "Feeds â \""
-- Check if the name starts with the defined prefix
if currentName starts with prefixToRemove then
-- Calculate the length of the prefix
set prefixLength to length of prefixToRemove
-- Extract the substring that comes after the prefix
set newName to (rich texts (prefixLength + 1) through -1 of currentName)
-- Check if the new name ends with a quote
if newName ends with "\"" then
-- Remove the last character (the trailing quote)
set newName to (rich texts 1 through -2 of newName)
end if
-- Update the record's name only if it has changed
if newName is not equal to currentName then
set name of eachRecord to newName
end if
end if
end repeat
on error error_message number error_number
-- Log any errors that occur to the DEVONthink log window for debugging
log message "Smart Rule Error: " & error_message & " (" & error_number & ")"
end try
end tell
end performSmartRule
Wouldnât a scan name
action, perhaps with a regular expression, followed by a set name
action, have done the same thing?
RE Feeds - "(.*?)"
set name \1
I donât have the original script to go on, but based on the title of the threadâŚ
tell application id "DNtp"
set minTags to 2
set unusedList to {}
set alltags to (every parent of (current database) whose (tag type is ordinary tag))
repeat with theTag in alltags
if ((count children) of theTag) ⤠minTags then
copy theTag to end of unusedList
end if
end repeat
if unusedList is not {} then delete record unusedList
end tell
Indeed and it also can be accomplished with the simpler String parameter in this caseâŚ
And with DEVONthink 4âs new Find & Replace smart actionâŚ
I told you I have no idea of JS and donât want to have. The AI generated one is enough to me. Perhaps your solution is more elegant, but I donât mind for a script that is going to run not more than one or two times a day.
A script isnât necessary in your case. In fact, most filenamings people want to accomplish require no scripting at all.
@chrillek, your JS version is syntactically clean, Iâll give you that. But your critiques were a mix of academic nitpicks and missing the forest for the trees.
- âUseless step indicatorâ: Wrong. Your script provides zero feedback during the longest phaseâthe initial scan. A frozen UI is a crashed UI to the user. A proper progress bar is a critical user-experience feature, not âcruft.â
- âNot creating the same string twiceâ: This is a micro-optimization that saves microseconds. Itâs irrelevant to real-world performance.
The real lesson here came from @BLUEFROG, who inadvertently gave us the perfect example of the âelegant, hand-written codeâ fallacy:
-- BLUEFROG's proposed script
tell application id "DNtp"
set minTags to 2
set unusedList to {}
set alltags to (every parent of (current database) whose (tag type is ordinary tag))
repeat with theTag in alltags
if ((count children) of theTag) ⤠minTags then
copy theTag to end of unusedList
end if
end repeat
if unusedList is not {} then delete record unusedList
end tell
This script looks great. Itâs concise. Itâs from an expert. And itâs completely, fundamentally broken. It doesnât count document usage at all. It counts sub-tags. It would delete a vital tag used on 500 documents simply because it has no sub-tags.
This brings me back to the original point: The origin of a scriptâwhether AI-assisted or hand-carved on a tablet by a masterâis irrelevant. All that matters is if it works correctly. Elegance is worthless if the logic is flawed. A âuselessâ but correct script is infinitely better than an âelegantâ but broken one.
The Script That Actually Works
This is the final version. It works because:
- Itâs Correct: It uses
lookup records with tags
, which is the only reliable way to count document usage. It doesnât fall into thechildren
trap. - Itâs Fast: It uses a single batch-delete command.
- Itâs Usable: It has a progress bar that actually informs the user during the slow part.
-- TAG DELETION SCRIPT (User-Specified Count)
-- Define variables
set tagsToDelete to {}
set tagNamesToDelete to {}
set targetCount to 0
try
-- === STEP 1: GET USER INPUT ===
repeat until targetCount > 0
try
set dialogResult to display dialog "Enter the exact number of items a tag must have to be deleted:" default answer "1" buttons {"Cancel", "Continue"} default button "Continue"
set userInputText to text returned of dialogResult
set targetCount to userInputText as integer
if targetCount < 1 then
display alert "Invalid Number" message "Please enter a whole number greater than 0."
set targetCount to 0
end if
on error number errNum
if errNum is -128 then error "User cancelled the operation." number -128
display alert "Invalid Input" message "Please enter a valid whole number (e.g., 1, 2, 3)."
end try
end repeat
set pluralSuffix to "s"
if targetCount is 1 then set pluralSuffix to ""
-- === STEP 2: FINDING (The Robust Method) ===
tell application id "DNtp"
set theDatabase to current database
if not (exists theDatabase) then error "No database is open."
set theTagsGroup to get record at "/Tags" in theDatabase
set allTagRecords to children of theTagsGroup
set totalTagCount to count of allTagRecords
if totalTagCount is 0 then
error "No tags found in the database to check."
end if
show progress indicator "Finding tags with " & targetCount & " item" & pluralSuffix & "..." steps totalTagCount
-- This loop is reliable. For each tag, we ask DEVONthink for its usage count.
repeat with aTagRecord in allTagRecords
set theTagName to name of aTagRecord
step progress indicator "Checking: " & theTagName
set foundRecords to lookup records with tags {theTagName} in theDatabase
-- The condition uses the user-provided 'targetCount' variable.
if (count of foundRecords) is targetCount then
set end of tagsToDelete to aTagRecord
set end of tagNamesToDelete to theTagName
end if
end repeat
hide progress indicator
end tell
set deletionCount to count of tagsToDelete
if deletionCount is 0 then
display dialog "Process complete. No tags were found with exactly " & targetCount & " item" & pluralSuffix & "."
return
end if
-- === STEP 3: CONFIRMATION DIALOG ===
set AppleScript's text item delimiters to ", "
set tagListForDisplay to tagNamesToDelete as text
set AppleScript's text item delimiters to ""
display dialog "Found " & deletionCount & " tags with exactly " & targetCount & " item" & pluralSuffix & " each." & return & return & "Are you sure you want to permanently delete these " & deletionCount & " tags in a single batch?" & return & return & "Example Tags: " & tagListForDisplay buttons {"Cancel", "Delete All"} default button "Cancel" cancel button "Cancel"
-- === STEP 4: EFFICIENT DELETING ===
tell application id "DNtp"
show progress indicator "Deleting " & deletionCount & " tags in a single batch..." steps -1
delete record tagsToDelete
hide progress indicator
end tell
-- === STEP 5: FINAL REPORT ===
display dialog "Deletion Complete!" & return & return & "Successfully deleted " & deletionCount & " tags that had " & targetCount & " item" & pluralSuffix & ":" & return & return & tagListForDisplay
on error errMsg number errNum
if errNum is -128 then return
try
tell application id "DNtp" to hide progress indicator
end try
display alert "An error occurred:" & return & "Error " & errNum & ": " & errMsg
end try
That is incorrect. It does not count sub-tags. It counts the children of the tag, which could be nested tags but more commonly would be documents. You have no brief indicating specifics of the tag structure and my script was a proof-of-concept to contrast with the incredibly verbose (and inefficient) AI-generated script.
Be that as it may, but in this case I didnât say anything about JS. I just tried to explain that you do not need any script for such trivial task.
But if you prefer to script, be my guest.
This remark was clearly targeted at the delete
phase. I deliberately chose to not add the step thingy to the collecting phase, because I doubt that it takes very long.
Itâs not about performance, youâre right there. Itâs about clarity. Several lives of code that do the same thing, without a comment explaining that.
That depends on the context. For the person that runs it â yes. For someone who wants to understand the code, other aspects are also important.
I followed the dictionary for DT4
- What is a child?
The dictionary states under the definition for the record class, we see:
child n, pl children [inh. record]: A child record of a group.
ELEMENTS
contained by records.
2.Conclusion: A child is an item that is hierarchically contained inside another record that is a group. Itâs like a file inside a folder.
- What is a tag?
The dictionary defines a tag as a type of group. (tag type enum):
ordinary tag : An âordinaryâ tag located inside of the tags group.
group tag : A âgroupâ tag located outside of the tags group.
4.Conclusion: A tag is a special kind of group that lives in a specific hierarchy. Since itâs a group, it can contain children.
-
The Logical Connection:
If a child is an item contained inside a group, and a tag is a group, then the children of a tag can only be other records contained inside it âi.e., its sub-tags. Documents are not contained inside tags; they live elsewhere in the database and have a link or relationship to the tag. -
The lookup records with tags Command
Why does the lookup records with tags command exist at all?
lookup records with tags v : Lookup records with all or any of the specified tags.
â list of record or missing value: The found records.
If getting the documents for a tag was as simple as asking for its children, this command would be completely redundant.
@rfog Why do you use the print dialog instead of Convert > to PDF (Paginated) ? The PDF results I get are nearly identical. But the convert command doesnât add <Database> â " "
around the filename.
@BLUEFROG I donât think the String parameter works. When you bring up the print dialog and use DTs PDF service from within DT, the resulting file is named <Database> â "<Filename.ext>".pdf
(Also, itâs an em-dash). Even with â "*
, you still need to remove the end quote.
This regular expression should work for any database name. It also gets rid of the file extension from the original filename:
^.+ â "(.+?)(?:\.[a-zA-Z]+)?"$