In the previous thread, Jim asked if I could share any scripts that I got AI to make. It looks as if I cannot post the actual .gplug file, But maybe I can paste the text below and anyone who wants can copy and paste it into the GEDitCOM Editor and go from there.
This script - isolated2a.gcscpt - was created by Claude Sonnet 4.6 following very basic instructions from me. It scans the database, and pulls out isolated individuals and clusters. It also identifies those individuals who are only linked to others via an ASSO tag. Say witnesses at a wedding or similar, or a lodger in a census record. The help documentation explains that, but I won't post it unless others want it. The output is a quite nice-looking report.
John already provides a similar option although I think it places the records in a separate album, rather than a report. I may be missing something there.
On my M1 Macbook with some 6,800 people in the database, it takes about 6 or 7 seconds to produce the report.
If anyone does use it, feedback appreciated. It's Claude though, not me. I cannot code at all.
! =====================================================================
! Script: Isolated Individuals and Clusters Report
! Language: GENerel (GEDitCOM II Internal Scripting)
! Written by Claude Sonnet 4.6 and Mick Reed, MAY 2026
! =====================================================================
#scriptName = "Isolated Individuals and Clusters"
PreChecks #scriptName,3,2,1,1
gcapp.get gdoc,frontDocument
! Initialize output report
CreateScriptOutput rpt,#scriptName,"html"
! Setup fast inline CSS styling
#styleHeader = "background-color:#f0f0f0; font-weight:bold; border:1px solid #ccc; padding:6px;"
#styleCell = "border:1px solid #ddd; padding:5px; vertical-align:top;"
#styleRowAlt = "background-color:#f9f9f9;"
! ---------------------------------------------------------------------
! Hoist document-level collections — fetched once, reused throughout
! ---------------------------------------------------------------------
gdoc.get indis,"individuals"
gdoc.get fams,"families"
#totalIndis = @indis.count
if #totalIndis = 0
#msg = "The front document contains no individuals."
UserOption "#any","No Records Found",#msg,"OK"
exit
endif
! ---------------------------------------------------------------------
! Step 1: Initialise tracking dictionaries
! ---------------------------------------------------------------------
CreateDictionary visitedDict
CreateDictionary clusterSizeDict
CreateDictionary clusterMembersDict
CreateDictionary clusterAnchorDict
! Small result sets built during traversal — no re-scan needed in report
! singletonRows: key = sequential index string, value = pre-built HTML <tr>
! assoRows: key = sequential index string, value = pre-built HTML <tr> block
CreateDictionary singletonRows
CreateDictionary assoRows
#singletonCount = 0
#assoSingletonCount = 0
#clusterCount = 0
! ---------------------------------------------------------------------
! Step 2: Core Loop — BFS network mapping + result row accumulation
! ---------------------------------------------------------------------
Repeat "#i",0,#totalIndis-1
indis.#i.get indi
#rootID = @
indi.id visitedDict.valueForKey "#isVisited",#rootID
ifNDef #isVisited
indi.get cParents,"parents"
indi.get cSpouses,"spouses"
indi.get cChildren,"children"
#hasParentsCount = @cParents.count
#hasSpousesCount = @cSpouses.count
#hasChildrenCount = @cChildren.count
if #hasParentsCount = 0 && #hasSpousesCount = 0 && #hasChildrenCount = 0
! ---------------------------------------------------------
! No family links — check for ASSO tags
! ---------------------------------------------------------
visitedDict.setStringForKey "Y",#rootID
indi.findStructures assoList,"ASSO"
#assoCount = @assoList.count
if #assoCount > 0
! ---- ASSO-linked isolate ----
#iName = @indi.alternateName
#iLifespan = @indi.lifeSpan
#iID = #rootID
! Build all rows for this individual now
#assoBlockHTML = ""
#spanRows = #assoCount
Repeat "#aIdx",0,#assoCount-1
assoList.#aIdx.get assoStruct
#targetRef = @assoStruct.contents
assoStruct.findStructures relaList,"RELA"
#relaText = ""
if @relaList.count > 0
relaList.0.get relaStruct
#relaText = @relaStruct.contents
endif
ifStr #relaText = ""
#relaText = "—"
endif
! Resolve target record
replace "#bareTargetID","@","",#targetRef
#fullTargetID = "@" & #bareTargetID & "@"
fams.#fullTargetID.get targetFam
#targetLink = ""
ifDef targetFam
#targetDisplayName = @targetFam.alternateName
#targetLink = "<a href='//" & #targetRef & "'>" & #targetDisplayName & "</a>"
else
indis.#fullTargetID.get targetIndi2
ifDef targetIndi2
#targetDisplayName = @targetIndi2.alternateName
#targetLink = "<a href='//" & #targetRef & "'>" & #targetDisplayName & "</a>"
else
#targetLink = #targetRef
endif
endif
if #aIdx = 0
! First row — includes name and lifespan with rowspan
#rowHTML = "<tr>"
#rowHTML = #rowHTML & "<td style='" & #styleCell & "' rowspan='" & #spanRows & "'><a href='//" & #iID & "'>" & #iName & "</a></td>"
#rowHTML = #rowHTML & "<td style='" & #styleCell & "' rowspan='" & #spanRows & "'>" & #iLifespan & "</td>"
#rowHTML = #rowHTML & "<td style='" & #styleCell & "'>" & #targetLink & "</td>"
#rowHTML = #rowHTML & "<td style='" & #styleCell & "'>" & #relaText & "</td>"
#rowHTML = #rowHTML & "</tr>"
else
! Continuation rows — target and RELA only
#rowHTML = "<tr>"
#rowHTML = #rowHTML & "<td style='" & #styleCell & "'>" & #targetLink & "</td>"
#rowHTML = #rowHTML & "<td style='" & #styleCell & "'>" & #relaText & "</td>"
#rowHTML = #rowHTML & "</tr>"
endif
#assoBlockHTML = #assoBlockHTML & #rowHTML
EndRepeat
assoRows.setStringForKey #assoBlockHTML,#assoSingletonCount
#assoSingletonCount += 1
else
! ---- True singleton ----
#sName = @indi.alternateName
#sLifespan = @indi.lifeSpan
if #singletonCount = 1
#sRowStyle = #styleCell & #styleRowAlt
else
#sRowStyle = #styleCell
endif
#sRow = "<tr>"
#sRow = #sRow & "<td style='" & #sRowStyle & "'><a href='//" & #rootID & "'>" & #sName & "</a></td>"
#sRow = #sRow & "<td style='" & #sRowStyle & "'>" & #sLifespan & "</td>"
#sRow = #sRow & "</tr>"
singletonRows.setStringForKey #sRow,#singletonCount
#singletonCount += 1
endif
else
! ---------------------------------------------------------
! Connected individual — BFS to map cluster
! ---------------------------------------------------------
#clusterCount += 1
#thisClusterID = "C_" & #clusterCount
CreateDictionary queueDict
#qHead = 0
#qTail = 0
queueDict.setStringForKey #rootID,#qTail
#qTail += 1
visitedDict.setStringForKey #thisClusterID,#rootID
#currentClusterSize = 0
#clusterMemberListStr = ""
#anchorName = ""
Repeat
if #qHead = #qTail
break
endif
queueDict.valueForKey "#currentID",#qHead
#qHead += 1
#currentClusterSize += 1
ifStr #clusterMemberListStr = ""
#clusterMemberListStr = #currentID
else
#clusterMemberListStr = #clusterMemberListStr & "," & #currentID
endif
replace "#bareLookupID","@","",#currentID
#fullLookupID = "@" & #bareLookupID & "@"
indis.#fullLookupID.get currIndi
ifDef currIndi
! Track anchor: first individual with no parents encountered
ifStr #anchorName = ""
currIndi.get chkParents,"parents"
if @chkParents.count = 0
#anchorName = @currIndi.alternateName
endif
endif
currIndi.get relParents,"parents"
currIndi.get relSpouses,"spouses"
currIndi.get relChildren,"children"
if @relParents.count > 0
Repeat "#pIdx",0,@relParents.count-1
relParents.#pIdx.get relative
#relID = @
relative.id visitedDict.valueForKey "#relVisited",#relID
ifNDef #relVisited
visitedDict.setStringForKey #thisClusterID,#relID
queueDict.setStringForKey #relID,#qTail
#qTail += 1
endif
EndRepeat
endif
if @relSpouses.count > 0
Repeat "#sIdx",0,@relSpouses.count-1
relSpouses.#sIdx.get relative
#relID = @
relative.id visitedDict.valueForKey "#relVisited",#relID
ifNDef #relVisited
visitedDict.setStringForKey #thisClusterID,#relID
queueDict.setStringForKey #relID,#qTail
#qTail += 1
endif
EndRepeat
endif
if @relChildren.count > 0
Repeat "#cIdx",0,@relChildren.count-1
relChildren.#cIdx.get relative
#relID = @
relative.id visitedDict.valueForKey "#relVisited",#relID
ifNDef #relVisited
visitedDict.setStringForKey #thisClusterID,#relID
queueDict.setStringForKey #relID,#qTail
#qTail += 1
endif
EndRepeat
endif
endif
EndRepeat
clusterSizeDict.setNumberForKey #currentClusterSize,#thisClusterID
clusterMembersDict.setStringForKey #clusterMemberListStr,#thisClusterID
! Fallback anchor if no parentless individual was found
ifStr #anchorName = ""
#anchorName = @indi.alternateName
endif
clusterAnchorDict.setStringForKey #anchorName,#thisClusterID
endif
endif
EndRepeat
! ---------------------------------------------------------------------
! Step 3: Generate the HTML Report
! ---------------------------------------------------------------------
#headBlock = "<head><title>Isolated Records Report</title></head>"
rpt.head #headBlock
#titleStr = "<h2>Unlinked and Isolated Tree Analysis</h2>"
rpt.out #titleStr
#summaryStr = "<p><b>Total Database Individuals:</b> " & #totalIndis & "<br>"
#summaryStr = #summaryStr & "<b>Isolated Individuals (Singletons):</b> " & #singletonCount & "<br>"
#summaryStr = #summaryStr & "<b>Isolated Individuals with ASSO links only:</b> " & #assoSingletonCount & "<br>"
#summaryStr = #summaryStr & "<b>Isolated Sub-Clusters Found:</b> " & #clusterCount & "</p><hr>"
rpt.out #summaryStr
! ---------------------------------------------------------------------
! Section 1: True Singletons — emit pre-built rows directly
! ---------------------------------------------------------------------
#s1Head = "<h3>Section 1: Isolated Individuals (Singletons)</h3>"
rpt.out #s1Head
if #singletonCount = 0
rpt.out "<p>No completely isolated individuals found.</p>"
else
#tableStart = "<table style='width:100%; border-collapse:collapse; margin-bottom:20px;'>"
#tableStart = #tableStart & "<thead><tr>"
#tableStart = #tableStart & "<th style='" & #styleHeader & "'>Full Name (Click to view)</th>"
#tableStart = #tableStart & "<th style='" & #styleHeader & "'>Lifespan</th>"
#tableStart = #tableStart & "</tr></thead><tbody>"
rpt.out #tableStart
Repeat "#si",0,#singletonCount-1
singletonRows.valueForKey "#sRowOut",#si
rpt.out #sRowOut
EndRepeat
rpt.out "</tbody></table>"
endif
! ---------------------------------------------------------------------
! Section 2: Isolated Clusters
! ---------------------------------------------------------------------
#s2Head = "<h3>Section 2: Isolated Family Clusters</h3>"
rpt.out #s2Head
#maxClusterSize = 0
#mainClusterID = ""
if #clusterCount > 0
Repeat "#cIdx",1,#clusterCount
#checkID = "C_" & #cIdx
clusterSizeDict.valueForKey "#cSize",#checkID
if #cSize > #maxClusterSize
#maxClusterSize = #cSize
#mainClusterID = #checkID
endif
EndRepeat
endif
#isolatedClusterCount = 0
if #clusterCount > 0
Repeat "#cIdx",1,#clusterCount
#checkID = "C_" & #cIdx
ifStr #checkID != #mainClusterID
#isolatedClusterCount += 1
endif
EndRepeat
endif
if #isolatedClusterCount = 0
rpt.out "<p>No isolated multi-person clusters found. All grouped individuals belong to the primary tree framework.</p>"
else
clusterAnchorDict.valueForKey "#mainAnchorName",#mainClusterID
#clusterText = "<p>The primary active network is the <b>"
#clusterText = #clusterText & #mainAnchorName
#clusterText = #clusterText & " Cluster</b> containing "
#clusterText = #clusterText & #maxClusterSize
#clusterText = #clusterText & " individuals. The table below lists the secondary isolated branches.</p>"
rpt.out #clusterText
#tableBStart = "<table style='width:100%; border-collapse:collapse;'>"
#tableBStart = #tableBStart & "<thead><tr>"
#tableBStart = #tableBStart & "<th style='" & #styleHeader & "'>Cluster Earliest Generation</th>"
#tableBStart = #tableBStart & "<th style='" & #styleHeader & "'>Size</th>"
#tableBStart = #tableBStart & "<th style='" & #styleHeader & "'>Cluster Network Members (Clickable Names)</th>"
#tableBStart = #tableBStart & "</tr></thead><tbody>"
rpt.out #tableBStart
#rowToggleB = 0
Repeat "#cIdx",1,#clusterCount
#thisClusterID = "C_" & #cIdx
ifStr #thisClusterID != #mainClusterID
clusterSizeDict.valueForKey "#sizeVal",#thisClusterID
clusterMembersDict.valueForKey "#rawMembersStr",#thisClusterID
clusterAnchorDict.valueForKey "#anchorNameVal",#thisClusterID
#thisRowStyleB = #styleCell
if #rowToggleB = 1
#thisRowStyleB = #styleCell & #styleRowAlt
#rowToggleB = 0
else
#rowToggleB = 1
endif
! Build member links — iterate comma-delimited ID list
#htmlMembersList = ""
replace "#membersNL2",",",return,#rawMembersStr
Lines "#mIDs2",1,999,#membersNL2
Repeat "#m2Idx",1,#mIDs2[0]
#mID = #mIDs2[#m2Idx]
replace "#bareM2ID","@","",#mID
#fullM2ID = "@" & #bareM2ID & "@"
indis.#fullM2ID.get memberIndi
ifDef memberIndi
#mName = @memberIndi.alternateName
#mLink = "<a href='//" & #mID & "'>" & #mName & "</a>"
ifStr #htmlMembersList = ""
#htmlMembersList = #mLink
else
#htmlMembersList = #htmlMembersList & ", " & #mLink
endif
endif
EndRepeat
#rowBStr = "<tr>"
#rowBStr = #rowBStr & "<td style='" & #thisRowStyleB & "'><b>" & #anchorNameVal & " Cluster</b></td>"
#rowBStr = #rowBStr & "<td style='" & #thisRowStyleB & "'>" & #sizeVal & "</td>"
#rowBStr = #rowBStr & "<td style='" & #thisRowStyleB & "; font-size:12px; line-height:1.4;'>" & #htmlMembersList & "</td>"
#rowBStr = #rowBStr & "</tr>"
rpt.out #rowBStr
endif
EndRepeat
rpt.out "</tbody></table>"
endif
! ---------------------------------------------------------------------
! Section 3: ASSO-linked isolates — emit pre-built row blocks directly
! ---------------------------------------------------------------------
#s3Head = "<h3>Section 3: Isolated Individuals with Association (ASSO) Links Only</h3>"
rpt.out #s3Head
#s3Intro = "<p>These individuals have no parent, spouse, or child links in the database, but are connected to another record via an ASSO tag. They may be witnesses, godparents, or otherwise associated with a family or individual.</p>"
rpt.out #s3Intro
if #assoSingletonCount = 0
rpt.out "<p>No such individuals found.</p>"
else
#tableCStart = "<table style='width:100%; border-collapse:collapse; margin-bottom:20px;'>"
#tableCStart = #tableCStart & "<thead><tr>"
#tableCStart = #tableCStart & "<th style='" & #styleHeader & "'>Full Name (Click to view)</th>"
#tableCStart = #tableCStart & "<th style='" & #styleHeader & "'>Lifespan</th>"
#tableCStart = #tableCStart & "<th style='" & #styleHeader & "'>Associated Record</th>"
#tableCStart = #tableCStart & "<th style='" & #styleHeader & "'>Relationship / Note</th>"
#tableCStart = #tableCStart & "</tr></thead><tbody>"
rpt.out #tableCStart
Repeat "#ai",0,#assoSingletonCount-1
assoRows.valueForKey "#aRowOut",#ai
rpt.out #aRowOut
EndRepeat
rpt.out "</tbody></table>"
endif
! Push report to viewport window
rpt.write gdoc