Generic Annotations Create Label and Link to Family Parameter

Huge gap in Autodesk Revit Family API I cannot seem to get past… I cannot create or tweak labels in Revit Families. The closest I have gotten is copying a Label parameter started from a seed file.

@jacob.small and @jeremytammik or @SeanP - any ideas on how we could programmatically populate labels in the REvit Family API?

SYMB-__-Applebees-Notes-Draft-TEST.rfa (412 KB)

# -*- coding: utf-8 -*-
# Dynamo Revit 2025 (Dynamo 3.3) - CPython3
# No inputs:
# - Read Family parameter "Note" (current type)
# - Prefer REAL Label control (bind its "Label" ElementId to FamilyParameter.Id)
# - Fallback to TextElement (set TextElement.Text)
# - Copy N times for visual verification

import clr
from RevitServices.Persistence import DocumentManager

clr.AddReference("RevitAPI")
from Autodesk.Revit.DB import (
FilteredElementCollector,
ElementTransformUtils,
StorageType,
Transaction,
XYZ,
)

doc = DocumentManager.Instance.CurrentDBDocument

# --- hardcoded defaults (no IN[]) ---
TARGET_FAMILY_PARAM = "Note"

COPIES = 1
DX_IN = 2.0
DY_IN = 0.0

def _collection_count(x):
if x is None:
return 0
try:
c = getattr(x, "Count", None)
if c is not None:
return int(c)
except Exception:
pass
try:
return len(x)
except Exception:
return 0

def _get_family_parameter_by_name(fm, name):
try:
for fp in fm.Parameters:
try:
if (fp.Definition.Name or "") == name:
return fp
except Exception:
pass
except Exception:
pass
return None

def _family_param_as_string(fm, fp):
if fp is None:
return None
try:
st = fp.StorageType
if st == StorageType.String:
return fm.CurrentType.AsString(fp)
if st == StorageType.Integer:
return str(fm.CurrentType.AsInteger(fp))
if st == StorageType.Double:
return str(fm.CurrentType.AsDouble(fp))
if st == StorageType.ElementId:
eid = fm.CurrentType.AsElementId(fp)
return "" if eid is None else str(eid.IntegerValue)
except Exception:
pass
return None

def _find_first_bindable_label_control(d):
elems = FilteredElementCollector(d).WhereElementIsNotElementType().ToElements()
for e in elems:
try:
p = e.LookupParameter("Label")
if p is None:
continue
if p.StorageType != StorageType.ElementId:
continue
if getattr(p, "IsReadOnly", False):
continue
return e
except Exception:
pass
return None

def _param_names_lower(e):
names = set()
try:
for p in e.Parameters:
try:
names.add((p.Definition.Name or "").strip().lower())
except Exception:
pass
except Exception:
pass
return names

def _score_labelish_text(e):
pn = _param_names_lower(e)
score = 0
if "label" in pn:
score += 200
if "sample text" in pn:
score += 120
if "horizontal align" in pn:
score += 40
if "vertical align" in pn:
score += 40
if "keep readable" in pn:
score += 20
return score

def _find_best_text_like_labelish(d):
elems = FilteredElementCollector(d).WhereElementIsNotElementType().ToElements()
best = None
best_score = 0
for e in elems:
try:
if not hasattr(e, "Text"):
continue
sc = _score_labelish_text(e)
if sc > best_score:
best_score = sc
best = e
except Exception:
pass
return best, best_score

if not bool(getattr(doc, "IsFamilyDocument", False)):
raise Exception("Open a Family Document in Revit (Family Editor) before running this node.")

fm = doc.FamilyManager
note_fp = _get_family_parameter_by_name(fm, TARGET_FAMILY_PARAM)
if note_fp is None:
raise Exception('Family parameter "{}" not found.'.format(TARGET_FAMILY_PARAM))

note_value = _family_param_as_string(fm, note_fp)
if note_value is None:
note_value = ""

copies = max(int(COPIES), 0)
dx_ft = float(DX_IN) / 12.0
dy_ft = float(DY_IN) / 12.0

mode = None
seed = None
seed_score = None
copy_ids = []
warnings = []

t = Transaction(doc, 'Update label (prefer bindable Label, else TextElement) + copy')
t.Start()
try:
# 1) Prefer true Label control: bind to Family parameter
label_control = _find_first_bindable_label_control(doc)
if label_control is not None:
lp = label_control.LookupParameter("Label")
lp.Set(note_fp.Id)
mode = "LabelControlBoundToFamilyParam"
seed = label_control
seed_score = None
else:
# 2) Fallback: TextElement-like
text_seed, sc = _find_best_text_like_labelish(doc)
if text_seed is None:
raise Exception(
'No bindable Label control found AND no suitable TextElement found to edit.'
)
text_seed.Text = note_value
mode = "TextElementTextSetFromFamilyParam"
seed = text_seed
seed_score = sc

# Copy for visual verification
for i in range(copies):
delta = XYZ(dx_ft * (i + 1), dy_ft * (i + 1), 0.0)
new_ids = ElementTransformUtils.CopyElement(doc, seed.Id, delta)
if _collection_count(new_ids) == 0:
warnings.append("CopyElement returned no ids on iteration {}".format(i + 1))
continue
for nid in new_ids:
copy_ids.append(nid.IntegerValue)

t.Commit()
except Exception:
try:
t.RollBack()
except Exception:
pass
raise

OUT = {
"mode": mode,
"seedId": seed.Id.IntegerValue,
"seedType": seed.GetType().FullName,
"seedScore": seed_score,
"familyParamName": TARGET_FAMILY_PARAM,
"familyParamId": note_fp.Id.IntegerValue,
"familyParamStorageType": int(note_fp.StorageType),
"valueUsed": note_value,
"copies": copies,
"dx_in": DX_IN,
"dy_in": DY_IN,
"copyIds": copy_ids,
"warnings": warnings,
}

As far as I know, and based on the last time I looked into doing this, the Label is not exposed in the API and therefore you cannot modify.

This matches my recollection as well. Depending on the what is necessitating the need there may be a functional work-arounds (copy from a seed file being quite valid and honestly might be a better solution than making it anew for many situations).