Hello! Claude and me have written a script to find a duplicate item from a “pending” item and replace that by copying an item from a backup database to this place. Except … we’re stuck with the duplicating / replacing step. I can’t figure it out why it does not work (DEVONthink 4). Does it have something to do that the duplicating is in another database?
Here’s the Skript:
-- DEVONthink 4 Script: Replace Pending Item with Found Item
-- This script searches for a matching item in another database and replaces the selected pending item
tell application "DEVONthink"
try
-- Check if DEVONthink is running and has windows
if not (exists think window 1) then
display dialog "Please open DEVONthink and select a pending item." buttons {"OK"} default button "OK"
return
end if
-- Get the selected record (should be the pending item)
set selectedRecords to selection
if (count of selectedRecords) = 0 then
display dialog "Please select a pending item first." buttons {"OK"} default button "OK"
return
end if
if (count of selectedRecords) > 1 then
display dialog "Please select only one pending item." buttons {"OK"} default button "OK"
return
end if
set pendingRecord to item 1 of selectedRecords
-- Get the name and parent of the pending item
set pendingName to name of pendingRecord
set pendingParent to parent of pendingRecord
set currentDatabase to database of pendingRecord
-- Validate the pending record
if pendingName is "" then
display dialog "Selected item has no name." buttons {"OK"} default button "OK"
return
end if
-- Get all databases
set allDatabases to databases
-- Create a list of databases to search (excluding the current one)
set searchDatabases to {}
repeat with db in allDatabases
if name of db ≠ name of currentDatabase then
set end of searchDatabases to db
end if
end repeat
if (count of searchDatabases) = 0 then
display dialog "No other databases found to search in." buttons {"OK"} default button "OK"
return
end if
-- Let user choose which database to search in
set databaseNames to {}
repeat with db in searchDatabases
set end of databaseNames to name of db
end repeat
set chosenDatabaseName to choose from list databaseNames with prompt "Choose database to search in:" default items {item 1 of databaseNames}
if chosenDatabaseName is false then
return -- User cancelled
end if
-- Find the chosen database
set targetDatabase to missing value
repeat with db in searchDatabases
if name of db = item 1 of chosenDatabaseName then
set targetDatabase to db
exit repeat
end if
end repeat
if targetDatabase is missing value then
display dialog "Selected database not found." buttons {"OK"} default button "OK"
return
end if
-- Search for the item in the target database using a more specific approach
try
set searchResults to {}
-- Use the lookup records command instead of search to avoid syntax issues
set allRecords to children of root of targetDatabase
set exactMatches to {}
-- Recursively search through all records
my searchRecordsRecursively(allRecords, pendingName, exactMatches)
if (count of exactMatches) = 0 then
display dialog "No matching item found in database \"" & name of targetDatabase & "\"." buttons {"OK"} default button "OK"
return
end if
on error errMsg
display dialog "Error during search: " & errMsg buttons {"OK"} default button "OK"
return
end try
-- If multiple matches, let user choose
set foundRecord to missing value
if (count of exactMatches) = 1 then
set foundRecord to item 1 of exactMatches
else
-- Multiple matches found, let user choose
set matchNames to {}
repeat with match in exactMatches
try
set matchPath to location of match
set end of matchNames to (name of match & " (" & matchPath & ")")
on error
set end of matchNames to name of match
end try
end repeat
set chosenMatch to choose from list matchNames with prompt "Multiple matches found. Choose one:" default items {item 1 of matchNames}
if chosenMatch is false then
return -- User cancelled
end if
-- Find the corresponding record
set chosenIndex to 1
repeat with i from 1 to count of matchNames
if item i of matchNames = item 1 of chosenMatch then
set chosenIndex to i
exit repeat
end if
end repeat
set foundRecord to item chosenIndex of exactMatches
end if
-- Confirm the replacement
set confirmMessage to "Replace pending item \"" & pendingName & "\" with item from \"" & name of targetDatabase & "\"?"
set userChoice to display dialog confirmMessage buttons {"Cancel", "Replace"} default button "Replace" with icon caution
if button returned of userChoice = "Cancel" then
return
end if
-- Perform the replacement
try
-- First duplicate the found record to the same location as the pending item
set duplicatedRecord to duplicate foundRecord to pendingParent
-- Then delete the pending record
delete record pendingRecord
-- Show success message
display dialog "Successfully replaced pending item with \"" & name of duplicatedRecord & "\"." buttons {"OK"} default button "OK"
on error errMsg
display dialog "Error during replacement: " & errMsg buttons {"OK"} default button "OK"
end try
on error errMsg
display dialog "Script error: " & errMsg buttons {"OK"} default button "OK"
end try
end tell
-- Helper function to recursively search through records
on searchRecordsRecursively(recordList, searchName, exactMatches)
tell application "DEVONthink"
repeat with rec in recordList
try
if name of rec = searchName then
set end of exactMatches to rec
end if
-- Check if this record has children (is a group)
try
set childRecords to children of rec
if (count of childRecords) > 0 then
my searchRecordsRecursively(childRecords, searchName, exactMatches)
end if
end try
on error
-- Skip records that can't be accessed
end try
end repeat
end tell
end searchRecordsRecursively
Some questions re the code, in no particular order:
Why would DT not be “open” after a tell application "DEVONthink", and how would one even inquire a think window 1 of a closed app?
Why can’t the script handle more than one pending record?
Why does the code first build a list of searchable databases and check if it contains anything later? It could simply check if count of allDatabases = 1 instead.
What is the purpose of the repeat with db in searchDatabases loop? Why not simply use chosenDatabaseName?
Why would one search for the name of a record, recursively at that, instead of simply using its UUID?
Why would an item in DT have no name property?
Why is the code using selected and assigning that to selectedRecords instead of using the selected records property in the first place? How can the targetDatabase ever be missing value?
Perhaps there’s someone willing to wade through that mud and find the problem (you didn’t even say what the problem was, though – “it does not work” doesn’t tell us anything). But I suppose the chances for that would vastly improve if the code were trimmed down to the relevant steps and these steps were clearly described. Technically, there is no problem duplicating a record to another database.
Thanks chrillek. I don‘t really have any experience with Applescript. The situation is that I have a number (approx. 2000) items that are “pending“, and I can neither access nor sync them across my devices. But I have a backup database that contains most if not all of these items. I have been looking for a way to - as much as possible - automate the transfer of that backup items into my main database (at the location of the “pending“ item, ideally).
So, I’m stuck with that and tried vibe coding with ChatGPT and Claude. While I can‘t answer ypur questions the Skript seems to work (identifying the corresponding item from the backup when a pending item is selected), but it stops at the duplicating/replacing step (ideally after the correct item is copied the pending one is deleted). One suggestion I‘m following is that in DT, copying across databases may not be straightforward or not possible. That‘s why I’m asking for help for that - but of course any other suggestion / solution to the main problem would be great too!
My programming experience is old (BASIC and TurboPascal), and I fear if I don’t find a solution is manual or nothing….
In any case, no problem if it’s not straightforward or much more complex. Thanks for taking the time!
Imo, the code is just too convoluted to be a good starting point.
Therefore, let’s take a step back and try describe the problem more clearly.
Why do you even have pending items in a database? Did you index these files, since that seems to be the prevalent cause of the issue? So, what does the inspector tell you about these pending records? What is the value of path? Can you get an item link to them with the context menu’s Copy link?
Thanks! Not sure where these items come from. I only have databases where I import items. Most are fine and sync, but a number is “pending“. I cannot download them using the menu, and when I try to open it says “File not yet available“. It’s for a few months now (at least); and all current devices syncing to that sync store are synced. Maybe the most likely explanation is that there was something with an old iPad or something…
I’m really not sure what to do, and I also can’t answer any questions about that Skript. But what I know is that it finds the pending record in the backup database, but then the duplication/replacement fails with that error message.
For the “pending” items, the “path” in the inspector is empty. I get an x-DEVONthink..item:// link/identifier that is exactly the same as when I get the link from the item in the backup database.
I appreciate your help and questions. And Bbtw, your comment reminded me on that old Irish (or Scottish?) joke about a city boy from Dublin, who comes out to the country for his cousin’s wedding. He can’t remember the way, so he stops to ask a farmer for directions. The farmer looks at him, scratches his head, thinks for a moment, frowns and says: ‘You know, if I were you, I wouldn’t start from here.’
So, if you get the item link from the pending item(s), you could proceed like that
get all pending records from database A
for each of them
get its item link
get the record with the same UUID from database B
duplicate this record into database A
delete the old record
In script code (and no, this is not AppleScript but JavaScript)
(() => {
/* faultyDB contains the pending records, okDB the corresponding ok ones
Both databases have to be open in DT! */
const faultyDB = "Name of faulty DB";
const okDB = "Name of ok DB";
const app = Application("DEVONthink");
/* Get all mising records from faultyDB */
const missingRecords = app.databases[faultyDB].contents.whose({pending: true});
missingRecords().forEach(record => {
/* Get the item link of the current record and its primary parent group */
const link = record.referenceURL();
const parent = record.locationGroup;
/* If the link and the parent exist, continue */
if (link && link !== "" && parent) {
/* Get the old record */
const oldRecord = app.getRecordWithUuid(link, {in: app.databases[okDB]});
/* If the old record exists, duplicate it into the faulty database */
if (oldRecord) {
const newRecord = app.duplicate({record: oldRecord, to: parent});
/* Uncomment the following three lines if you're sure that the rest of the script works – they remove the pending records so the script can't be run ever again afterwords */
/* DANGEROUS STUFF !
if (newRecord) {
app.delete({record: record})
}
*/
}
}
})
})()
I can’t test this script since I do not have any pending records. Note that at the end of the forEach loop, the code to delete the pending record is commented out. This is a safeguard, since once all these records are removed, you can never run the script again.
And while I would have opted for an AppleScript solution, look at the brevity of @chrillek’s code. Strip out all the comments and you have a beautifully distilled script. Great code is the product of experience – including failures and frustrations – but in the end you are able to produce powerful automations for yourself. A worthy endeavor, I’d say.