Create Extension Workflow
Use the create_extension MCP tool to create a fully-wired TD extension:
create_extension(
parent_path='/path/to/parent',
class_name='MyFeatureExt',
name='MyFeature', # Optional: COMP name
code='...', # Optional: initial Python code
promote=True, # Optional: promote extension methods
ext_name='MyFeature', # Optional: extension name
ext_index=0 # Optional: extension index
)
This creates: baseCOMP + text DAT + extension wiring, initialized and ready to use.
Wiring by hand: the Extension Object parameter is a constant-mode Str par whose VALUE is Python (par.extension1.val = "op('./MyFeatureExt').module.MyFeatureExt(me)"). Never set its .expr: the extension comes back None with extensionsReady True and errors() empty; the only trace is a SyntaxError ... Context:(Extension 1) on scriptErrors() (get_op_errors kind: 'script').
Extension Lifecycle Methods
Always implement these in your extension class:
class MyFeatureExt:
def __init__(self, ownerComp: COMP) -> None:
self.ownerComp = ownerComp
def onDestroyTD(self) -> None:
"""Called on old instance before TD reinitializes. Clean up callbacks, timers, etc."""
pass
def onInitTD(self) -> None:
"""Called at end of frame after init. Safe to access other extensions and cooked network."""
pass
Naming Convention
Extension classes and source DATs must follow the NameExt convention:
- Class:
MyFeatureExt - DAT:
MyFeatureExt(matches class name)
Three Namespace Tiers
TD promotes every capitalized member -- methods and class constants alike. There is no per-member opt-out, so the capital letter is the access modifier. Pick the tier by who calls it:
| Tier | Name | Reached as | Who calls it |
|---|---|---|---|
| 1. Public API | UpperCamelCase |
op.MyFeature.DoSomething() |
A user or an agent, deliberately |
| 2. Wiring | lowerCamelCase |
op.MyFeature.ext.MyFeature.onFrame() |
The COMP's own exec / callback / parexec DATs |
| 3. Private | _lowerCamelCase |
inside the class only | The class itself |
Test: could a user or an agent reasonably call this ON the COMP? If not, it is not tier 1. Promoting a frame hook is a design flaw, not a shortcut.
What makes an over-wide tier 1 harmful is concrete, not cosmetic: co-mounted extensions share one namespace and TD documents no precedence for a duplicate, so one name silently wins; and every promoted name -- constants included -- is reachable by any getattr(comp, name) dispatcher. Promoted members do NOT appear in dir(), so this is not an autocomplete argument.
Two traps:
.ext.<Name>resolves by the Extension Name parameter, not the class name. Ifext_name='MyFeature'and the class isMyFeatureExt, thenop.MyFeature.ext.MyFeature.x()works andop.MyFeature.ext.MyFeatureExt.x()raises.op.MyFeaturerequires a Global OP Shortcut, andcreate_extensionnever sets one. Straight out of the tool,op.MyFeatureraisesAttributeError. Reach the COMP by a relative path (op('./MyFeature'),parent.Host.op('MyFeature')), or setpar.opshortcutdeliberately -- it is globally unique, so assigning a name already in use silently steals it from the previous holder. Reserve it for genuine project-wide singletons.
Pass parent_shortcut when descendants need to reach the COMP. It sets par.parentshortcut, which is what makes parent.MyFeature resolve and keeps the COMP's own callback DATs off depth-coupled parent() chains:
create_extension(parent_path='/project1', class_name='MyFeatureExt',
name='MyFeature', ext_name='MyFeature',
parent_shortcut='MyFeature')
It refuses to overwrite a shortcut the COMP already declares -- re-pointing one silently redirects every reference already using it -- and says so in the result's warning.
Both shortcuts can also be set by hand:
comp.par.parentshortcut = 'MyFeature' # descendants reach it as parent.MyFeature
comp.par.opshortcut = 'MyFeature' # project-wide: op.MyFeature -- singletons only
NEVER cache extension references in variables -- always call inline. Cached refs go stale on reinit.
Referencing From Inside the Extension
Capture the owner once and navigate from it. self.ownerComp is rung 1 of the referencing ladder in td-python.md; the bare global parent() is banned in an extension class body.
class MyFeatureExt:
def __init__(self, ownerComp: COMP) -> None:
self.ownerComp = ownerComp # rung 1 -- everything else hangs off this
def rebuild(self):
target = self.ownerComp.op('./render1') # own children
cfg = self.ownerComp.par.Mode.eval() # own parameters
Extensions Inside TDXN COMPs
If the extension lives inside a TDXN-strategy COMP (or the extension's ownerComp is one), onInitTD will fire before TDXN import reconstructs the network. Any state set up during onInitTD - created operators, parameter values, stored data - is overwritten when ImportNetwork runs with clear_first=True.
Always defer initialization:
def onInitTD(self):
"""Defer setup so it runs after TDXN import completes."""
run('args[0].postInit()', self, delayFrames=5)
def postInit(self):
"""Safe to set up state here - TDXN import is complete."""
# Create operators, set parameters, initialize state
pass
This applies on project open, after every Ctrl+S (strip/restore cycle), and on manual TDXN reimport. The deferred method must be idempotent - it may run multiple times.