hermes-agent/skills/creative/touchdesigner/references/python-api.md
kshitijk4poor 7a5371b20d feat: add TouchDesigner integration skill
New skill: creative/touchdesigner — control a running TouchDesigner
instance via REST API. Build real-time visual networks programmatically.

Architecture:
  Hermes Agent -> HTTP REST (curl) -> TD WebServer DAT -> TD Python env

Key features:
- Custom API handler (scripts/custom_api_handler.py) that creates a
  self-contained WebServer DAT + callback in TD. More reliable than the
  official mcp_webserver_base.tox which frequently fails module imports.
- Discovery-first workflow: never hardcode TD parameter names. Always
  probe the running instance first since names change across versions.
- Persistent setup: save the TD project once with the API handler baked
  in. TD auto-opens the last project on launch, so port 9981 is live
  with zero manual steps after first-time setup.
- Works via curl in execute_code (no MCP dependency required).
- Optional MCP server config for touchdesigner-mcp-server npm package.

Skill structure (2823 lines total):
  SKILL.md (209 lines) — setup, workflow, key rules, operator reference
  references/pitfalls.md (276 lines) — 24 hard-won lessons
  references/operators.md (239 lines) — all 6 operator families
  references/network-patterns.md (589 lines) — audio-reactive, generative,
    video processing, GLSL, instancing, live performance recipes
  references/mcp-tools.md (501 lines) — 13 MCP tool schemas
  references/python-api.md (443 lines) — TD Python scripting patterns
  references/troubleshooting.md (274 lines) — connection diagnostics
  scripts/custom_api_handler.py (140 lines) — REST API handler for TD
  scripts/setup.sh (152 lines) — prerequisite checker

Tested on TouchDesigner 099 Non-Commercial (macOS/darwin).
2026-04-18 17:43:42 -07:00

12 KiB

TouchDesigner Python API Reference

The td Module

TouchDesigner's Python environment auto-imports the td module. All TD-specific classes, functions, and constants live here. Scripts inside TD (Script DATs, CHOP/DAT Execute callbacks, Extensions) have full access.

When using the MCP execute_python_script tool, these globals are pre-loaded:

  • op — shortcut for td.op(), finds operators by path
  • ops — shortcut for td.ops(), finds multiple operators by pattern
  • me — the operator running the script (not meaningful via MCP — will be the WebServer DAT)
  • parent — shortcut for me.parent()
  • project — the root project component
  • td — the full td module

Finding Operators: op() and ops()

op(path) — Find a single operator

# Absolute path (always works from MCP)
node = op('/project1/noise1')

# Relative path (relative to current operator — only in Script DATs)
node = op('noise1')      # sibling
node = op('../noise1')   # parent's sibling

# Returns None if not found (does NOT raise)
node = op('/project1/nonexistent')  # None

ops(pattern) — Find multiple operators

# Glob patterns
nodes = ops('/project1/noise*')       # all nodes starting with "noise"
nodes = ops('/project1/*')            # all direct children
nodes = ops('/project1/container1/*') # all children of container1

# Returns a tuple of operators (may be empty)
for n in ops('/project1/*'):
    print(n.name, n.OPType)

Navigation from a node

node = op('/project1/noise1')

node.name        # 'noise1'
node.path        # '/project1/noise1'
node.OPType      # 'noiseTop'
node.type         # <class 'noiseTop'>
node.family       # 'TOP'

# Parent / children
node.parent()              # the parent COMP
node.parent().children     # all siblings + self
node.parent().findChildren(name='noise*')  # filtered

# Type checking
node.isTOP   # True
node.isCHOP  # False
node.isSOP   # False
node.isDAT   # False
node.isMAT   # False
node.isCOMP  # False

Parameters

Every operator has parameters accessed via the .par attribute.

Reading parameters

node = op('/project1/noise1')

# Direct access
node.par.seed.val        # current evaluated value (may be an expression result)
node.par.seed.eval()     # same as .val
node.par.seed.default    # default value
node.par.monochrome.val  # boolean parameters: True/False

# List all parameters
for p in node.pars():
    print(f"{p.name}: {p.val} (default: {p.default})")

# Filter by page (parameter group)
for p in node.pars('Noise'):  # page name
    print(f"{p.name}: {p.val}")

Setting parameters

# Direct value setting
node.par.seed.val = 42
node.par.monochrome.val = True
node.par.resolutionw.val = 1920
node.par.resolutionh.val = 1080

# String parameters
op('/project1/text1').par.text.val = 'Hello World'

