agentleFS
dylanroscoverdylanroscover/Embody/.claude/skills/td-api-reference/SKILL.md

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.

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
  1. Very specific:

    Always verify a reference resolves from the calling context.

    line 59
  2. Very specific:

    Never call op(), parent() or touch TD objects at module level: module code runs at import, before the network is ready.

    line 69

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md
<!-- 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 wrong

Creating 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 / appendFile

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

RungPatternUse whenPrerequisite
1self.ownerCompAnywhere inside an extension; capture it once in __init__none
2parent.CompNameInside the COMP but outside the extension: callback DATs, parameter expressionsthe COMP sets par.parentshortcut
3op('sibling'), op('./child'), op('../up_one')The target is in the same network or nearbynone
3biop.Name / ipar.NameAn internal op or parameter the COMP publishes for its own descendantsInternal OP shortcuts on the Common page
4op.CompNameA project-wide singleton no parent shortcut can reachthe COMP sets par.opshortcut
  • parent.CompName searches UP by shortcut name and survives renesting; parent(2) encodes the depth and breaks the moment anything moves. Set par.parentshortcut when you create a reusable COMP.
  • The bare global parent() / parent(N) is banned inside an extension class body: use self.ownerComp. Exemptions: a COMP's own callback DAT (where parent() 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.Name names one internal operator, ipar.Name a parameter-holding COMP; inside-only, and an operator-valued internal par needs op(ipar.X.Opname). Reserve them for stable internal addresses.
  • op.CompName is globally unique: assigning a name in use steals it from the previous holder. Use it for singleton services (op.Embody.Log(...)), otherwise prefer parent.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), and run() 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 or maxDepth=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 json shadows Python's json: TD searches DATs by name before sys.path.
  • mod.utils.func() re-resolves the DAT every call; in a loop cache it (m = mod.utils). In a parameter expression import is 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 (use addScriptError() 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 subscribing

tdu.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] + 1 auto-casts, n[1,2].val + 1 raises. dat.text strips multi-line cells (dat.csv keeps them); dat.jsonObject parses directly; dat.write() APPENDS; dat.module imports 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), GLSL gl_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 against sample() on a vertical ramp). PIL/OpenCV images and panel screen coordinates are top-down, so np.flipud converts 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) and bounds(delayed=True) are non-blocking; pop.points('P') downloads from the GPU. fileinPOP (meshes) and pointfileinPOP (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 through fromOP or 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 DAT onFrameStart), never a for loop.
  • 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 framestart toggle 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:

  1. Prototype a fetch synchronously once to prove URL, auth and parse; never ship it.
  2. Fast, TD-only, no I/O: inline. Any network, disk or subprocess call is never this rung.
  3. Fetch data with a native operator: Web Client DAT (request(url, 'GET', timeout=8000) returns at once; onResponse runs on the MAIN thread), WebSocket DAT, Web Server DAT, OSC, File In / Folder DAT. Parse with a JSON DAT, bridge with DAT to CHOP.
  4. Long TD-touching work: chunk with run(delayFrames=N); run() controls WHEN, not HOW MUCH.
  5. Blocking pure-Python work: the Thread Manager (Palette Thread Manager Client preferred), a target with zero TD access, results applied in main-thread hooks. Never EnqueueTask() from a worker.
  6. Long-lived loop or server: ThreadManager standalone=True or a top-level threading.Thread with zero TD access draining a queue.Queue from 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.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.