How to export user keynote if txt is missing but keynote are somehow accessable in Revit

Hi all

could someone help me a bit.

in one of my projects i dont have keynote.txt file anymore, someone has deleted it a log time ago.

When i open the project in Revit i can go ti Keynote setings and see old location where file was and also i can click “View” and see all keynots. how can I extract or export this keynotes to my new txt?

they are somehow saved in rvt project and i dont know how to save them.

Thanks

This should allow you to export to excel, and from there into a TSV.

GetKeynotes:

#Sean Page, 2023
import clr

clr.AddReference('RevitAPI')
from Autodesk.Revit.DB import *

clr.AddReference('System')
from System.Collections.Generic import List

clr.AddReference('RevitServices')
import RevitServices
from RevitServices.Persistence import DocumentManager

doc = DocumentManager.Instance.CurrentDBDocument

#Preparing input from dynamo to revit
element = UnwrapElement(IN[0])

table = KeynoteTable.GetKeynoteTable(doc)

OUT = [[x.Key,x.KeynoteText,x.ParentKey] for x in table.GetKeyBasedTreeEntries()]
1 Like

Dear @SeanP,
Thank you, this is exactly what i am looking for this whole day.
I owe you a :beer:
Best regards

1 Like

This formats to export to an excel file directly with a few enhancements. ready to run on any file with Dynamo player to output the Keynote format directly. We could also add a CSV output to save the TXT file directly to save a few steps

# =====================================================================================
# Dynamo CPython3 - Recover Revit KeynoteTable (key-based tree) into Excel-safe 2D data.
#
# Purpose:
# - Extract the internal KeynoteTable entries (Key, Text, Parent).
# - Add a fixed TOP header row and a second-level header row (from IN[1] or filename/project).
# - Re-parent all original root entries under the second-level header key.
# - Output a rectangular (Excel-safe) List-of-Lists plus a safe sheet name and status string.
#
# Inputs:
#   IN[0] = include headers (bool), default True
#   IN[1] = second-level header key override (string), optional
#   IN[2] = (optional future use, unused here)
#
# Outputs:
#   OUT[0] = Excel-safe 2D list (rectangular)
#   OUT[1] = safe sheet name (<= 31 chars, no invalid characters)
#   OUT[2] = status string
#
# Output row format (3 columns only, per keynote file needs):
#   [Key ID] [TAB] [Keynote text] [TAB] [Parent]
# =====================================================================================

# -----------------------------------------
# Imports (declarations)
# -----------------------------------------
import clr                                   # .NET bridge used by Dynamo Python
import re                                    # Regular expressions for sheet name sanitizing

# -----------------------------------------
# Revit/Dynamo references (declarations)
# -----------------------------------------
clr.AddReference("RevitServices")            # Dynamo Revit services assembly
from RevitServices.Persistence import DocumentManager  # Provides access to the active Revit document

clr.AddReference("RevitAPI")                 # Revit API assembly
from Autodesk.Revit.DB import (              # Import needed Revit API classes
    KeynoteTable,                            # Access keynote table stored/loaded in the model session
    BuiltInParameter                         # Used to read project number/name fallback
)

# -----------------------------------------
# Active document (declaration)
# -----------------------------------------
doc = DocumentManager.Instance.CurrentDBDocument  # Current model open in Revit

# -----------------------------------------
# Inputs (declarations)
# -----------------------------------------
include_headers = IN[0] if len(IN) > 0 else True   # IN[0] header row toggle, default True
header_key_in = IN[1] if len(IN) > 1 else ""       # IN[1] optional second-level header key

# -----------------------------------------
# Constants (declarations)
# -----------------------------------------
TOP_KEY = "00 00 00-DIVISION-00"                   # Fixed top entry key
TOP_TEXT = "USER NOTES SECTION"                    # Fixed top entry text
TOP_PARENT = ""                                    # Fixed top entry parent is blank

SECOND_TEXT = "User extracted keynotes"            # Second-level header text (under TOP_KEY)

# -----------------------------------------
# Helper functions (def declarations)
# -----------------------------------------
def s(x):
    """
    Safe string conversion.
    - Converts None to empty string.
    - Avoids exceptions if str() fails.
    """
    try:
        return "" if x is None else str(x)
    except:
        return ""

def clean_text(val):
    """
    Normalizes text:
    - Replace tabs/newlines with spaces
    - Collapse whitespace
    - Trim edges
    """
    t = s(val)                                     # Convert safely to string
    t = t.replace("\t", " ")                       # Remove TABs (important for TSV output safety)
    t = t.replace("\r", " ")                       # Remove CR
    t = t.replace("\n", " ")                       # Remove LF
    t = " ".join(t.split()).strip()                # Collapse whitespace and trim
    return t

