As far as I’m aware, Safari doesn’t have any direct scripting interface to access the reading list. Another user asked for a way to import the reading list in DEVONthink as bookmarks, and I’ve been wanting something similar. So I explored a bit and wrote this script.
Safari’s Reading List is stored along with your bookmarks in ~/Library/Safari/Bookmarks.plist. The scripting dictionary for System Events includes a “Property List Suite” for reading .plist files. I never used it before, but it worked surprisingly well.
Each item in the reading list is represented as a dictionary, with a bunch of information like the date added, title, URL and such. Each item also has a UUID, and any downloaded assets are stored in ~/Library/Safari/ReadingListArchives/<UUID>. This will mostly be a thumbnail image, but if you use Safari’s option to download articles there will also be a web archive. The script has two flags at the start for specifying whether you want to use them.
The script creates a Safari Reading List group in the root of the current database and uses that as the import destination. The creation date of imported items is set to the date added in Safari and any preview text is stored in the comment field.
The script is written for DEVONthink 3, but should only need small adjustments for DEVONthink 4. (I will revisit this post when I’ve upgraded, hopefully soon.)
That should be enough of an introduction.
Here is the script, written in JavaScript for Automation (JXA):
Original script (DT3)
/* Import Safari’s Reading List to DEVONthink
as either bookmarks or webarchives,
keeping useful metadata from Safari.
Written by troejgaard on Nov 27, 2025 */
(() => {
const DT = Application("DEVONthink 3"); // For DEVONthink 4 use `Application("DEVONthink")`
DT.includeStandardAdditions = true;
const Sys = Application("System Events");
Sys.includeStandardAdditions = true;
const importThumbnails = true;
const importWebarchives = false;
const destDB = DT.currentDatabase();
// Access Safari’s Bookmarks.plist
const SafariLib = `${Sys.userDomain.libraryFolder.posixPath()}/Safari`;
const bookmarksFile = Sys.propertyListFiles[`${SafariLib}/Bookmarks.plist`];
// Find the nested dictionary representing the Reading List...
const readingList = bookmarksFile.propertyListItems["Children"]
.propertyListItems().filter(dict =>
dict.propertyListItems["Title"].value() === "com.apple.ReadingList"
)[0]
// ...storing list items as an array of dictionaries:
.propertyListItems["Children"];
const itemCount = readingList.propertyListItems.length;
if (itemCount === 0) {
DT.displayAlert("Safari Reading List is empty");
return;
}
const dest = DT.createLocation("/Safari Reading List", {in: destDB});
DT.showProgressIndicator("Safari Reading List",
{cancelButton: true, steps: itemCount});
let i = 0; let userCancelled;
while (i < itemCount) {
DT.stepProgressIndicator(`Importing ${i+1} of ${itemCount}`);
// Read list item’s dictionary:
const dict = readingList.propertyListItems[i].value();
// Map properties we’re interested in:
const title = dict.URIDictionary.title;
const link = dict.URLString;
const date = dict.ReadingList.DateAdded;
const desc = dict.ReadingList.PreviewText ?? '';
const uid = dict.WebBookmarkUUID;
// Check downloaded assets
const assets = `${SafariLib}/ReadingListArchives/${uid}`;
let img = `${assets}/thumbnail.png`;
let webarchive = `${assets}/Page.webarchive`;
if (!importThumbnails || !Sys.files[img].exists()) img = null;
if (!importWebarchives || !Sys.files[webarchive].exists()) webarchive = null;
if (webarchive) {
const r = DT.import(webarchive, {name:title, to:dest});
r.creationDate = date;
r.comment = desc;
if (img) r.thumbnail = img;
} else {
DT.createRecordWith({
"type": "bookmark", // For DT4 use "record type"
"name": title,
"URL": link,
"creation date": date,
"comment": desc,
"thumbnail": img
},{ in: dest });
}
i++;
if (DT.cancelledProgress()) {userCancelled = true; break;}
}
const msg = userCancelled ?
`Imported ${i}/${itemCount} items (Cancelled by user)`
: `Imported ${i} items`;
DT.hideProgressIndicator();
DT.logMessage(msg, {record:dest});
})()
Edit, Dec 7: Here is an up-to-date version tested in DT4. I only had to make 3 small adjustments to comply with the new dictionary. Besides that, I added a count of thumbnails/webarchives to the log message and followed chrillek’s suggestion of using const for img and webarchive.
/* Import Safari’s Reading List to DEVONthink
as either bookmarks or webarchives,
keeping useful metadata from Safari.
Written by troejgaard on Nov 27, 2025
Updated on Dec 5, 2025 */
(() => {
const DT = Application("DEVONthink");
DT.includeStandardAdditions = true;
const Sys = Application("System Events");
Sys.includeStandardAdditions = true;
const destDB = DT.currentDatabase(); // Alternatively, 'DT.inbox()' or 'DT.preferredImportDestination()'
const importThumbnails = true;
const importWebarchives = false;
let imgCount = 0, webCount = 0;
// Access Safari’s Bookmarks.plist
const SafariLib = `${Sys.userDomain.libraryFolder.posixPath()}/Safari`;
const bookmarksFile = Sys.propertyListFiles[`${SafariLib}/Bookmarks.plist`];
// Find the nested dictionary representing the Reading List...
const readingList = bookmarksFile.propertyListItems["Children"]
.propertyListItems().filter(dict =>
dict.propertyListItems["Title"].value() === "com.apple.ReadingList"
)[0]
// ...storing list items as an array of dictionaries:
.propertyListItems["Children"];
const itemCount = readingList.propertyListItems.length;
if (itemCount === 0) {
DT.displayAlert("Safari Reading List is empty");
return;
}
const dest = DT.createLocation("/Safari Reading List", {in: destDB});
DT.showProgressIndicator("Safari Reading List",
{cancelButton:true, steps:itemCount});
let i = 0; let userCancelled;
while (i < itemCount) {
DT.stepProgressIndicator(`Importing ${i+1} of ${itemCount}`);
// Read list item’s dictionary:
const dict = readingList.propertyListItems[i].value();
// Map properties we’re interested in:
const title = dict.URIDictionary.title;
const link = dict.URLString;
const date = dict.ReadingList.DateAdded;
const desc = dict.ReadingList.PreviewText ?? '';
const uid = dict.WebBookmarkUUID;
// Check downloaded assets:
const assets = `${SafariLib}/ReadingListArchives/${uid}`;
const img = importThumbnails && Sys.files[`${assets}/thumbnail.png`].exists() ?
`${assets}/thumbnail.png` : null;
const webarchive = importWebarchives && Sys.files[`${assets}/Page.webarchive`].exists() ?
`${assets}/Page.webarchive` : null;
if (img) imgCount++;
if (webarchive) {
const r = DT.importPath(webarchive, {name:title, to:dest});
r.creationDate = date;
r.comment = desc;
if (img) r.thumbnail = img;
webCount++;
} else {
DT.createRecordWith({
"record type": "bookmark",
"name": title,
"URL": link,
"creation date": date,
"comment": desc,
"thumbnail": img
},{ in: dest });
}
i++;
if (DT.cancelledProgress()) {userCancelled = true; break;}
}
const msg = (userCancelled ?
`Imported ${i}/${itemCount} items (Cancelled by user)`
: `Imported ${i} items`)
+ (imgCount ? `\n- ${imgCount} with thumbnail` : '')
+ (webCount ? `\n- ${webCount} as Web Archive` : '');
DT.hideProgressIndicator();
DT.logMessage(msg, {record:dest});
})()
Nerdy details
If anyone wants to play around with this, here is an empty dictionary of a reading list item to show the keys and structure.
Example
<dict>
<key>ReadingList</key>
<dict>
<key>DateAdded</key>
<date></date>
<key>PreviewText</key>
<string></string>
</dict>
<key>ReadingListNonSync</key>
<dict>
<key>addedLocally</key>
<true/>
<key>DateLastFetched</key>
<date></date>
<key>FetchResult</key>
<integer></integer>
<key>NumberOfFailedLoadsWithUnknownOrNonRecoverableError</key>
<integer></integer>
<key>neverFetchMetadata</key>
<false/>
<key>topicQIDs</key>
<array>
<string></string>
<string></string>
</array>
<key>didAttemptToFetchIconFromImageUrlKey</key>
<true/>
<key>siteName</key>
<string></string>
</dict>
<key>Sync</key>
<dict>
<key>Data</key>
<data>
...
</data>
<key>ServerID</key>
<string></string>
</dict>
<key>URIDictionary</key>
<dict>
<key>title</key>
<string></string>
</dict>
<key>URLString</key>
<string></string>
<key>WebBookmarkType</key>
<string>WebBookmarkTypeLeaf</string>
<key>WebBookmarkUUID</key>
<string></string>
<key>imageURL</key>
<string></string>
</dict>
Some keys are not present in every dictionary (like DateLastFetched, topicQIDs, siteName, imageURL), but a majority of them are. The example should include all possible keys found in my own Bookmarks.plist, but I’m not sure if there are more.