# File paths
op('/project1/moviefilein1').par.file.val = '/path/to/video.mp4'

# Reference another operator (for "dat", "chop", "top" type parameters)
op('/project1/glsl1').par.dat.val = '/project1/shader_code'

Parameter expressions

# Python expressions that evaluate dynamically
node.par.seed.expr = "me.time.frame"
node.par.tx.expr = "math.sin(me.time.seconds * 2)"

# Reference another parameter
node.par.brightness1.expr = "op('/project1/constant1').par.value0.val"

# Export (one-way binding from CHOP to parameter)
# This makes the parameter follow a CHOP channel value
op('/project1/noise1').par.seed.val  # can also be driven by exports

Parameter types

Type Python Type Example
Float float node.par.brightness1.val = 0.5
Int int node.par.seed.val = 42
Toggle bool node.par.monochrome.val = True
String str node.par.text.val = 'hello'
Menu int (index) or str (label) node.par.type.val = 'sine'
File str (path) node.par.file.val = '/path/to/file'
OP reference str (path) node.par.dat.val = '/project1/text1'
Color separate r/g/b/a floats node.par.colorr.val = 1.0
XY/XYZ separate x/y/z floats node.par.tx.val = 0.5

Creating and Deleting Operators

# Create via parent component
parent = op('/project1')
new_node = parent.create(noiseTop)         # using class reference
new_node = parent.create(noiseTop, 'my_noise')  # with custom name

# The MCP create_td_node tool handles this automatically:
# create_td_node(parentPath="/project1", nodeType="noiseTop", nodeName="my_noise")

# Delete
node = op('/project1/my_noise')
node.destroy()

# Copy
original = op('/project1/noise1')
copy = parent.copy(original, name='noise1_copy')

Connections (Wiring Operators)

Output to Input connections

# Connect noise1's output to level1's input
op('/project1/noise1').outputConnectors[0].connect(op('/project1/level1'))

# Connect to specific input index (for multi-input operators like Composite)
op('/project1/noise1').outputConnectors[0].connect(op('/project1/composite1').inputConnectors[0])
op('/project1/text1').outputConnectors[0].connect(op('/project1/composite1').inputConnectors[1])

# Disconnect all outputs
op('/project1/noise1').outputConnectors[0].disconnect()

# Query connections
node = op('/project1/level1')
inputs = node.inputs          # list of connected input operators
outputs = node.outputs        # list of connected output operators

Connection patterns for common setups

# Linear chain: A -> B -> C -> D
ops_list = [op(f'/project1/{name}') for name in ['noise1', 'level1', 'blur1', 'null1']]
for i in range(len(ops_list) - 1):
    ops_list[i].outputConnectors[0].connect(ops_list[i+1])

# Fan-out: A -> B, A -> C, A -> D
source = op('/project1/noise1')
for target_name in ['level1', 'composite1', 'transform1']:
    source.outputConnectors[0].connect(op(f'/project1/{target_name}'))

# Merge: A + B + C -> Composite
comp = op('/project1/composite1')
for i, source_name in enumerate(['noise1', 'text1', 'ramp1']):
    op(f'/project1/{source_name}').outputConnectors[0].connect(comp.inputConnectors[i])

DAT Content Manipulation

Text DATs

dat = op('/project1/text1')

# Read
content = dat.text          # full text as string

# Write
dat.text = "new content"
dat.text = '''multi
line
content'''

# Append
dat.text += "\nnew line"

Table DATs

dat = op('/project1/table1')

# Read cell
val = dat[0, 0]         # row 0, col 0
val = dat[0, 'name']    # row 0, column named 'name'
val = dat['key', 1]     # row named 'key', col 1

# Write cell
dat[0, 0] = 'value'

# Read row/col
row = dat.row(0)         # list of Cell objects
col = dat.col('name')    # list of Cell objects

# Dimensions
rows = dat.numRows
cols = dat.numCols

# Append row
dat.appendRow(['col1_val', 'col2_val', 'col3_val'])

# Clear
dat.clear()

# Set entire table
dat.clear()
dat.appendRow(['name', 'value', 'type'])
dat.appendRow(['frequency', '440', 'float'])
dat.appendRow(['amplitude', '0.8', 'float'])

Time and Animation

# Global time
td.absTime.frame       # absolute frame number (never resets)
td.absTime.seconds     # absolute seconds

# Timeline time (affected by play/pause/loop)
me.time.frame          # current frame on timeline
me.time.seconds        # current seconds on timeline
me.time.rate           # FPS setting