def try_get(obj, prop_name):
    """
    Safely read a property from a .NET object if it exists.
    """
    try:                                           # Attempt property access
        if hasattr(obj, prop_name):                # If the property exists...
            return getattr(obj, prop_name)         # ...return its value
    except:                                        # If anything fails...
        pass                                       # ...ignore and return None
    return None                                    # Property not found or not readable

def get_entry_text(entry):
    """
    KeynoteEntry/KeyBasedTreeEntry text accessor with fallbacks.
    Different builds/wrappers may expose different property names:
      - KeynoteText
      - Text
      - Name
    """
    v = try_get(entry, "KeynoteText")              # Try canonical property name
    if v is None:                                  # If not available...
        v = try_get(entry, "Text")                 # ...try alternate
    if v is None:                                  # If still not available...
        v = try_get(entry, "Name")                 # ...try last resort
    return clean_text(v)                           # Return cleaned text

def derive_header_key():
    """
    If IN[1] is blank, derive the second-level header key from:
    1) RVT file name stem (preferred)
    2) Project Number + Project Name
    3) "Project" fallback
    """
    # 1) filename stem (control structure: try/if)
    try:
        p = doc.PathName                           # Full file path
        if p:                                      # If doc has a saved path...
            fn = p.replace("\\", "/").split("/")[-1]  # Get file name only
            if fn.lower().endswith(".rvt"):        # If it ends with .rvt...
                fn = fn[:-4]                       # ...strip extension
            fn = clean_text(fn)                    # Clean
            if fn:                                 # If non-empty...
                return fn                          # ...return filename stem
    except:
        pass                                       # Ignore and continue to project info fallback

    # 2) project info fallback (control structure: try/if)
    pn = ""                                        # Project number accumulator
    nm = ""                                        # Project name accumulator
    try:
        pi = doc.ProjectInformation                # ProjectInformation element
        if pi:                                     # If available...
            try:
                p1 = pi.get_Parameter(BuiltInParameter.PROJECT_NUMBER)
                if p1 and p1.HasValue:
                    pn = clean_text(p1.AsString() or "")
            except:
                pass
            try:
                p2 = pi.get_Parameter(BuiltInParameter.PROJECT_NAME)
                if p2 and p2.HasValue:
                    nm = clean_text(p2.AsString() or "")
            except:
                pass
    except:
        pass

    combo = (pn + " " + nm).strip()                # Combine and trim
    if combo:                                      # If non-empty...
        return combo                               # ...use it

    # 3) final fallback
    return "Project"

def make_safe_sheet_name(name):
    """
    Excel sheet name rules:
    - Max length 31
    - Cannot contain: : \ / ? * [ ]
    - Cannot be blank
    """
    n = clean_text(name)                            # Normalize input
    if not n:                                       # If blank...
        n = "Keynotes"                              # ...fallback name
    n = re.sub(r"[:\\\/\?\*\[\]]", " ", n)          # Replace invalid chars with spaces
    n = " ".join(n.split()).strip()                 # Collapse whitespace
    if not n:                                       # If still blank...
        n = "Keynotes"                              # ...fallback
    if len(n) > 31:                                 # If too long...
        n = n[:31]                                  # ...truncate
    return n

def rectangularize(table):
    """
    Ensure output is a strict 2D rectangular list:
    - All rows have identical column counts
    - Pads with "" where needed
    This prevents Excel interop failures when row lengths vary.
    """
    if not table:                                   # If nothing to process...
        return []                                   # ...return empty list

    maxc = 0                                        # Max column count (declaration)
    for r in table:                                 # Control structure: for
        try:
            if len(r) > maxc:                        # If this row is longer...
                maxc = len(r)                        # ...update max
        except:
            if maxc < 1:
                maxc = 1

    out = []                                        # Output table accumulator
    for r in table:                                 # Control structure: for
        try:
            rr = list(r)                             # Convert row to list
        except:
            rr = [s(r)]                              # If row isn't iterable, wrap as one cell

        while len(rr) < maxc:                        # Control structure: while
            rr.append("")                            # Pad with blanks until rectangular

        if len(rr) > maxc:                           # Control structure: if
            rr = rr[:maxc]                           # Truncate if somehow longer than max

        out.append(rr)                               # Append normalized row

    return out                                       # Return rectangular table

# -----------------------------------------
# Determine second-level header key (control structure: if)
# -----------------------------------------
header_key = clean_text(header_key_in)               # Normalize IN[1]
if not header_key:                                   # If IN[1] not provided...
    header_key = derive_header_key()                  # ...derive from filename/project

# -----------------------------------------
# Determine Excel sheet name (declaration)
# -----------------------------------------
sheet_name = make_safe_sheet_name(header_key)         # Use header key as sheet name (sanitized)

# -----------------------------------------
# Build output rows (declarations)
# -----------------------------------------
rows = []                                             # Output accumulator (list-of-lists)

