Hello DEVONthink Community,
I thought I’d share this Applescript with you that I have been working on for some time to get right…because Applescript is so very helpful when it throws errors ;).
What started as an attempt to give AI better context when asking for help (re)structuring my databases, turned into an equally useful tool as a nicely formatted Table of Contents in the root of each database (of course not as a substitute for the actual file tree), but it is complete with item links to corresponding records, or use as snapshot versioning tool to help return state if a move classify automation ever goes awry or for nothing more than pure joy in watching your databases grow over time with automated snapshots.
Note: If your database is large, it twill take time to run, and possibly prohibitive for larger databases on slower machines (I’m not sure). For me, running on my M4, on a database with 3651 items in 342 groups took 2m 45 sec. Just so you don’t throw out the script before it finishes generating nice output (maybe I’m biased). In any case, I welcome anyone’s feedback on improvements, especially on efficiency.
Cheers,
Jamie
(*
=================================================================
DEVONthink Database Tree Generator
-----------------------------------
Generates an ASCII tree structure of any open DEVONthink database
and saves it as a timestamped plain text file in the database root.
Compatible with: DEVONthink 3 / DEVONthink 4
Author: Jamie Forrest (https://github.com/drjforrest)
Date: 2026-02-27
Usage:
- Run from Script Editor, Keyboard Maestro, or DEVONthink's Script menu
- A dialog will prompt you to choose which open database to map
- The tree is saved as a plain text record in the database root
- Filename includes ISO 8601 timestamp for versioning
Output format:
📁 Database Name
├── 📁 Group A
│ ├── 📄 Document 1.md
│ ├── 📄 Document 2.pdf
│ └── 📁 Subgroup
│ └── 📄 Nested Doc.txt
├── 📄 Root Document.md
└── 📁 Group B
└── 📄 Another Doc.pdf
Notes:
- Excludes Trash and Tags system groups by default
- Uses Unicode box-drawing characters for clean tree rendering
- Record types are indicated with emoji prefixes:
📁 = group/folder
🔍 = smart group
📄 = document (all other types)
- Large databases may take time to process
=============================================================================
*)
use AppleScript version "2.4"
use scripting additions
-- ============================================================
-- CONFIGURATION
-- ============================================================
property excludeTrash : true
property excludeTags : true
property excludeSmartGroups : false
property showRecordType : true
property showItemCount : true
property maxDepth : 50 -- safety limit for recursion depth
-- ============================================================
-- MAIN SCRIPT
-- ============================================================
on run
tell application id "DNtp"
-- Gather open database names
set dbList to name of every database
if (count of dbList) is 0 then
display dialog "No databases are currently open in DEVONthink." buttons {"OK"} default button "OK" with icon stop
return
end if
-- Prompt user to select a database
set chosenDB to choose from list dbList with prompt ¬
"Select a database to generate a tree structure:" with title ¬
"DEVONthink Tree Generator" default items {item 1 of dbList}
if chosenDB is false then return
set dbName to item 1 of chosenDB
-- Get the database reference
set theDB to database dbName
set rootRecord to root of theDB
-- Generate timestamp (ISO 8601)
set theDate to current date
set dateStamp to my formatDate(theDate)
-- Build header
set treeOutput to "============================================================" & linefeed
set treeOutput to treeOutput & " DEVONthink Database Tree Structure" & linefeed
set treeOutput to treeOutput & " Database: " & dbName & linefeed
set treeOutput to treeOutput & " Generated: " & dateStamp & linefeed
set treeOutput to treeOutput & "============================================================" & linefeed & linefeed
-- Start tree with database root
set treeOutput to treeOutput & "📁 " & dbName & linefeed
-- Get children of root and process recursively
set rootChildren to children of rootRecord
set itemTotal to 0
set groupTotal to 0
set {treeBody, itemTotal, groupTotal} to my buildTree(rootChildren, "", true, 0, 0, 0)
set treeOutput to treeOutput & treeBody
-- Append summary footer
set treeOutput to treeOutput & linefeed
set treeOutput to treeOutput & "============================================================" & linefeed
set treeOutput to treeOutput & " Summary" & linefeed
set treeOutput to treeOutput & " Total items: " & itemTotal & linefeed
set treeOutput to treeOutput & " Total groups: " & groupTotal & linefeed
set treeOutput to treeOutput & "============================================================" & linefeed
-- Create the record name with timestamp
set recordName to "Tree_" & dbName & "_" & my filenameSafeDate(theDate)
-- Create a plain text record in the database root
set newRecord to create record with {name:recordName, type:txt, plain text:treeOutput} in rootRecord
-- Notify user
set dialogMsg to "Tree structure generated successfully!" & linefeed & linefeed & ¬
"📄 " & recordName & ".txt" & linefeed & ¬
"📊 " & itemTotal & " items in " & groupTotal & " groups" & linefeed & linefeed & ¬
"Saved to root of \"" & dbName & "\" database."
display dialog dialogMsg buttons {"Open Record", "OK"} default button "OK" with title "Tree Generator Complete"
if button returned of result is "Open Record" then
open tab for record newRecord in think window 1
end if
end tell
end run
-- ============================================================
-- RECURSIVE TREE BUILDER
-- ============================================================
(*
Builds the ASCII tree string recursively.
Parameters:
records - list of records to process
prefix - the current indentation prefix string
isRoot - whether this is the root level call
depth - current recursion depth
itemCount - running total of items processed
groupCount - running total of groups processed
Returns: {treeString, itemCount, groupCount}
*)
on buildTree(recList, prefix, isRoot, depth, itemCount, groupCount)
if depth > maxDepth then return {"", itemCount, groupCount}
set treeOutput to ""
-- Build filtered list
set filteredRecords to {}
tell application id "DNtp"
repeat with i from 1 to count of recList
set thisRecord to item i of recList
set recName to name of thisRecord
set recType to type of thisRecord as string
set shouldExclude to false
if excludeTrash and recName is "Trash" then set shouldExclude to true
if excludeTags and recName is "Tags" then set shouldExclude to true
if excludeSmartGroups and recType is "smart group" then set shouldExclude to true
if not shouldExclude then
set end of filteredRecords to thisRecord
end if
end repeat
end tell
set filteredCount to count of filteredRecords
repeat with i from 1 to filteredCount
-- Fetch all DEVONthink properties upfront
tell application id "DNtp"
set thisRecord to item i of filteredRecords
set recName to name of thisRecord
set recType to type of thisRecord as string
set recFilename to filename of thisRecord
set groupChildren to {}
set smartCount to 0
if recType is "group" then
set groupChildren to children of thisRecord
else if recType is "smart group" then
set smartCount to count of children of thisRecord
end if
end tell
-- All logic outside the tell block
set isLastItem to (i = filteredCount)
if isLastItem then
set connector to "└── "
set childPrefix to prefix & " "
else
set connector to "├── "
set childPrefix to prefix & "│ "
end if
if recType is "group" then
set typeIcon to "📁 "
else if recType is "smart group" then
set typeIcon to "🔍 "
else
set typeIcon to "📄 "
end if
set itemCount to itemCount + 1
set thisLine to prefix & connector & typeIcon & recName
if showRecordType then
if recType is not "group" and recType is not "smart group" then
if recFilename is missing value or recFilename is "" then
set thisLine to thisLine & " [" & recType & "]"
end if
end if
end if
set treeOutput to treeOutput & thisLine & linefeed
if recType is "group" then
set groupCount to groupCount + 1
if (count of groupChildren) > 0 then
set {childOutput, itemCount, groupCount} to my buildTree(groupChildren, childPrefix, false, depth + 1, itemCount, groupCount)
set treeOutput to treeOutput & childOutput
end if
else if recType is "smart group" then
set groupCount to groupCount + 1
if showItemCount and smartCount > 0 then
set treeOutput to treeOutput & childPrefix & "└── (" & smartCount & " matching items)" & linefeed
end if
end if
end repeat
return {treeOutput, itemCount, groupCount}
end buildTree
-- ============================================================
-- DATE FORMATTING HELPERS
-- ============================================================
-- Returns ISO 8601 formatted date string: "2026-02-27 18:22:15 PST"
on formatDate(theDate)
set y to year of theDate as string
set m to my zeroPad(month of theDate as integer)
set d to my zeroPad(day of theDate)
set h to my zeroPad(hours of theDate)
set min to my zeroPad(minutes of theDate)
set s to my zeroPad(seconds of theDate)
return y & "-" & m & "-" & d & " " & h & ":" & min & ":" & s
end formatDate
-- Returns filename-safe date string: "2026-02-27_182215"
on filenameSafeDate(theDate)
set y to year of theDate as string
set m to my zeroPad(month of theDate as integer)
set d to my zeroPad(day of theDate)
set h to my zeroPad(hours of theDate)
set min to my zeroPad(minutes of theDate)
set s to my zeroPad(seconds of theDate)
return y & "-" & m & "-" & d & "_" & h & min & s
end filenameSafeDate
-- Zero-pads a number to 2 digits
on zeroPad(n)
set s to n as string
if (count of s) < 2 then set s to "0" & s
return s
end zeroPad