- name
- create-extension
- description
- MUST READ before calling create_extension. Required parameters, lifecycle methods, and wiring steps.
<!-- Generated by Embody/Envoy - Do not remove this comment - sha:6b85e409552f32cb -->
# 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.ext0object.val = "op('./MyFeatureExt').module.MyFeatureExt(me)"` -- the Extensions page is the `ext` sequence on 2025 builds: `ext0object`, `ext0name`, `ext0promote`; there is no `extension1`). 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:
```python
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.** If `ext_name='MyFeature'` and the class is `MyFeatureExt`, then `op.MyFeature.ext.MyFeature.x()` works and `op.MyFeature.ext.MyFeatureExt.x()` raises.
- **`op.MyFeature` requires a Global OP Shortcut, and `create_extension` never sets one.** Straight out of the tool, `op.MyFeature` raises `AttributeError`. Reach the COMP by a relative path (`op('./MyFeature')`, `parent.Host.op('MyFeature')`), or set `par.opshortcut` deliberately -- 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:
```python
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.
```python
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:**
```python
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.
Ver en GitHub