# -----------------------------------------
# Optional Excel headers row (control structure: if)
# -----------------------------------------
if include_headers:
    rows.append(["Key ID", "Keynote text", "Parent"]) # Header row for Excel export node

# -----------------------------------------
# Always add required top entry (fixed) (statement)
# -----------------------------------------
rows.append([TOP_KEY, TOP_TEXT, TOP_PARENT])          # Top/root line

# -----------------------------------------
# Always add required second-level header entry (statement)
# -----------------------------------------
rows.append([header_key, SECOND_TEXT, TOP_KEY])       # Links under the fixed top node

# -----------------------------------------
# Acquire KeynoteTable (declarations + try/except)
# -----------------------------------------
kt = None                                             # KeynoteTable variable declaration
try:
    kt = KeynoteTable.GetKeynoteTable(doc)            # Get the document keynote table
except:
    kt = None                                         # Fail gracefully

# -----------------------------------------
# If no keynote table is available (control structure: if/else)
# -----------------------------------------
if not kt:
    rows.append(["", "ERROR: Could not get KeynoteTable from document.", header_key])  # Error row
    OUT = (rectangularize(rows), sheet_name, "ERROR: No KeynoteTable")                 # Output tuple
else:
    # -------------------------------------
    # Pull key-based tree entries (declarations + try/except)
    # -------------------------------------
    entries = None                                    # Entries variable declaration
    try:
        entries = kt.GetKeyBasedTreeEntries()          # Retrieve the full key-based keynote tree
    except:
        entries = None                                 # Fail gracefully

    # -------------------------------------
    # If entries are missing (control structure: if/else)
    # -------------------------------------
    if not entries:
        rows.append(["", "ERROR: No KeyBasedTreeEntries available.", header_key])     # Error row
        OUT = (rectangularize(rows), sheet_name, "ERROR: No KeyBasedTreeEntries")     # Output tuple
    else:
        # ---------------------------------
        # Build dictionaries for traversal (declarations)
        # ---------------------------------
        data = {}                                     # key -> (text, parentKey)
        children = {}                                 # parentKey -> [childKey...]

        # ---------------------------------
        # Enumerate .NET entries (control structure: while)
        # ---------------------------------
        it = entries.GetEnumerator()                   # Get .NET enumerator
        while it.MoveNext():                           # While enumerator advances...
            e = it.Current                             # Current entry

            key = clean_text(try_get(e, "Key"))        # Entry key
            parent = clean_text(try_get(e, "ParentKey")) # Entry parent key
            text = get_entry_text(e)                   # Entry text

            if not key and not text:                   # Control structure: if
                continue                               # Skip empty rows

            if key:                                    # Control structure: if
                data[key] = (text, parent)             # Store key metadata
                if parent not in children:             # Control structure: if
                    children[parent] = []              # Initialize child list for parent
                children[parent].append(key)           # Add key under its parent

        # ---------------------------------
        # Depth-first emission in keynote order (declarations)
        # ---------------------------------
        visited = set()                                # Track emitted keys to prevent duplicates/loops

        def emit_key(k):
            """
            Emit row in required 3-column format:
              [Key ID, Keynote text, Parent]
            Root-level entries (parent == "") are re-parented under header_key.
            """
            if k in visited:                           # Control structure: if
                return                                 # Prevent loops
            visited.add(k)                              # Mark as emitted

            text, parent = data.get(k, ("", ""))        # Get metadata

            # Root-level (no parent) gets re-parented to header_key
            if not parent:                              # Control structure: if/else
                parent_out = header_key
            else:
                parent_out = parent

            rows.append([k, text or "", parent_out])    # Append required 3-column output row

            for ck in children.get(k, []):              # Control structure: for
                emit_key(ck)                            # Recurse into children

        # ---------------------------------
        # Emit from original roots (parent == "") (control structure: for)
        # ---------------------------------
        for root_key in children.get("", []):
            emit_key(root_key)

        # ---------------------------------
        # Emit orphans not reached from roots (control structure: for/if)
        # ---------------------------------
        for k in data.keys():
            if k not in visited:
                text, parent = data.get(k, ("", ""))
                if not parent:
                    parent_out = header_key
                else:
                    parent_out = parent
                rows.append([k, text or "", parent_out])

        # ---------------------------------
        # Final output (declarations)
        # ---------------------------------
        rows2d = rectangularize(rows)                   # Ensure Excel-safe rectangle
        status = "OK: {} rows".format(                  # Build status string
            max(0, len(rows2d) - (1 if include_headers else 0))
        )

        OUT = (rows2d, sheet_name, status)              # Output tuple for Dynamo wiring

Keynote-Extract+Internal-keynote-tree.dyn (36.6 KB) (Tested in Revit 24)

2 Likes