Duplicating a record between databases?

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.

2 Likes

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?

The duplicate command is really duplicate record. You’d follow that with your variable.

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.’

1 Like

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.

This works for me - tested on separate backup databases.

Thanks a lot chrillek! Greatly appreciate your time and support with this. It helps me a lot!

Good. Another argument for learning to code (or asking people to help) instead of relying on a mechanical ape hacking away on a keyboard.

And, of course: Understanding the task and the situation at hand well before starting to code.

2 Likes

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.

The AppleScript code would look very similar, I think. About the same length, too