# Create Extension

> MUST READ before calling create_extension. Required parameters, lifecycle methods, and wiring steps.

- Skill: `dylanroscover/create-extension` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dylanroscover/create-extension`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dylanroscover/create-extension/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: dylanroscover (https://skillmd.com/u/dylanroscover)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dylanroscover/create-extension

---

<!-- Generated by Embody/Envoy - Do not remove this comment -->

# 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:

```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.

