td-api-reference
Skill213 starsChanged 2 months agoMITView on GitHub
MUST READ before writing TD Python via execute_python, set_dat_content, or edit_dat_content, and before any heavy build. Parameters, storage, operators, referencing, threading, cook model, heavy-build safety caps.
In this repository30 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- web/llms-full.txtllms.txt
- web/llms.txtllms.txt
- add-mcp-toolSkill
- agent-testsSkill
- briefSkill
- build-uiSkill
- collabSkill
- create-extensionSkill
- create-operatorSkill
- debug-operatorSkill
- externalize-operatorSkill
- glsl-shadersSkill
- manage-annotationsSkill
- mcp-tools-referenceSkill
- merge-divergent-toxSkill
- movie-exportSkill
- multi-instanceSkill
- multi-session-etiquetteSkill
- operator-gotchasSkill
- parameter-designSkill
- pop-networksSkill
- publish-specimenSkill
- releaseSkill
- run-testsSkill
- specimen-authoringSkill
- td-recoverySkill
- testingSkill
- visual-aestheticsSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 59
Always verify a reference resolves from the calling context.
- Very specific: line 69
Never call op(), parent() or touch TD objects at module level: module code runs at import, before the network is ready.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
<!-- Generated by Embody/Envoy - Do not remove this comment - sha:1517fd24f8397a92 -->TouchDesigner Python API Reference
Verify unfamiliar TD behavior before asserting it: get_docs('<Operator or Class>') reads the offline help mirror first, the wiki (https://docs.derivative.ca: OP Class, COMP Class, Par Class, Extensions, Cook) second, and describe_op_type gives an operator type's real parameter names. Assumptions about TD's Python API are frequently wrong. Two companions in this skill's folder hold the long tail: references/background-work.md (the Web Client DAT and Thread Manager code) and references/heavy-build-safety.md (crash causes and caps). Read them before any fetch, thread or heavy build; without a skills folder, get_guidance(topic='td-api-reference/background-work').
Parameter Access Patterns
value = op('geo1').par.tx.eval() # CORRECT: the current runtime value
value = op('geo1').par.tx.val # WRONG: constant-mode value only
op('geo1').par.tx = 5 # set; .val = 5 also silently switches the mode to CONSTANT
op('geo1').par.xord = 'trs' # menus take the name or the index
me.par.tx.eval().hex() # cast AFTER eval(); me.par.tx.hex() is wrongCreating Custom Parameters
All append* methods return a ParGroup (tuple-like), never a single Par: index with [0].
page = comp.appendCustomPage('Controls')
p = page.appendFloat('Speed', label='Speed')[0]
p.default = 0.5; p.val = p.default # .default never sets the value
p.normMin = 0; p.normMax = 2 # slider range
p.min = 0; p.clampMin = True # hard clamp
p.help = "Playback speed multiplier." # ALWAYS set help
p.startSection = True # separator above
# appendInt / appendToggle / appendStr / appendMenu (EMPTY menu: set .menuNames/.menuLabels)
# appendPulse / appendRGB ('Color' -> Colorr/g/b) / appendXYZ ('Pos' -> Pos1/2/3) / appendOP / appendFileNames start uppercase, then lowercase letters and digits, no underscores. Get-or-create (never a blind append*), callbacks, styles, ranges, help text: /parameter-design. Docs: https://docs.derivative.ca/Custom_Parameters
op() vs opex()
op('/missing') returns None silently; opex('/missing') raises tdError with a clear message. Use op() only when None is an acceptable result. ops('noise*') returns a list (wildcards). opex('level1').asType(levelTOP, checkType=True) is the typed, runtime-asserted one-liner for a known type.
Operator Referencing Patterns
Absolute paths (op('/project1/...')) are always wrong, in code, expressions AND parameter values. Take the highest rung that resolves from where the code runs:
| Rung | Pattern | Use when | Prerequisite |
| 1 | self.ownerComp | Anywhere inside an extension; capture it once in __init__ | none |
| 2 | parent.CompName | Inside the COMP but outside the extension: callback DATs, parameter expressions | the COMP sets par.parentshortcut |
| 3 | op('sibling'), op('./child'), op('../up_one') | The target is in the same network or nearby | none |
| 3b | iop.Name / ipar.Name | An internal op or parameter the COMP publishes for its own descendants | Internal OP shortcuts on the Common page |
| 4 | op.CompName | A project-wide singleton no parent shortcut can reach | the COMP sets par.opshortcut |
parent.CompNamesearches UP by shortcut name and survives renesting;parent(2)encodes the depth and breaks the moment anything moves. Setpar.parentshortcutwhen you create a reusable COMP.- The bare global
parent()/parent(N)is banned inside an extension class body: useself.ownerComp. Exemptions: a COMP's own callback DAT (whereparent()IS the owner), generic tooling inside a COMP it did not author, parameter expressions on replicated or cloned widgets.some_op.parent()is documented OP-class API and untouched. iop.Namenames one internal operator,ipar.Namea parameter-holding COMP; inside-only, and an operator-valued internal par needsop(ipar.X.Opname). Reserve them for stable internal addresses.op.CompNameis globally unique: assigning a name in use steals it from the previous holder. Use it for singleton services (op.Embody.Log(...)), otherwise preferparent.CompName.- Always verify a reference resolves from the calling context; a reference that returns
None(or finds the wrong operator) is a latent bug. - After ANY rename (operator, method or parameter) sweep every live surface, not just DAT text on disk: DAT text inside the
.toe(findChildren(type=DAT)and read.text),par.expr,par.bindExpr, constant-mode OP-reference values (par.val), andrun()strings composed at call time (issue #94: an agent swept DAT content only and left stale expressions and binds behind).findChildren(depth=N)matches EXACTLY depth N, a silent all-clear for anything shallower; walk with no depth argument ormaxDepth=N.
Type Hints
Annotate every def (arguments AND return) and let a checker read them; Python ignores them at runtime. TD's classes are the types (COMP, DAT, OP, Par, constantCHOP, ...); put from __future__ import annotations at the top of a module DAT. x: str = None declares a str that is None: write Optional[str] = None. A callback returns -> None; a query that can miss returns -> Optional[COMP]. The hint is the soft check; asType(..., checkType=True) is the runtime assertion. Check softly, in CI, with a baseline (pyright basic, failing only on NEW errors).
Everyday Essentials
debug('value is', x)prints with the source DAT and line number;print()does not.- Never call
op(),parent()or touch TD objects at module level: module code runs at import, before the network is ready. Defer to methods. - A DAT named
jsonshadows Python'sjson: TD searches DATs by name beforesys.path. mod.utils.func()re-resolves the DAT every call; in a loop cache it (m = mod.utils). In a parameter expressionimportis unavailable,mod.utils.f()works.mod('utils')resolves relative to the current op, never an absolute path.- Expressions that read a promoted attribute guard with
parent().X if parent().extensionsReady else 0. changeType()returns the NEW op (capture it);copyOPs([list])preserves wires,COMP.copy()does not;addError()/addWarning()work only in cook callbacks (useaddScriptError()from extension code).
onInitTD and TDXN Import Timing
Inside a TDXN-strategy COMP, onInitTD fires BEFORE the import that rebuilds the children (project open around frame 60, every Ctrl+S strip/restore, every manual reimport with clear_first=True), so anything it creates, sets or stores inside the COMP is wiped. Defer network-dependent setup with run('args[0].postInit()', self, delayFrames=5), keep it idempotent (it runs several times), null-check children (op('child') is None during the strip window), and keep durable state OUTSIDE the boundary or in store() on the COMP.
Parameter, Storage, or Dependency?
User-facing state is a custom parameter; hidden durable bookkeeping is storage (lives on the COMP, survives an extension reinit, wiped when the COMP's contents are replaced by a tox reload or TDXN reconstruction); a derived value that must recook its readers is a tdu.Dependency (dies with the extension instance on every source save, rebuilt in __init__). Publish once and let readers pull; never push values into consumers. The lifetime table, what actually notifies dependents, and the storage mechanics are measured in /parameter-design and its references/state-lifetimes.md.
op('base1').store('count', 42) # write THROUGH store(); comp.storage[k] = v notifies nobody
val = op('base1').fetch('count', 0) # no default -> a miss RAISES; fetch() searches UP the parents
op('base1').unstore('count') # glob-matched: never a computed key
op('base1').storeStartupValue('version', 1) # a separate dict restored on load
dep = tdu.Dependency(0)
dep.val = 5 # CORRECT: recooks readers. dep = 5 destroys the Dependency
dep.val.append(4); dep.modified() # in-place mutation needs modified()
current = dep.peekVal # read without subscribingtdu.clamp, tdu.remap, tdu.rand(seed), tdu.base('noise3') -> 'noise', tdu.digits, tdu.validName, tdu.match, tdu.expand('A[1-3]'), tdu.tryExcept(expr, fallback).
DAT, CHOP and TOP Access
- DAT cells are strings;
n[1,2] + 1auto-casts,n[1,2].val + 1raises.dat.textstrips multi-line cells (dat.csvkeeps them);dat.jsonObjectparses directly;dat.write()APPENDS;dat.moduleimports it. - CHOP:
op('noise1')['chan1']by name (no wildcard),.chans('tx*')for patterns,ch.eval(),ch[10],ch.evalFrame(30),numpyArray()shaped(numChans, numSamples). - TOP coordinates:
TOP.sample(x, y), GLSLgl_FragCoord, UVs and Crop/Transform parameters put (0, 0) at the BOTTOM-LEFT with Y up.TOP.numpyArray()follows the same y-up order: row 0 is y = 0, the bottom of the image (shape[height, width, channels]; measured 2025.33230 againstsample()on a vertical ramp). PIL/OpenCV images and panel screen coordinates are top-down, sonp.flipudconverts between the two.sample()downloads the whole texture: never in a loop.numpyArray()returns linear-light floats, not the sRGB bytes a file decode gives: compare same-domain only. - POPs:
pop.numPoints(delayed=True)andbounds(delayed=True)are non-blocking;pop.points('P')downloads from the GPU.fileinPOP(meshes) andpointfileinPOP(point clouds, splats) are distinct operators. run("me.cook(force=True)", fromOP=op('base1'), delayFrames=1),run(fn, arg, delayMilliSeconds=500),run("...", endFrame=True); resolve ops throughfromOPor shortcuts, never absolute paths.
Cook Model Gotchas
cook(force=True)does not advance a feedback loop within a frame: evolution needs real frames with the chain demanded (run(..., delayFrames=1)or an Execute DATonFrameStart), never aforloop.- A Movie File In reload (
par.file,reloadpulse) lands only across a real frame advance, and ops downstream in the same forced pass can still serve the pre-reload texture. Verify content at the point of capture and let each reload settle (/movie-export). - A CHOP Execute DAT is a per-frame demand source: its watched CHOP cooks every frame (measured 2025.33230). An Execute DAT's
framestarttoggle is OFF by default, so a frame driver that was never enabled demands nothing. - Animate cheaply: make a heavy generator static (no time reference, cooks once) and put the motion in a cheap downstream op that animates the SAMPLING. Confirm with
cookedThisFrame.
Background and Long-Running Work
Ironclad rule: a worker thread touches NO TD object, and a read counts as a write. No op()/opex(), no Par (not even .eval()), no DAT/CHOP/SOP/TOP content, no storage, no tdu.Dependency, no debug()/print(), and never run()/td.run() from a worker: it does not raise, the scheduled code even runs, and the crash lands later far from the call site (Derivative-confirmed 2026-08-17). A worker gets pure Python, tdu math utilities, values resolved on the main thread and passed in, queue/threading primitives, td.isMainThread(), and the Thread Manager's InfoQueue/SafeLogger; it hands plain data back for a main-thread callback to apply.
Match the rung to the problem; threading is the LAST resort:
- Prototype a fetch synchronously once to prove URL, auth and parse; never ship it.
- Fast, TD-only, no I/O: inline. Any network, disk or subprocess call is never this rung.
- Fetch data with a native operator: Web Client DAT (
request(url, 'GET', timeout=8000)returns at once;onResponseruns on the MAIN thread), WebSocket DAT, Web Server DAT, OSC, File In / Folder DAT. Parse with a JSON DAT, bridge with DAT to CHOP. - Long TD-touching work: chunk with
run(delayFrames=N);run()controls WHEN, not HOW MUCH. - Blocking pure-Python work: the Thread Manager (Palette Thread Manager Client preferred), a
targetwith zero TD access, results applied in main-thread hooks. NeverEnqueueTask()from a worker. - Long-lived loop or server: ThreadManager
standalone=Trueor a top-levelthreading.Threadwith zero TD access draining aqueue.Queuefrom a main-thread callback armed BEFORE the worker starts.
Trigger a fetch from a Pulse parameter or a Timer CHOP (cycle on, limit off), never a sleep loop, a self-rescheduling run() poller or an auto-fetch on open. Gate with primary evidence: fps and droppedFrames held (get_project_performance) AND the result actually arrived. requests has no default timeout and blocks the frame: timeout=(2, 8), and only ever inside a worker. Code patterns: references/background-work.md.
Heavy-Build Safety
Load references/heavy-build-safety.md (crash causes with their warning metrics and mitigations) before render chains, feedback loops, instancing, large geometry, GLSL, high-resolution TOPs or many operators at once; the gating protocol and stop conditions in performance.md always apply. The caps in one breath: TOPs <= 1920x1080 in 8/16-bit fixed formats unless asked; feedback loops bounded (fixed resolution, decay below 1, reset wired, bypassed while wiring, terminated in a Null); heavy sources bypassed while wiring; GPU instancing or POPs over Copy SOP, counts ramped with metrics (CPU particles ~10k max); Time Slicing on and small buffers for CHOPs; constant-bounded GLSL loops with TD_NUM_* index guards; short non-blocking execute_python calls with no sleep, no TOP.sample() in loops, and large builds chunked across frames. allowCooking = False is COMP-only; gate other operators with bypass.
Pre-Installed Packages and Types
numpy and cv2 are documented as bundled; requests, yaml, cryptography, attrs are commonly present (verify in your build). Auto-imported stdlib: math, re, sys, collections, enum, inspect, traceback, warnings. TD parameters auto-cast in expressions but stay TD objects: convert with int(), float(), str() before handing them to standard Python; repr() reveals the real type.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

