# TDN Specification

**Version 1.3**

TDN is the substrate that makes "create at the speed of thought" possible. It's the format your AI agent reads to understand what's on the screen, the format that lets you compare two attempts side by side, and the format a network rebuilds itself from on the next project open. Without it, AI-driven TouchDesigner work is one-directional — you generate, and you're stuck with what you got. With it, every step of the loop — generate, compare, revert, branch — runs at the speed of typing.

TDN (TouchDesigner Network) is a JSON-based file format for representing TouchDesigner operator networks as human-readable, diffable text. It stores only non-default properties, keeping files minimal.

- File extension: `.tdn`
- MIME type: `application/json`
- Encoding: UTF-8
- JSON Schema: [`tdn.schema.json`](../tdn.schema.json)

---

## Document Structure

A `.tdn` file is a JSON object with the following top-level fields:

```json
{
  "format": "tdn",
  "version": "1.3",
  "build": 1,
  "generator": "Embody/5.0.237",
  "td_build": "2025.32050",
  "exported_at": "2025-02-19T12:34:56Z",
  "network_path": "/",
  "type": "containerCOMP",
  "options": {
    "include_dat_content": true,
    "include_storage": true
  },
  "type_defaults": { ... },
  "par_templates": { ... },
  "custom_pars": { ... },
  "parameters": { ... },
  "flags": [ ... ],
  "color": [0.3, 0.5, 0.9],
  "tags": ["tdn"],
  "comment": "Main UI container",
  "storage": { ... },
  "operators": [ ... ],
  "annotations": [ ... ]
}
```

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `format` | string | Yes | Always `"tdn"`. Identifies the file format. |
| `version` | string | Yes | Format version. Currently `"1.3"`. |
| `build` | integer | No | Embody build number for the exported COMP. Incremented each time the network is saved via Embody. Useful for version tracking and git diffs. `null` if the COMP has no build tracking. |
| `generator` | string | Yes | Tool that produced the file (e.g., `"Embody/5.0.237"`). |
| `td_build` | string | Yes | TouchDesigner version and build number (e.g., `"2025.32050"`). |
| `exported_at` | string | Yes | ISO 8601 UTC timestamp of export (e.g., `"2025-02-19T12:34:56Z"`). |
| `network_path` | string | Yes | The COMP path represented by this file (e.g., `"/"` for the entire project). |
| `type` | string | No | TouchDesigner operator type of the target COMP (e.g., `"baseCOMP"`, `"containerCOMP"`, `"geometryCOMP"`). Added in v1.1. Makes the file self-describing for portable import into other projects. On import, a mismatch between this field and the destination COMP's type triggers a warning. |
| `options` | object | Yes | Export settings used when generating this file. |
| `options.include_dat_content` | boolean | Yes | Whether DAT text/table content was included in the export. |
| `options.include_storage` | boolean | No | Whether operator storage entries were included in the export. Absent means `true` (included). Added in v1.2. Can be toggled per-COMP via the `embed_storage_in_tdn` storage key. |
| `type_defaults` | object | No | Per-type shared properties (parameters, flags, size, color, tags). See [Type Defaults](#type-defaults). |
| `par_templates` | object | No | Reusable custom parameter page definitions. See [Parameter Templates](#parameter-templates). |
| `custom_pars` | object | No | Target COMP's own custom parameter definitions and values. Same format as operator-level [`custom_pars`](#custom-parameters). Only present if the target COMP has custom parameters. |
| `parameters` | object | No | Target COMP's own non-default built-in parameter values. Same format as operator-level [`parameters`](#parameters). Only present if the target COMP has non-default built-in parameters. |
| `flags` | array | No | Target COMP's own non-default [flags](#flags). Same format as operator-level flags. Added in v1.1. |
| `color` | `[r, g, b]` | No | Target COMP's node color, if different from default gray. RGB floats 0.0–1.0, rounded to 4 decimal places. Added in v1.1. |
| `tags` | array of strings | No | Target COMP's tags, if any. Added in v1.1. |
| `comment` | string | No | Target COMP's node comment, if non-empty. Added in v1.1. |
| `storage` | object | No | Target COMP's persistent [storage entries](#operator-storage). Same format as operator-level storage. Added in v1.1. |
| `operators` | array | Yes | Array of [operator objects](#operator-object). |
| `annotations` | array | No | Array of [annotation objects](#annotations). Only present if the root COMP contains annotations. |

---

## Operator Object

Each entry in the `operators` array (and in nested `children` arrays) is an operator object:

```json
{
  "name": "noise1",
  "type": "noiseTOP",
  "position": [200, -100],
  "size": [300, 150],
  "color": [0.2, 0.6, 0.9],
  "comment": "Primary noise source",
  "tags": ["audio", "generator"],
  "parameters": { ... },
  "custom_pars": { ... },
  "flags": [ ... ],
  "storage": { ... },
  "startup_storage": { ... },
  "inputs": [ ... ],
  "comp_inputs": [ ... ],
  "dat_content": "...",
  "dat_content_format": "text",
  "children": [ ... ],
  "palette_clone": true
}
```

### Field Reference

| Field | Type | Required | Condition for inclusion |
|-------|------|----------|------------------------|
| `name` | string | Yes | Always included. The operator's name. |
| `type` | string | Yes | Always included. TouchDesigner operator type (e.g., `"baseCOMP"`, `"noiseTOP"`, `"textDAT"`, `"waveCHOP"`). |
| `position` | `[x, y]` | No | Omitted when `[0, 0]` (default). Included only for operators not at the origin. |
| `size` | `[width, height]` | No | Only if different from the default `[200, 100]`. |
| `color` | `[r, g, b]` | No | Only if different from the default gray `[0.545, 0.545, 0.545]` (tolerance: 0.01 per channel). RGB values are floats from 0.0 to 1.0, rounded to 4 decimal places. |
| `comment` | string | No | Only if non-empty. Annotation text on the node. |
| `tags` | array of strings | No | Only if the operator has tags. |
| `dock` | string | No | Only if the operator is docked to another operator. Sibling name or full path. |
| `parameters` | object | No | Only if there are non-default [built-in parameters](#built-in-parameters) (after [type_defaults](#type-defaults) are factored out). |
| `custom_pars` | object | No | Only if the operator has [custom parameters](#custom-parameters). Dict keyed by page name. |
| `flags` | array | No | Only if any [flags](#flags) differ from their defaults. |
| `storage` | object | No | Only if the operator has non-transient [storage entries](#operator-storage). Dict of key-value pairs. |
| `startup_storage` | object | No | Only if the operator has [startup storage entries](#startup-storage). Values restored via `storeStartupValue()` on import. |
| `inputs` | array | No | Only if the operator has [operator-level connections](#operator-connections). |
| `comp_inputs` | array | No | Only if the operator has [COMP-level connections](#comp-connections). COMPs only. |
| `dat_content` | string or array | No | Only for DAT-family operators when `include_dat_content` is `true`. See [DAT Content](#dat-content). |
| `dat_content_format` | string | No | `"text"` or `"table"`. Present whenever `dat_content` is present. |
| `children` | array | No | Only for COMPs with child operators (excluding palette clones). Contains nested operator objects. See [Children and Hierarchy](#children-and-hierarchy). |
| `annotations` | array | No | Only for COMPs with [annotations](#annotations). Contains annotation objects. |
| `palette_clone` | boolean | No | `true` if this COMP is cloned from the TouchDesigner palette (`/sys/`). When set, children are not exported (TD recreates them from the clone source). |
| `sequences` | object | No | Only if the operator has built-in parameter sequences with non-default block counts or values. See [Built-in Parameter Sequences](#built-in-parameter-sequences). *Added in v1.3.* |
| `tdn_ref` | string | No | Only for COMPs with their own TDN externalization. Relative file path to the child's `.tdn` file. Mutually exclusive with `children`. See [COMP References](#comp-references-tdn_ref). *Added in v1.2.* |
| `tox_ref` | string | No | Only for COMPs with their own TOX externalization. Relative file path to the child's `.tox` file. Mutually exclusive with `children`. See [TOX References](#tox-references-tox_ref). *Added in v1.4.* |

### Compact Formatting

Short arrays and objects (≤80 characters when inlined) are written on a single line:

```json
"position": [200, -100],
"size": [300, 150],
"color": [0.2, 0.6, 0.9],
"tags": ["audio", "generator"],
"inputs": ["noise1"]
```

Longer arrays remain multi-line. This dramatically reduces file size while maintaining readability.

---

## Built-in Parameters

The `parameters` object maps parameter names to their values. Only built-in (non-custom) parameters whose current value differs from their default are included. Parameters shared unanimously across all operators of a type are factored into [type_defaults](#type-defaults) instead.

### Parameter Modes

Parameters can be in one of three exportable modes:

**Constant** — the value is stored directly:
```json
"parameters": {
  "tx": 100,
  "name": "hello",
  "active": true
}
```

**Expression** — prefixed with `=`. A Python expression that TouchDesigner evaluates each frame:
```json
"parameters": {
  "tx": "=absTime.frame * 0.1",
  "resizecomp": "=me"
}
```

**Bind** — prefixed with `~`. A reference expression that binds this parameter to another:
```json
"parameters": {
  "tx": "~op('controller').par.posx"
}
```

!!! note
    A fourth mode, **Export**, exists in TouchDesigner but is not stored in TDN. Export mode is set by the exporting operator, not the parameter itself, and cannot be meaningfully imported.

### Escaping

Constant string values that literally start with `=` or `~` are escaped by doubling the prefix:

| Stored value | Meaning |
|-------------|---------|
| `"=foo"` | Expression: `foo` |
| `"==foo"` | Constant string: `"=foo"` |
| `"~bar"` | Bind expression: `bar` |
| `"~~bar"` | Constant string: `"~bar"` |

### Skipped Parameters

The following built-in parameters are never exported, as they are internal actions or not meaningful outside a live project:

**By name:**

- `externaltox`, `enableexternaltox`, `reloadtox`
- `reinitextensions`, `savebackup`
- `savecustom`, `reloadcustom`
- `pageindex`

!!! info
    `file` and `syncfile` parameters ARE exported when non-default, so that TDN files are self-contained for externalized DATs. This ensures file references survive import into a different project.

**By style:**

- `Pulse` — action buttons (fire-once, no persistent state)
- `Momentary` — momentary buttons (no persistent state)
- `Header` — visual section dividers (no value)

**Other exclusions:**

- Read-only parameters
- Custom parameters (handled separately in `custom_pars`)

### Non-Default Comparison

A constant parameter is included only if its current value differs from its default:

- **Floats**: considered different if `abs(current - default) > 1e-9`
- **OP-reference parameters**: `None` and `""` are treated as equivalent (both mean "no operator connected")
- **All other types**: standard equality comparison (`!=`)

### Divergent Defaults and the Creation-Defaults Catalog

Some TouchDesigner operators reset certain parameters during initialization, meaning the value reported by `p.default` differs from the value TouchDesigner actually assigns when the operator is created. For example:

- `cameraCOMP` `tz`: `p.default` reports `0`, but TD creates cameras with `tz = 5`
- `lightCOMP` `tz`: `p.default` reports `0`, but TD creates lights with `tz = 5`
- `renderTOP` resolution parameters: reported defaults differ from creation values

If TDN used `p.default` directly for non-default comparison, a user-set value that happens to match `p.default` but differs from the actual creation value would be silently omitted from export. On import, TD would create the operator with the (different) creation value, and the user's intended value would be lost. Conversely, a parameter at the true creation value might be incorrectly included in the export, bloating the file with false positives.

#### The Catalog System

Embody solves this with a three-tier system that discovers and caches the true creation values for every operator type:

**1. Background scan at startup**

On project open, the `CatalogManager` extension checks whether a creation-values catalog exists for the current TD build (stored as a JSON file in the `.embody/` directory at the project root). If no catalog exists, it runs a background scan:

- Iterates every creatable operator type in TouchDesigner (TOPs, CHOPs, SOPs, DATs, MATs, COMPs, POPs)
- Creates a temporary instance of each type
- Records `p.val` (the actual value TD assigned) for every non-custom, non-read-only parameter

The catalog stores **all** creation values, not just divergent ones. This means TDN always has the ground-truth creation value for every parameter — `_getCreationDefault()` returns the catalog value directly, falling back to `p.default` only for parameters the catalog doesn't cover. The divergent-default problem (where `p.val` differs from `p.default`) is solved implicitly: by recording what TD actually assigns, the catalog captures both correct and divergent defaults without needing to distinguish between them.

The scan processes 1–2 operator types per frame to avoid dropping frames. The resulting catalog is written to `.embody/catalog_{build}.json` (e.g., `.embody/catalog_099.2025.32280.json`) and cached for future sessions on the same TD build.

**2. Export/import correction**

During TDN export, the `_getCreationDefault()` method checks the catalog before falling back to `p.default`:

```python
def _getCreationDefault(self, op_type, par_name, par):
    divergent = self._getDivergentDefaults(op_type)
    if par_name in divergent:
        return divergent[par_name]
    return par.default
```

This means TDN compares parameter values against the *actual creation value*, not the *reported default*. Parameters are included in the export if and only if the user's value truly differs from what TD would assign to a freshly created operator.

**3. Cross-build patching**

When a `.tdn` file is opened on a different TD build than the one that exported it, creation defaults may have shifted between builds. The `CatalogManager` handles this automatically:

1. Reads the `td_build` field from each `.tdn` file header
2. Loads the catalog for the source build (if available)
3. Loads the catalog for the current build
4. Finds parameters whose creation defaults changed between the two builds
5. For each affected operator, checks if the current value matches the *new* default — if so, the parameter was omitted during export (because it matched the *old* default), and TD has now assigned the wrong new default
6. Patches those parameters back to the old creation value

A summary dialog is shown to the user listing every corrected parameter.

#### Fallback chain

The catalog is loaded in priority order:

1. **`.embody/` catalog file** — per-build JSON written by the background scan
2. **Embedded `divergent_defaults` table** — a DAT inside the Embody COMP with bootstrap data for known TD builds
3. **On-the-fly probing** — if neither source has data for the current build, a temporary operator is created at export time to discover the true creation value

This ensures correct non-default comparison regardless of TD version, even on builds that Embody has never seen before.

---

## Built-in Parameter Sequences

*Added in v1.3.*

Many TouchDesigner operators have **resizable parameter blocks** — mathmixPOP Combine blocks, glslPOP/glslTOP uniform sequences, attributePOP attribute blocks, constantCHOP channel blocks, etc. These are called **parameter sequences** in TD's API.

The `sequences` object stores per-operator sequence data. It is keyed by sequence name, where each value is an array of block objects containing only non-default parameter values using **base names** (without the sequence prefix or block index):

```json
{
  "name": "mathmix1",
  "type": "mathmixPOP",
  "sequences": {
    "comb": [
      {"oper": "A", "scopea": "P", "result": "startPos"},
      {"oper": "A + B", "scopea": "vel", "scopeb": "direction", "result": "vel"},
      {}
    ]
  }
}
```

### Design

- **Array length = `numBlocks`**: The importer sets `seq.numBlocks = len(blocks)` to create the right number of parameter slots before setting values.
- **Base names**: `"oper"` rather than `"comb0oper"`. The full parameter name is `{seqName}{blockIndex}{baseName}` (e.g., `comb2oper`), but only the base name is stored. This makes the format portable and readable.
- **Empty objects `{}`**: Represent blocks where all parameters are at their default values. Included to preserve correct block count and ordering.
- **Omission**: The `sequences` key is omitted entirely when all sequences on the operator have their default block count and all block values are defaults.
- **Value shorthand**: Same as built-in parameters — `=` prefix for expressions, `~` prefix for binds, literal values for constants.

### Import Phase

Sequences are expanded in **Phase 2.5** (between custom parameter creation and parameter value setting). This ensures the dynamically-created sequence parameters exist before Phase 3 attempts to set values on them.

### Exclusion from type_defaults

Sequence data is **never included in `type_defaults`**. Sequences are inherently per-instance (different operators have different block counts), so they cannot be compressed into per-type defaults.

---

## Custom Parameters

The `custom_pars` object maps page names to arrays of parameter definitions. Unlike built-in parameters, custom parameters are **always fully exported** (including their definitions, ranges, and current values) because the importer must recreate them from scratch.

!!! note
    Only COMPs can have custom parameters in TouchDesigner.

### Page-Grouped Format

Custom parameters are grouped by page name. Each page contains an array of parameter definitions:

```json
"custom_pars": {
  "Controls": [
    {
      "name": "Speed",
      "style": "Float",
      "default": 1,
      "max": 10,
      "clampMin": true,
      "normMax": 5,
      "value": 2.5
    },
    {
      "name": "Mode",
      "style": "Menu",
      "menuNames": ["linear", "ease", "bounce"],
      "menuLabels": ["Linear", "Ease In/Out", "Bounce"],
      "value": 1
    }
  ],
  "About": [
    {
      "name": "Build",
      "style": "Int",
      "label": "Build Number",
      "readOnly": true,
      "value": 14
    }
  ]
}
```

The page name is the dict key — individual parameter definitions do not include a `"page"` field.

### Template References

When a page's parameter definitions match a [parameter template](#parameter-templates), the page is stored as a template reference with value overrides:

```json
"custom_pars": {
  "About": {
    "$t": "about",
    "Build": 14,
    "Date": "2026-02-19 16:09:43 UTC",
    "Touchbuild": "2025.32050"
  }
}
```

The `$t` field names the template. Other keys are parameter value overrides (parameter name → current value). See [Parameter Templates](#parameter-templates).

### Custom Parameter Definition

| Field | Type | Condition | Description |
|-------|------|-----------|-------------|
| `name` | string | Always | Base name of the parameter. For multi-component parameters, this is the group name without any suffix (e.g., `"Pos"` for a group of `Posx`, `Posy`, `Posz`). |
| `label` | string | If different from `name` | Display label shown in the parameter dialog. Omitted when the label matches the parameter name. |
| `style` | string | Always | Parameter style. See [Supported Styles](#supported-styles). |
| `size` | integer | Multi-component `Float`/`Int` only | Number of components when > 1 (e.g., `3` for a 3-component float). |
| `default` | any | If non-standard | Default value. Omitted when the default is a standard value (`0`, `0.0`, `""`, or `false`). |
| `min` | number | If != `0` | Minimum value. |
| `max` | number | If != `1` | Maximum value. |
| `clampMin` | boolean | If `true` | Whether the value is clamped to `min`. |
| `clampMax` | boolean | If `true` | Whether the value is clamped to `max`. |
| `normMin` | number | If != `0` | Normalized range minimum (for slider UI). |
| `normMax` | number | If != `1` | Normalized range maximum (for slider UI). |
| `menuNames` | array of strings | Manually defined menus | Internal names for each menu option. |
| `menuLabels` | array of strings | If different from `menuNames` | Display labels for each menu option. Omitted when labels match names. |
| `menuSource` | string | Dynamically populated menus | DAT path or expression that populates the menu. When present, `menuNames`/`menuLabels` are omitted. |
| `startSection` | boolean | If `true` | Whether this parameter starts a new visual section. |
| `readOnly` | boolean | If `true` | Whether the parameter is read-only. |
| `help` | string | If non-empty | Tooltip help text shown when hovering the parameter in the dialog. Omitted when empty. |
| `value` | any | Single-component, if non-default | Current value. Can be a constant, `"=expr"` string, or `"~bind"` string. Omitted when the value equals the default. |
| `values` | array | Multi-component, if any non-default | Current values for each component. Same format as `value` per element. Omitted when all values equal their defaults. |

### Supported Styles

All 32 parameter styles recognized by TDN:

| Style | Category | Description |
|-------|----------|-------------|
| `Float` | Numeric | Floating-point number. Supports `size` > 1 for multi-component (suffixed `1`, `2`, `3`...). |
| `Int` | Numeric | Integer number. Supports `size` > 1 for multi-component (suffixed `1`, `2`, `3`...). |
| `XY` | Numeric compound | Two-component float (`x`, `y`). |
| `XYZ` | Numeric compound | Three-component float (`x`, `y`, `z`). |
| `XYZW` | Numeric compound | Four-component float (`x`, `y`, `z`, `w`). |
| `WH` | Numeric compound | Two-component float (`w`, `h`). |
| `UV` | Numeric compound | Two-component float (`u`, `v`). |
| `UVW` | Numeric compound | Three-component float (`u`, `v`, `w`). |
| `RGB` | Numeric compound | Three-component float (`r`, `g`, `b`). |
| `RGBA` | Numeric compound | Four-component float (`r`, `g`, `b`, `a`). |
| `Str` | String | Text string. |
| `Menu` | Menu | Dropdown menu. Uses `menuNames`/`menuLabels` for static menus, or `menuSource` for dynamic menus. |
| `StrMenu` | Menu | Editable string with dropdown suggestions. Uses `menuNames`/`menuLabels` or `menuSource`. |
| `Toggle` | Boolean | On/off checkbox. |
| `Pulse` | Action | Fire-once button (no persistent value). |
| `Momentary` | Action | Button that is active while held. |
| `Header` | Visual | Section header label (no value). |
| `File` | Path | File path selector (open). |
| `FileSave` | Path | File path selector (save). |
| `Folder` | Path | Folder path selector. |
| `Python` | Code | Python expression field. |
| `OP` | Reference | Operator path reference (any type). |
| `COMP` | Reference | COMP operator reference. |
| `TOP` | Reference | TOP operator reference. |
| `CHOP` | Reference | CHOP operator reference. |
| `SOP` | Reference | SOP operator reference. |
| `DAT` | Reference | DAT operator reference. |
| `MAT` | Reference | MAT operator reference. |
| `POP` | Reference | POP operator reference. |
| `Object` | Reference | Object COMP reference. |
| `PanelCOMP` | Reference | Panel COMP reference. |
| `Sequence` | Sequence | Sequence block parameter. |

### Multi-Component Parameters

Some parameters consist of multiple related components grouped together (called "tuplets" in TouchDesigner).

**Compound styles** (XY, XYZ, XYZW, WH, UV, UVW, RGB, RGBA) have named suffixes:

```json
{
  "name": "Pos",
  "style": "XYZ",
  "values": [10.0, 20.0, 30.0]
}
```

This creates three parameters: `Posx`, `Posy`, `Posz`. The suffix mappings are:

| Style | Suffixes |
|-------|----------|
| `XY` | `x`, `y` |
| `XYZ` | `x`, `y`, `z` |
| `XYZW` | `x`, `y`, `z`, `w` |
| `WH` | `w`, `h` |
| `UV` | `u`, `v` |
| `UVW` | `u`, `v`, `w` |
| `RGB` | `r`, `g`, `b` |
| `RGBA` | `r`, `g`, `b`, `a` |

**Numeric multi-component** (Float or Int with `size` > 1) use numeric suffixes:

```json
{
  "name": "Weight",
  "style": "Float",
  "size": 3,
  "values": [0.5, 0.3, 0.2]
}
```

This creates three parameters: `Weight1`, `Weight2`, `Weight3`.

---

## Type Defaults

The `type_defaults` section hoists properties that are shared unanimously across **all** operators of a given type into a single location, removing them from individual operators. Supported properties: `parameters`, `flags`, `size`, `color`, and `tags`.

```json
"type_defaults": {
  "containerCOMP": {
    "parameters": {
      "borderover": false,
      "reloadbuiltin": false,
      "resizecomp": "=me",
      "repocomp": "=me"
    },
    "flags": ["viewer"],
    "size": [300, 150]
  },
  "textDAT": {
    "parameters": {
      "language": "text"
    },
    "flags": ["viewer"],
    "size": [130, 90],
    "color": [0.67, 0.67, 0.67],
    "tags": ["source"]
  }
}
```

### Unanimity Rule

A property enters `type_defaults` **only** if:

1. The operator type appears 2+ times in the export
2. The property is present on **every** operator of that type
3. The property has the **same value** across all operators of that type

This eliminates the need for a "reset to default" marker — if a property is in `type_defaults`, every operator of that type has it.

### Import Behavior

On import, `type_defaults` are merged into each operator before the relevant import phase. `parameters` use dict-level merge (operator-specific keys override individual defaults). `flags`, `size`, `color`, and `tags` use whole-value replacement (the operator either has its own value or inherits entirely from type_defaults):

```
effective_params = type_defaults[op_type].parameters | operator.parameters
effective_flags  = operator.flags  ?? type_defaults[op_type].flags
effective_size   = operator.size   ?? type_defaults[op_type].size
effective_color  = operator.color  ?? type_defaults[op_type].color
effective_tags   = operator.tags   ?? type_defaults[op_type].tags
```

### When Type Defaults are Omitted

- If no types have 2+ operators with shared properties, the `type_defaults` key is absent
- Single-instance operator types never contribute to type_defaults

---

## Parameter Templates

The `par_templates` section extracts custom parameter page definitions that repeat across 2+ operators into named, reusable templates.

```json
"par_templates": {
  "about": [
    {"name": "Build", "style": "Int", "label": "Build Number", "readOnly": true},
    {"name": "Date", "style": "Str", "label": "Build Date", "readOnly": true},
    {"name": "Touchbuild", "style": "Str", "label": "Touch Build", "readOnly": true}
  ]
}
```

Templates contain parameter definitions **without values** — they define the structure (name, style, label, ranges, etc.) of a page's parameters.

### Template References

Operators reference templates via `$t` in their `custom_pars`:

```json
"custom_pars": {
  "About": {
    "$t": "about",
    "Build": 14,
    "Date": "2026-02-19 16:09:43 UTC",
    "Touchbuild": "2025.32050"
  }
}
```

| Field | Description |
|-------|-------------|
| `$t` | Template name (matches a key in `par_templates`) |
| Other keys | Value overrides: parameter name → current value |

### Import Behavior

On import, `$t` references are resolved before Phase 2 (create custom parameters). Each template reference is expanded back into a full array of parameter definitions, with value overrides applied:

```json
// Resolved from template + overrides:
"About": [
  {"name": "Build", "style": "Int", "label": "Build Number", "readOnly": true, "value": 14},
  {"name": "Date", "style": "Str", "label": "Build Date", "readOnly": true, "value": "2026-02-19 16:09:43 UTC"},
  {"name": "Touchbuild", "style": "Str", "label": "Touch Build", "readOnly": true, "value": "2025.32050"}
]
```

### Template Naming

Template names are derived from the page name (lowercased, spaces replaced with underscores). Collision suffixes (`_2`, `_3`) are added if multiple distinct page definitions share the same page name.

### When Templates are Omitted

- If no page definition appears on 2+ operators, the `par_templates` key is absent
- Pages unique to a single operator are always stored inline

---

## Flags

The `flags` array contains string names of flags whose values differ from their defaults.

| Flag | Default | Description |
|------|---------|-------------|
| `bypass` | `false` | Operator is skipped in the processing chain. |
| `lock` | `false` | Operator output is locked (frozen). See [Lock Flag Limitation](#lock-flag-limitation). |
| `display` | `false` | Marks this operator as the display output (blue flag). |
| `render` | `false` | Marks this operator for rendering (purple flag). |
| `viewer` | `false` | Shows the operator's viewer on its node tile. |
| `expose` | `true` | Whether the node is visible in the network editor. |
| `allowCooking` | `true` | Whether the COMP is allowed to cook. **COMPs only.** |

### Format

Flags that default to `false` are listed by name when set to `true`:
```json
"flags": ["viewer", "display"]
```

Flags that default to `true` use a `-` prefix when set to `false`:
```json
"flags": ["-expose"]
```

Combined example — viewer on, cooking disabled:
```json
"flags": ["viewer", "-allowCooking"]
```

### Lock Flag Limitation

!!! warning "Locked content is NOT preserved for TOPs, CHOPs, or SOPs"

    TDN preserves the **lock flag** for all operator families, but it **cannot store frozen pixel, channel, or geometry data**. After a TDN round-trip (export + import), locked non-DAT operators will be locked but **empty** — no texture, no samples, no mesh.

    **This is by design, not a bug.** Storing binary data would defeat TDN's purpose as a diffable, version-control-friendly format. A single locked 4K TOP could add over 100 MB to a `.tdn` file.

    Embody warns you at save time if your network contains locked non-DAT operators.

The `lock` flag applies to **all** operator families — DATs, TOPs, CHOPs, and SOPs — freezing their cooked output so it no longer updates from inputs or parameters. However, TDN only persists the frozen data for DATs.

| Family | Flag persisted? | Frozen data persisted? | Notes |
|--------|:-:|:-:|---|
| **DAT** | Yes | Yes (via `dat_content`) | Full round-trip: both the lock state and text/table content are preserved. |
| **TOP** | Yes | **No** | Pixel data is not stored. On import, the lock flag is set but no texture data exists. The operator will appear black. |
| **CHOP** | Yes | **No** | Channel data is not stored. On import, the lock flag is set but no sample data exists. |
| **SOP** | Yes | **No** | Geometry data is not stored. On import, the lock flag is set but no mesh data exists. |

**Workarounds:**

- **Unlock before saving** — the operator will re-cook from its inputs on reload.
- **Use TOX strategy** instead of TDN for COMPs containing locked non-DAT operators. TOX files are binary and preserve all locked content.
- **Store data externally** — write pixel data to image files, channel data to CSV, etc., and reference them from your network.

---

## Connections

TouchDesigner operators have two kinds of connections. TDN stores both as string arrays where array position equals the input index.

### Operator Connections

Standard wiring between operators (left/right connectors). Stored in the `inputs` array:

```json
"inputs": ["noise1"]
```

Multi-input example — `noise1` at index 0, nothing at index 1, `level1` at index 2:
```json
"inputs": ["noise1", null, "level1"]
```

### COMP Connections

COMP-level wiring (top/bottom connectors). Only applicable to COMPs. Stored in the `comp_inputs` array:

```json
"comp_inputs": ["container1"]
```

### Source Resolution

Each string element references the source operator:

- If the source operator is a **sibling** (same parent), only the operator **name** is stored (e.g., `"noise1"`).
- If the source is in a **different parent**, the full **path** is stored (e.g., `"/project/other/transform1"`).
- `null` means no connection at that index.

On import, the source is resolved by first looking for a sibling with that name, then falling back to interpreting it as a full path.

---

## Docking

Operators in TouchDesigner can be visually docked to other operators. A docked operator moves with its host in the network editor and can be collapsed into the host's tile.

When an operator is docked, TDN records a `"dock"` field on it:

| Field | Type | Condition |
|-------|------|-----------|
| `dock` | string | Only if the operator is docked to another operator. |

The value is the **sibling name** of the dock host when they share a parent COMP, or the **full operator path** for cross-hierarchy docking. This follows the same reference convention as [operator connections](#connections).

Docking is a purely visual/organizational relationship — it has no effect on operator behavior, data flow, or cooking. It is omitted from `type_defaults` because docking is always instance-specific.

**Example:**

```json
{
  "name": "info1",
  "type": "infoDAT",
  "dock": "noise1"
}
```

During import, the dock target is resolved by sibling name first, then full path fallback. If the target cannot be found, a warning is logged and docking is skipped gracefully.

---

## DAT Content

DAT-family operators can optionally include their text or table data. This is controlled by the `include_dat_content` option.

### Text Format

For text-based DATs (textDAT, etc.):

```json
{
  "name": "script1",
  "type": "textDAT",
  "dat_content": "print('hello world')\nprint('goodbye')",
  "dat_content_format": "text"
}
```

- `dat_content`: raw text string with newlines
- `dat_content_format`: `"text"`

### Table Format

For table-based DATs (tableDAT, etc.):

```json
{
  "name": "lookup1",
  "type": "tableDAT",
  "dat_content": [
    ["name", "value", "type"],
    ["speed", "1.5", "float"],
    ["active", "1", "int"]
  ],
  "dat_content_format": "table"
}
```

- `dat_content`: array of row arrays (each row is an array of cell value strings)
- `dat_content_format`: `"table"`

### Inclusion Conditions

DAT content is only included when:

1. The operator belongs to the DAT family
2. The `include_dat_content` option is `true`
3. The DAT has content (non-empty text or at least one row)

---

## Operator Storage

Every TouchDesigner operator has a `.storage` dictionary for persistent Python data. TDN exports all serializable storage entries except known transient/internal keys used by Embody's runtime.

### Per-COMP Storage Toggle

Storage export can be disabled per-COMP by setting the `embed_storage_in_tdn` storage key to `false` on the target COMP, or globally via Embody's `Embedstorageintdns` parameter. When disabled, the `options.include_storage` field is `false` and operator storage entries are omitted — except for Embody control keys (`embed_dats_in_tdn`, `embed_storage_in_tdn`) which are always preserved to maintain round-trip fidelity of export preferences.

### Format

```json
{
  "name": "my_comp",
  "type": "baseCOMP",
  "storage": {
    "count": 42,
    "label": "hello",
    "items": [1, 2, 3],
    "config": {"key": "value"},
    "coords": {"$type": "tuple", "$value": [10, 20]},
    "tags": {"$type": "set", "$value": ["a", "b", "c"]},
    "raw": {"$type": "bytes", "$value": "AAEC/w=="}
  }
}
```

### Value Serialization

Python types that map directly to JSON are stored as-is. Non-JSON types use a `$type`/`$value` wrapper:

| Python Type | JSON Representation |
|-------------|---------------------|
| `str`, `int`, `float`, `bool` | Direct JSON value |
| `None` | `null` |
| `list` | JSON array (recursive) |
| `dict` | JSON object (recursive, string keys only) |
| `tuple` | `{"$type": "tuple", "$value": [...]}` |
| `set` | `{"$type": "set", "$value": [...]}` (sorted for determinism) |
| `bytes` | `{"$type": "bytes", "$value": "<base64>"}` |

Values that cannot be serialized to JSON (threading objects, operator references, custom class instances) are silently skipped during export.

### Skipped Keys

The following storage keys are never exported (runtime/transient state managed by Embody):

`_tdn_stripped_paths`, `_git_root`, `envoy_running`, `envoy_shutdown_event`, `expanded_paths`, `manage_file_path`, `visible_count`, `hover`

### Startup Storage

TouchDesigner supports `storeStartupValue(key, value)` — values that reset to their initial state on every project open, regardless of what they were when the file was saved. TDN supports this via an optional `startup_storage` field:

```json
{
  "name": "my_comp",
  "type": "baseCOMP",
  "storage": {"runtime_count": 0},
  "startup_storage": {"version": 1, "default_mode": "auto"}
}
```

On import, keys in `startup_storage` are restored via `storeStartupValue()`, while keys in `storage` use `store()`.

**Export limitation:** TouchDesigner provides no API to introspect which storage keys were set with `storeStartupValue()` vs `store()`. During automatic export, all entries go into `storage`. The `startup_storage` field must be populated manually or by tools that know the intent (e.g., code generators, StorageManager-aware exporters).

### Import Behavior

Storage is restored during Phase 6a (after DAT content, before positions). Keys in `storage` are restored via `op.store(key, value)`. Keys in `startup_storage` are restored via `op.storeStartupValue(key, value)`. `$type` wrappers are deserialized back to their Python types. Unknown `$type` values are treated as plain dicts with a warning logged.

---

## Children and Hierarchy

COMPs can contain child operators. These are stored in the `children` array, which contains nested operator objects following the exact same schema:

```json
{
  "name": "container1",
  "type": "baseCOMP",
  "children": [
    {
      "name": "noise1",
      "type": "noiseTOP"
    },
    {
      "name": "null1",
      "type": "nullTOP",
      "position": [300, 0],
      "inputs": ["noise1"]
    }
  ]
}
```

Note that `container1` omits `position` (defaults to `[0, 0]`) and `noise1` also omits `position`. Only `null1` at `[300, 0]` includes its position.

Nesting is recursive — COMPs inside COMPs can have their own `children`. The optional `max_depth` export parameter limits recursion depth (`null` means unlimited). COMPs may also contain an `annotations` array alongside `children` — see [Annotations](#annotations).

### Nested TDN-Externalized COMPs

When a parent TDN file contains `children` for a child COMP that has its **own** TDN externalization entry in the externalizations table, the child's `children` array is **skipped during import**. The child COMP shell is still created (its operator definition — name, type, position, parameters — is applied), but its internal network is **not** populated from the parent's snapshot. The child's own `.tdn` file is the source of truth for its contents.

This prevents a common problem: if a child COMP is updated and re-exported to its own `.tdn` file, but the parent is not re-exported, importing the parent would silently overwrite the child with stale data. By skipping nested TDN children, each `.tdn` file owns exactly one level of the hierarchy.

**What this means in practice:**

- **Export** is unchanged — parent TDN files still include the full recursive hierarchy in their `children` arrays. This keeps the file self-contained and useful as a portable snapshot.
- **Import** detects child COMPs with their own TDN entries and skips their children. A log message is emitted for each skipped child (e.g., `Skipping children of /project/parent/child — has its own TDN externalization`).
- **Reconstruction on project open** imports parents before children (sorted by path depth). Combined with the skip logic, this means each COMP's network is populated exactly once, from its own authoritative `.tdn` file.

If a child COMP is removed from the externalizations table (no longer tagged for TDN), its `children` array in the parent TDN will be imported normally — no special handling needed.

### COMP References (`tdn_ref`)

*Added in TDN v1.2.*

When a parent COMP is exported and a child COMP has its own TDN externalization, the parent's operator definition for that child includes a `tdn_ref` field instead of a `children` array:

```json
{
  "name": "audio_mixer",
  "type": "baseCOMP",
  "tdn_ref": "Embody/project1/audio_mixer.tdn",
  "position": [600, 0]
}
```

| Field | Type | Description |
|---|---|---|
| `tdn_ref` | `string` | Relative file path from the externalization folder to the child's `.tdn` file. Includes the COMP name in the path for cross-validation. |

**Mutually exclusive with `children`**: When `tdn_ref` is present, the operator definition does not contain a `children` array. The COMP's internal network is defined entirely in the referenced file.

**Resolution**: On import, the importer creates the COMP shell (name, type, position, parameters, flags) but does not populate its children. The referenced `.tdn` file is imported separately during reconstruction (sorted by path depth — parents before children).

**Cross-validation**: The `tdn_ref` value is checked against two independent sources:

1. **Externalizations table**: The child COMP's path must have an entry with `strategy='tdn'` and a matching `rel_file_path`.
2. **Disk**: The referenced `.tdn` file must exist at the resolved absolute path.

Mismatches produce warnings, not errors — the COMP shell is always created regardless. This ensures graceful degradation when files are moved or the table is out of sync.

**Backward compatibility**:

- Files **without** `tdn_ref` (TDN v1.1 and earlier) continue to work. The existing `_stripNestedTDNChildren` mechanism handles them via the externalizations table.
- Files **with** `tdn_ref` imported by an older Embody that doesn't recognize the field will silently ignore it. The `_stripNestedTDNChildren` path handles the nested COMP correctly as a fallback.
- The `embed_all=True` export option suppresses `tdn_ref` and inlines all children, producing a fully self-contained file regardless of child externalization status.

### TOX References (`tox_ref`)

*Added in TDN v1.4.*

The same ownership principle applies when a child COMP is externalized as `.tox` instead of `.tdn`. The parent's operator definition for that child includes a `tox_ref` field instead of a `children` array:

```json
{
  "name": "wave_speed",
  "type": "sliderCOMP",
  "tags": ["tox"],
  "tox_ref": "Embody/project1/wave_speed.tox",
  "position": [475, 425]
}
```

| Field | Type | Description |
|---|---|---|
| `tox_ref` | `string` | Relative file path from the externalization folder to the child's `.tox` file. |

**Mutually exclusive with `children`**: When `tox_ref` is present, the operator definition does not contain a `children` array. The COMP's internal network is defined entirely in the referenced `.tox` file. This prevents the parent `.tdn` from duplicating the contents of the child `.tox`, which would otherwise pollute `type_defaults` with the child's internal operator types and bloat the parent file.

**Resolution**: On import, the importer creates the COMP shell (name, type, position, parameters, flags) but does not populate its children. `externaltox` is **not** present in the parent `.tdn`'s parameter dict (it's an Embody-managed parameter, excluded from TDN export). Instead, the importer stores the `tox_ref` path on the new shell as `_pending_tox_restore` storage, then a post-import phase (`_restoreTOXShells`) sets `externaltox` from that marker and calls `_reloadTox` to load the `.tox` content immediately. This means the `.tox` content is fully restored after import — both for runtime imports (e.g. `import_network` via MCP) and for project-open reconstruction. `RestoreTOXComps` (frame 45) still handles the case where the parent `.tdn` is not re-imported and the table is the only source.

**TOX vs TDN, when to use which**:

- **TOX**: opaque binary encapsulation. The `.tox` is a single self-contained file, fast to load, but not git-diffable. Suitable for palette widgets, third-party COMPs, and anything where you don't need text-level review of internals.
- **TDN**: text/JSON snapshot of the network. Fully git-diffable. Use this when you want pull requests to show changes to the COMP's internals.

Both strategies receive the same ownership treatment in parent `.tdn` files — neither embeds children into the parent. The strategy choice is about the **child file format**, not whether the parent embeds.

**Backward compatibility**:

- Pre-v1.4 `.tdn` files that embedded TOX children's contents still import correctly: on import, `_stripNestedTOXChildren` consults the externalizations table and clears any embedded `children` for TOX-tagged paths. The COMP shell is created and `RestoreTOXComps` loads from the `.tox` file.
- Files **with** `tox_ref` imported by an older Embody that doesn't recognize the field will silently ignore it. The strip path handles the nested COMP correctly as a fallback.
- The `embed_all=True` export option suppresses `tox_ref` and inlines all children.

### Palette Clones

COMPs that originate from the TouchDesigner palette (e.g. `abletonLink`, Widget components, anything under `Samples/Palette/`) are detected and marked with `"palette_clone": true`. Their children are **not** exported because TouchDesigner automatically recreates them from the palette source when the project loads.

**Parameter handling for palette clones**: During export, parameters are compared against two baselines — the built-in default (`p.default`) and the clone source's actual value. If a parameter matches `p.default` but differs from the clone source, it is still exported. This prevents user-set values from being silently dropped when they happen to match the built-in default but not the clone source (e.g., a `buttontype` whose `p.default` is `"momentary"` but whose clone source is `"toggledown"`). The `clone` and `enablecloning` parameters are always excluded — TD auto-sets these during rebuild.

#### Palette Detection

Detection uses two strategies:

1. **Palette catalog** (primary): Embody ships a catalog at `embody/Embody/palette_catalog.tsv` built by scanning every `.tox` in TD's installed palette directory. The catalog records each component's `name`, `OPType`, and `min_children` count (264 entries for TD 099.2025.32280). A COMP is detected as a palette if its name matches a catalog entry, its `OPType` matches, and it has at least `min_children // 2` children (a floor that tolerates user modifications while rejecting empty user COMPs that happen to share a palette name).
2. **Clone expression heuristic** (fallback): if the `clone` parameter points to `/sys/` or references `TDBasicWidgets`, `TDResources`, or `TDTox`, the COMP is detected as a palette. Catches cases where the catalog doesn't cover the current TD build. **Exception**: paths and expressions under `/sys/TDTox/defaultCOMPs/` are explicitly excluded. That directory holds TD's native-operator templates — every freshly-created `buttonCOMP`, `panelCOMP`, etc. clones from there by default, and those are stock types, not palette components. Export them as regular COMPs.

The catalog is loaded into memory by `CatalogManagerExt.EnsureCatalogs()` at startup from the shipped TSV (skipping a runtime scan) or from `.embody/catalog_<build>.json` if already cached locally.

#### Palette Handling

When the export path encounters a detected palette COMP, the `Tdnpalettehandling` parameter on Embody's TDN page decides what to do:

| Value | Behavior |
|---|---|
| `Ask` (default) | On first encounter of each palette COMP, prompts with four buttons: **Black Box** (this COMP), **Full Export** (this COMP), **Black Box for All** (flips the project-wide par), **Full Export for All** (flips the project-wide par). The per-COMP decision is persisted via `comp.store('_tdn_palette_handling', 'blackbox'|'fullexport')` so subsequent exports don't re-prompt. |
| `Black Box` | Always emit `"palette_clone": true` with parameter overrides only. Children are re-dropped from the palette on import. Correct for stock palette COMPs; lets upstream palette updates from Derivative flow through on round-trip. |
| `Full Export` | Always export all internal children as if the COMP were a regular user COMP. Use when you've heavily customized the palette internals and need that state preserved across round-trip. |

Per-COMP stored decisions take precedence over the project-wide par. To reset a COMP's stored decision, call `comp.unstore('_tdn_palette_handling')`.

---

## Annotations

Annotations are visual documentation elements in TouchDesigner networks (comments, network boxes, and annotate panels). They are stored in an `annotations` array at the top level (for root-level annotations) and optionally on each COMP operator (for nested annotations).

```json
{
  "operators": [ ... ],
  "annotations": [
    {
      "name": "annot_core",
      "mode": "annotate",
      "title": "Core Tests",
      "text": "Unit tests for core functionality",
      "position": [-70, -300],
      "size": [1070, 660],
      "color": [0.5, 0.5, 0.5],
      "opacity": 0.8
    }
  ]
}
```

For nested COMPs, `annotations` appears alongside `children`:

```json
{
  "name": "container1",
  "type": "baseCOMP",
  "children": [ ... ],
  "annotations": [
    {
      "name": "annot1",
      "mode": "comment",
      "text": "Signal processing chain"
    }
  ]
}
```

### Annotation Object

| Field | Type | Required | Condition for inclusion |
|-------|------|----------|------------------------|
| `name` | string | Yes | Always included. The annotation operator's name. |
| `mode` | string | Yes | Always included. One of `"annotate"`, `"comment"`, or `"networkbox"`. |
| `title` | string | No | Only if non-empty. Title bar text (for `annotate` and `networkbox` modes). |
| `text` | string | No | Only if non-empty. Body text content. |
| `position` | `[x, y]` | No | Omitted when `[0, 0]` (default). |
| `size` | `[width, height]` | Yes | Always included — annotations have no standard default size. |
| `color` | `[r, g, b]` | No | Only if different from the default gray `[0.545, 0.545, 0.545]`. Background color. |
| `opacity` | number | No | Only if different from `1.0`. Background opacity (0.0 to 1.0). |

### Import Behavior

Annotations are created during Phase 7a (after operator positions, before file link restoration). Each annotation is created as an `annotateCOMP` with `utility=True` (matching TD's native behavior — annotations are utility operators hidden from `.children`).

---

## Value Serialization

All parameter and content values are converted to JSON-safe types using these rules, applied in order:

| Python Type | JSON Output | Rule |
|-------------|-------------|------|
| `None` | string | Converted to empty string `""`. |
| `bool` | boolean | Stored as-is (`true`/`false`). |
| `int` | number | Stored as-is. |
| `float` | number (int) | If the value is a whole number (and fits in 53-bit integer range), it is converted to an integer. E.g., `1.0` becomes `1`. |
| `float` | number (float) | Rounded to 10 decimal places to eliminate floating-point noise. |
| `str` | string | Stored as-is (with `=`/`~` [escaping](#escaping) applied for parameter values). |
| `list` / `tuple` | array | Each element is recursively serialized. |
| Any other type | string | Converted via `str()`. |

**Color values** (`color` field on operators) are rounded to 4 decimal places.

---

## System Exclusions

The following top-level paths and all their descendants are always excluded from export. These contain TouchDesigner system internals that should not be version-controlled:

| Path | Contents |
|------|----------|
| `/local` | Local parameters |
| `/sys` | System operators (Thread Manager, TDJSON, etc.) |
| `/perform` | Performance monitoring |
| `/ui` | UI framework operators |

An operator is excluded if its path equals one of these or starts with one followed by `/` (e.g., `/sys/TDResources` is excluded).

---

## Import Process

Importing a `.tdn` file reconstructs the network in a pre-phase plus eight sequential phases. This ordering ensures that dependencies are satisfied — for example, operators must exist before they can be connected, and positions are set last because creating operators may shift existing nodes.

| Phase | Action | Details |
|-------|--------|---------|
| Pre | **Resolve templates and defaults** | If `par_templates` is present, `$t` references in `custom_pars` are expanded to full definitions with value overrides. If `type_defaults` is present, shared properties are merged into each operator (`parameters` via dict merge, `flags`/`size`/`color`/`tags` via whole-value injection; operator-specific values take precedence). |
| 1 | **Create operators** | All operators are created depth-first. COMPs are created first so their children can be placed inside them. |
| 2 | **Create custom parameters** | Custom parameter definitions are created on COMPs (pages, types, ranges, menu entries, defaults). |
| 3 | **Set parameter values** | Both built-in and custom parameter values are applied. `=` prefix sets expression mode, `~` prefix sets bind mode, all other values set constant mode. |
| 4 | **Set flags** | Operator flags are applied. Array entries without `-` prefix set the flag to `true`; entries with `-` prefix set to `false`. |
| 5 | **Wire connections** | Operator and COMP connections are established. Source references are resolved (sibling name first, then full path). Array position equals input index. |
| 6 | **Set DAT content** | Text or table data is loaded into DAT operators. |
| 6a | **Restore storage** | Storage key-value pairs are restored via `op.store()`. `$type` wrappers are deserialized to Python types (tuple, set, bytes). |
| 7 | **Set positions** | Node positions, sizes, colors, and comments are applied last. Missing position defaults to `[0, 0]`. |
| 7a | **Create annotations** | Annotations are created from the `annotations` array (top-level and per-COMP). Each annotation is created as an `annotateCOMP` with `utility=True`, then its mode, title, body text, position, size, color, and opacity are set. |

The importer accepts either a full `.tdn` document (with metadata) or just the `operators` array directly.

### Extension Initialization Timing

!!! danger "Extensions initialize BEFORE TDN import"
    When a TDN COMP is reconstructed (on project open or after save), the COMP shell is created first and any extensions on it initialize immediately. The TDN import runs **after** extension initialization, calling `ImportNetwork` with `clear_first=True` — which deletes all children and recreates them from the `.tdn` file. This means any state set up by `onInitTD` inside the COMP is **overwritten**.

**Timeline on project open:**

| Step | Frame | What happens |
|------|-------|--------------|
| 1 | Early | COMP shell created (exists but empty) |
| 2 | Early | Extension `__init__` runs |
| 3 | End of frame | `onInitTD` fires — network may not exist yet |
| 4 | Frame 60 | `ReconstructTDNComps` runs `ImportNetwork(clear_first=True)` |
| 5 | Frame 60+ | All children deleted and recreated from `.tdn` |

**Timeline on save (strip/restore cycle):**

| Step | What happens |
|------|--------------|
| 1 | Pre-save: children stripped from TDN COMPs |
| 2 | `.toe` saved without TDN children |
| 3 | Post-save: `ImportNetwork` re-imports children from `.tdn` |
| 4 | Extensions may reinitialize during restore |

**Impact:** If an extension's `onInitTD` creates operators, sets parameter values, writes to storage, or builds any state inside the COMP, that work is destroyed by the import. This affects extensions that live inside TDN COMPs **and** extensions whose ownerComp is a TDN-strategy COMP.

**Solution:** Defer initialization using `run()` with `delayFrames`:

```python
def onInitTD(self):
    run('args[0].postInit()', self, delayFrames=5)

def postInit(self):
    """Runs after TDN import completes. Safe to set up state."""
    pass
```

The deferred method must be **idempotent** — it will run on every project open, after every save, and on manual reimport. Use a delay of at least 5 frames to ensure all import phases have completed.

For full guidance on writing extensions that coexist with TDN, see the [Extensions](../td-development/extensions.md#initialization-and-tdn-import-timing) documentation.

### Version Compatibility

When importing a full `.tdn` document, the importer checks the metadata fields for compatibility:

- **`version`**: Compared against the current TDN format version. A warning is logged if they differ, indicating the file may use a newer or older schema.
- **`td_build`**: Compared against the running TouchDesigner version. An informational message is logged if they differ, since operator types and parameter defaults may vary between TD builds.
- **`build`**: Logged for informational purposes, identifying which save iteration is being imported.

These checks are non-blocking — the import always proceeds regardless of mismatches.

---

## Round-Trip Guarantees

For most networks, export → import → re-export produces identical `.tdn` output. The format is designed to be stable across round-trips, with a few documented exceptions.

### Preserved

- **Target COMP metadata** (v1.1+): type, flags, color, tags, comment, storage
- Operator names, types, and hierarchy
- Non-default parameter values (constant, expression, and bind modes)
- Custom parameter definitions (all fields, all styles)
- Flags, connections, positions, sizes, colors, comments, tags
- Operator storage (serializable entries only — see [Operator Storage](#operator-storage))
- Annotations (mode, title, body text, position, size, color, opacity)
- DAT text and table content (byte-for-byte when `include_dat_content` is `true`)
- Float values (stable after the first export — see below)
- Type defaults and parameter templates (re-computed on each export)

### Known Exceptions

**Palette clones** — On first export, a palette-cloned COMP is marked `"palette_clone": true` and its children are skipped. After import, TouchDesigner materializes the children from the clone source. A subsequent re-export will include those children as regular operators. This means the second export is larger than the first. Parameters that match `p.default` but differ from the clone source are preserved (see [Palette Clones](#palette-clones)).

**Color tolerance** — Colors within `0.01` per channel of the default gray `[0.545, 0.545, 0.545]` are treated as default and not exported. A color of `[0.55, 0.55, 0.55]` survives; `[0.546, 0.546, 0.546]` is dropped.

**Float precision** — Values are rounded to 10 decimal places on first export. This can change the last digits of very precise values (e.g., `3.14159265358979` → `3.1415926536`). After that first rounding, subsequent exports are stable.

**Type defaults recomputation** — Type defaults and parameter templates are recomputed from scratch on each export. If operator populations change between exports (operators added/removed), different properties may qualify as "unanimous" for type_defaults, and different pages may qualify as templates. The final network state is always identical, but the JSON structure may differ.

**Locked non-DAT data** — When a TOP, CHOP, or SOP is locked, TDN preserves the lock flag but not the frozen pixel, channel, or geometry data. After import, the operator is locked but empty. See [Lock Flag Limitation](#lock-flag-limitation).

### Intentionally Excluded

The following are never exported and are not considered a loss:

- **Export-mode parameters** — set by the exporting operator, not the parameter itself
- **Pulse / Momentary / Header styles** — no persistent state
- **Read-only parameters** — cannot be set on import
- **COMP externalization parameters** (`externaltox`, `enableexternaltox`, `reloadtox`) — COMP `.tox` externalization is managed separately by Embody
- **Transient storage keys** — runtime state used by Embody (`envoy_running`, `_git_root`, etc.)
- **Non-serializable storage values** — threading objects, operator references, custom class instances

---

## Error Handling

TDN import is **best-effort** — individual failures should not abort the entire operation. This section describes the expected behavior for developers working with TDN files.

### Unknown Fields

Developers should ignore unknown fields when parsing TDN documents. This ensures forward compatibility — a file exported by a newer version of Embody can still be imported by an older version, with unrecognized fields silently skipped.

### Failure Modes

| Situation | Expected behavior |
|-----------|-------------------|
| Unknown field in any object | Ignore it. |
| Missing required field (`name`, `type`) on an operator | Skip that operator, log an error. |
| Missing connection source (operator not found) | Skip that connection, log a warning. |
| Unrecognized custom parameter `style` | Skip that parameter definition, log a warning. |
| Unrecognized flag name | Ignore it. |
| Invalid parameter value type | Attempt type coercion; if impossible, skip with a warning. |
| Version mismatch (`version`, `td_build`) | Log a warning, proceed with import. |
| Target COMP type mismatch (`type` vs destination) | Log a warning, proceed with import. The file's `type` field is informational — import does not change the destination COMP's type. |
| Unknown `$t` template reference | Log a warning, skip that page. |
| Missing `type_defaults` entry for a type | No-op (operator uses its own properties). |
| Non-serializable storage value on export | Skip that value, log at DEBUG level. |
| Unknown `$type` in storage on import | Treat as plain dict, log a warning. |
| Failed `store()` call on import | Skip that key, log a warning. |

### General Principle

Log warnings for anything skipped so the developer can inspect the result. Never abort an entire import because a single operator, parameter, or connection failed — the partial result is more useful than no result.

---

## Complete Example

A realistic `.tdn` file demonstrating all major features:

```json
{
  "format": "tdn",
  "version": "1.3",
  "build": 3,
  "generator": "Embody/5.0.237",
  "td_build": "2025.32050",
  "exported_at": "2026-02-19T14:30:00Z",
  "network_path": "/",
  "type": "baseCOMP",
  "options": {
    "include_dat_content": true,
    "include_storage": true
  },
  "type_defaults": {
    "baseCOMP": {
      "parameters": {
        "resizecomp": "=me",
        "repocomp": "=me"
      }
    }
  },
  "par_templates": {
    "about": [
      {"name": "Build", "style": "Int", "label": "Build Number", "readOnly": true},
      {"name": "Version", "style": "Str", "label": "Version", "readOnly": true}
    ]
  },
  "operators": [
    {
      "name": "controller",
      "type": "baseCOMP",
      "color": [0.2, 0.4, 0.8],
      "comment": "Main controller",
      "tags": ["core"],
      "custom_pars": {
        "Controls": [
          {
            "name": "Speed",
            "style": "Float",
            "default": 1,
            "max": 10,
            "clampMin": true,
            "normMax": 5,
            "value": 2.5
          },
          {
            "name": "Mode",
            "style": "Menu",
            "menuNames": ["linear", "ease", "bounce"],
            "menuLabels": ["Linear", "Ease In/Out", "Bounce"],
            "value": 1
          },
          {
            "name": "Color",
            "style": "RGB",
            "clampMin": true,
            "clampMax": true,
            "values": [1, 0.5, 0]
          }
        ],
        "About": {
          "$t": "about",
          "Build": 3,
          "Version": "1.0.0"
        }
      },
      "flags": ["viewer"],
      "comp_inputs": ["renderer"],
      "children": [
        {
          "name": "noise1",
          "type": "noiseTOP",
          "parameters": {
            "type": "sparse",
            "amp": 0.8,
            "period": 2,
            "monochrome": true,
            "resolutionw": 1920,
            "resolutionh": 1080
          }
        },
        {
          "name": "level1",
          "type": "levelTOP",
          "position": [300, 0],
          "parameters": {
            "opacity": "=parent().par.Speed / 10"
          },
          "inputs": ["noise1"],
          "flags": ["display"]
        },
        {
          "name": "config",
          "type": "tableDAT",
          "position": [0, -200],
          "dat_content": [
            ["key", "value"],
            ["resolution", "1920x1080"],
            ["fps", "60"]
          ],
          "dat_content_format": "table",
          "flags": ["lock"]
        },
        {
          "name": "script1",
          "type": "textDAT",
          "position": [300, -200],
          "dat_content": "# Initialize\nprint('Controller ready')",
          "dat_content_format": "text"
        }
      ]
    },
    {
      "name": "renderer",
      "type": "baseCOMP",
      "position": [500, 0],
      "size": [300, 150],
      "custom_pars": {
        "About": {
          "$t": "about",
          "Build": 1,
          "Version": "0.9.0"
        }
      }
    }
  ]
}
```

Key observations:

- **`type_defaults`**: Both `baseCOMP`s share `resizecomp` and `repocomp` expressions, so those are hoisted out of individual operators
- **`par_templates`**: The "About" page definition is shared between `controller` and `renderer`, with different values
- **Expression shorthand**: `"=parent().par.Speed / 10"` instead of `{"expr": "..."}`
- **Flags as arrays**: `["viewer"]`, `["display"]`, `["lock"]`
- **Simplified connections**: `["noise1"]` instead of `[{"index": 0, "source": "noise1"}]`
- **Optional position**: `noise1` at `[0, 0]` omits `position`; `controller` at `[0, 0]` also omits it
- **Compact formatting**: Arrays like `[300, 0]`, `[0.2, 0.4, 0.8]`, `["core"]` are inline

---

## Changelog

| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2026-02-19 | Initial release with 8 format optimizations: expression shorthand (`=`/`~` prefixes), flags as arrays, page-grouped custom parameters, type defaults, parameter templates, optional position, simplified connections, compact JSON formatting. |
| 1.0 | 2026-02-22 | Extended `type_defaults` to support `flags`, `size`, `color`, and `tags` in addition to `parameters`. Backward-compatible: old importers ignore unknown keys, new importers handle files without the new keys. |
| 1.0 | 2026-03-01 | Added annotation support (`annotations` array at top level and per-COMP). Added Phase 7a to import process. Removed `file`/`syncfile` from SKIP_PARAMS so DAT file references are preserved in TDN exports. Pre-save now auto-exports current state before stripping TDN COMPs. |
| 1.3 | 2026-04-07 | Added built-in parameter sequence support (`sequences` key on operator objects). Operators with resizable parameter blocks (mathmixPOP, glslPOP, attributePOP, constantCHOP, etc.) now round-trip correctly. Added Phase 2.5 to import process. Sequence parameters excluded from `type_defaults` compression and `_buildParCache`. |