# Timeline control (via execute_python_script)
project.play = True
project.play = False
project.frameRange = (1, 300)   # set timeline range

# Cook frame (when operator was last computed)
node.cookFrame
node.cookTime

Extensions (Custom Python Classes on Components)

Extensions add custom Python methods and attributes to COMPs.

# Create extension on a Base COMP
base = op('/project1/myBase')

# The extension class is defined in a Text DAT inside the COMP
# Typically named 'ExtClass' with the extension code:

extension_code = '''
class MyExtension:
    def __init__(self, ownerComp):
        self.ownerComp = ownerComp
        self.counter = 0

    def Reset(self):
        self.counter = 0

    def Increment(self):
        self.counter += 1
        return self.counter

    @property
    def Count(self):
        return self.counter
'''

# Write extension code to DAT inside the COMP
op('/project1/myBase/extClass').text = extension_code

# Configure the extension on the COMP
base.par.extension1 = 'extClass'  # name of the DAT
base.par.promoteextension1 = True  # promote methods to parent

# Call extension methods
base.Increment()       # calls MyExtension.Increment()
count = base.Count     # accesses MyExtension.Count property
base.Reset()

Useful Built-in Modules

tdu — TouchDesigner Utilities

import tdu

# Dependency tracking (reactive values)
dep = tdu.Dependency(initial_value)
dep.val = new_value   # triggers dependents to recook

# File path utilities
tdu.expandPath('$HOME/Desktop/output.mov')

# Math
tdu.clamp(value, min, max)
tdu.remap(value, from_min, from_max, to_min, to_max)

TDFunctions

from TDFunctions import *

# Commonly used utilities
clamp(value, low, high)
remap(value, inLow, inHigh, outLow, outHigh)
interp(value1, value2, t)  # linear interpolation

TDStoreTools — Persistent Storage

from TDStoreTools import StorageManager

# Store data that survives project reload
me.store('myKey', 'myValue')
val = me.fetch('myKey', default='fallback')

# Storage dict
me.storage['key'] = value

Common Patterns via execute_python_script

Build a complete chain

# Create a complete audio-reactive noise chain
parent = op('/project1')

# Create operators
audio_in = parent.create(audiofileinChop, 'audio_in')
spectrum = parent.create(audiospectrumChop, 'spectrum')
chop_to_top = parent.create(choptopTop, 'chop_to_top')
noise = parent.create(noiseTop, 'noise1')
level = parent.create(levelTop, 'level1')
null_out = parent.create(nullTop, 'out')

# Wire the chain
audio_in.outputConnectors[0].connect(spectrum)
spectrum.outputConnectors[0].connect(chop_to_top)
noise.outputConnectors[0].connect(level)
level.outputConnectors[0].connect(null_out)

# Set parameters
audio_in.par.file = '/path/to/music.wav'
audio_in.par.play = True
spectrum.par.size = 512
noise.par.type = 1  # Sparse
noise.par.monochrome = False
noise.par.resolutionw = 1920
noise.par.resolutionh = 1080
level.par.opacity = 0.8
level.par.gamma1 = 0.7

Query network state

# Get all TOPs in the project
tops = [c for c in op('/project1').findChildren(type=TOP)]
for t in tops:
    print(f"{t.path}: {t.OPType} {'ERROR' if t.errors() else 'OK'}")

# Find all operators with errors
def find_errors(parent_path='/project1'):
    parent = op(parent_path)
    errors = []
    for child in parent.findChildren(depth=-1):
        if child.errors():
            errors.append((child.path, child.errors()))
    return errors

result = find_errors()

Batch parameter changes

# Set parameters on multiple nodes at once
settings = {
    '/project1/noise1': {'seed': 42, 'monochrome': False, 'resolutionw': 1920},
    '/project1/level1': {'brightness1': 1.2, 'gamma1': 0.8},
    '/project1/blur1': {'sizex': 5, 'sizey': 5},
}

for path, params in settings.items():
    node = op(path)
    if node:
        for key, val in params.items():
            setattr(node.par, key, val)

Python Version and Packages

TouchDesigner bundles Python 3.11+ (as of TD 2024) with these pre-installed:

  • numpy — array operations, fast math
  • scipy — signal processing, FFT
  • OpenCV (cv2) — computer vision
  • PIL/Pillow — image processing
  • requests — HTTP client
  • json, re, os, sys — standard library

Custom packages can be installed to TD's Python site-packages directory. See TD documentation for the exact path per platform.