MUST READ before creating or moving any operator (create_op, copy_op, set_op_position, or execute_python builds). Required verification, canonical positioning recipe, and error-checking steps.
MUST READ before creating or moving any operator (create_op, copy_op, set_op_position, or execute_python builds). Required verification, canonical positioning recipe, and error-checking steps.
Create Operator Workflow
Follow these steps every time you create operators via MCP:
Choose the correct parent network -- ASSOCIATE with where the user already works. Inconsistent placement is the #1 scoping bug (/ one run, /project1 the next). The fix is to anchor on the user's own structure, which is stable across runs -- not on a transient pane:
NEVER create under /local or /local/* -- volatile storage, not saved with the .toe.
If the user named a target, use it.
Default home = the container that holds the Embody COMP.execute_python with result = op.Embody.parent().path. Embody in /project1 -> build in /project1; Embody at the root / -> build at /. It returns the SAME home every run -- this is the consistency mechanism, and it is the level the user chose by placing Embody there.
Override only for deliberate navigation. If the user has actively opened a specific content network to work in, build there instead (ui.panes.current.owner.path) -- UNLESS that pane is sitting at the bare root / (a non-deliberate default, e.g. right after the project opens), in which case ignore it and use the Embody home above. Never treat bare / as a content home.
Discover, never guess. Confirm the real container names with query_network on -- never hardcode (it may be renamed, and there may be more than one).
/
/project1
Keep one task's COMPs together under the SAME parent, grouped in one container / annotation. Don't split related COMPs across levels.
Discover the target network: query_network on the target parent to confirm it exists and see existing operators
Scan existing layout: Use get_network_layout on the parent COMP. Note each operator's nodeX, nodeY, nodeWidth, and nodeHeight — operators vary in size (100–300+ units wide)
Plan positions BEFORE creating: Batch-compute grid-aligned positions for ALL operators you intend to create. Signal flow is left-to-right: inputs on the left, outputs on the right. Supporting operators (DATs feeding a TOP, CHOPs feeding parameters) go to the left of or below the operator they feed. Snap all coordinates to the 200-unit grid. See the Positioning Rules section below.
Create each operator: create_op with the desired type and name
Position each operator: set_op_position to place it at the pre-computed grid position. Auto-placement is NOT acceptable — it produces messy, unreadable networks. You MUST explicitly position every operator you create. Moving a host via set_op_position carries its docked companions along (re-hugged below the new spot, reported as docks_moved), so position the host FIRST and any deliberately-placed dock after.
Docked companions (callback/shader/info DATs): create_op, copy_op, and set_op_position auto-hug every docked op in a tight row ~30 units below its host (docks_placed / docks_moved in the result) — do not re-plan grid slots for them. Ops created inside execute_python get NO such placement at create time (Envoy only auto-hugs badly scattered docks after the call, with a LAYOUT WARNING); if you create dock-spawning ops (GLSL TOP/MAT, execute DATs, OSC in/out) in a script, place their docks yourself per the Docked companion layout formula below.
Connect: connect_ops to wire inputs/outputs. Wires must flow left-to-right (positive X). If a wire would go backward, the downstream op is misplaced — reposition it.
Set OP-reference parameters with relative paths: If the operator has parameters referencing other operators (Camera, Geometry, Lights, TOP, CHOP, etc.), use sibling names (cam) or relative paths (../shared/lut) — NEVER absolute paths (/project1/scene/cam). See parameters.md § OP-Reference Parameter Values.
Verify layout: Call get_network_layout again. Confirm no overlaps, grid alignment is intact, signal flows left-to-right, and every entry carrying dockedTo sits in a tight row just below its named host (docked ops are the one exception to 200-grid spacing — they hug).
Verify errors: get_op_errors with recurse=true to check for errors and warnings. Fix all errors before considering the task complete
Visual verification: For any renderable result (a TOP chain or a render), capture the output TOP with capture_top and confirm it actually renders (not black) and matches intent; for a 3D render, confirm a camera, a light, and geometry display/render flags are present. For anything but a trivial op, load the /visual-aesthetics skill to judge composition/value/color/contrast.
Operator Type Preferences
Prefer POPs over SOPs for geometry and particle work. POPs (Point Operators) are GPU-accelerated and significantly more performant than SOPs (Surface operators). Only use SOPs when POP equivalents don't exist or when CPU-side geometry manipulation is specifically required.
Geometry COMP: delete the default torus
A freshly created geometryCOMP ships with a default torus1 SOP inside, render flag ON. The moment you add your OWN geometry to that COMP -- a SOP chain, a POP chain terminating in a nullPOP, or imported geometry -- delete torus1 (or turn OFF its render flag). It is the RENDER flag (purple) that selects a SOP for the Render TOP, so leaving it on draws BOTH: your geometry AND a phantom torus. It is easy to miss: adding your own SOP auto-clears the torus's DISPLAY flag (blue, exclusive per viewer) so the SOP / geometry viewer looks correct, while its RENDER flag (non-exclusive) stays on and keeps drawing it in the render output. Applies to EVERY geometryCOMP you populate, not just POP builds. (TDN import already strips these auto-created defaults, so this bites only live create_op builds -- which is why it shows up in some projects and not others.)
Positioning Rules (canonical layout recipe)
Every operator MUST be explicitly positioned on the 200-unit grid. Do NOT rely on auto-placement.
Spacing is size + gap, BOTH axes, ALWAYS -- never a fixed step. This is the #1 overlap bug: stepping by a constant (e.g. nodeY - 100, nodeX + 300) while the tiles are bigger than the step (a 134-tall node stepped by 100 overlaps by 34). The step MUST be computed from the ACTUAL node extent on that axis plus a gap, then snapped up to the grid:
Horizontal: next_x = prev_nodeX + prev_nodeWidth + gap (gap >= 200), rounded up to the next 200 multiple.
Vertical: next_y = prev_nodeY - (prev_nodeHeight + gap), rounded so the step clears the tile. For a stack of same-size tiles: step = ceil((maxNodeHeight + gap) / 200) * 200 and place tile i at y = -i * step. A 134-tall stack -> step = 200 (66 gap); a 260-tall stack -> step = 400. Bigger nodes => bigger spacing, mechanically.
The grid is the SNAP target, not the step size -- when a node is taller/wider than 200, the step is 400/600/... (the next multiple that clears size + gap), NEVER a flat 200. 400 units vertical minimum between parallel chains or annotation groups (widen when tiles are tall). Y increases upward; new rows go downward.
Use actual dimensions, not assumptions. COMPs (~160+ wide) and panel widgets (often 130-160 wide x 130+ tall) are much bigger than a bare TOP (~120 sq) -- a step that clears one overlaps the other.
Signal flow is left-to-right; wires must flow forward (positive X) -- never a backward "S" wire. Every operator must have a higher nodeX than each operator feeding it. The classic mistake: placing a source at the SAME nodeX as its destination (directly above or below) -- the source's output then sits right of the dest's input and the wire loops back. Place each source so its right edge (nodeX + nodeWidth) is left of the destination's nodeX. If a wire bends backward, move the downstream op right or the source left.
Branches split vertically, each continuing left-to-right; minimize crossings. Same row = same stage in a chain (same X); same column = same function across parallel chains (same Y).
New operators extend a group to the RIGHT (rightmost_nodeX + rightmost_nodeWidth + 200, snapped); a parallel chain goes DOWN (lowest Y in group - 400). Don't fill a gap mid-row of a finished chain.
Batch-compute ALL positions before placing anything; after placing, get_network_layout to verify.
Related operators stay close
Operators that reference each other belong near each other, even with no wire or dock: a MAT beside the Geometry COMP it shades; a camera and light(s) near their Render TOP; a CHOP/DAT that drives parameters by reference near the op it drives; any COMP named in an op() call, expression, or material slot near the op that references it. Rule of thumb: if op A names op B, a reader should see both without scrolling.
Panel-COMP widgets stack too
Buttons/fields/containers inside an align-driven panel (Container/Button laid out verttb/horizlr) render by their align + order pars, so their nodeX/nodeY are cosmetic to the UI -- BUT they still get network tiles, and a verttb container full of 134-tall buttons stepped by 100 overlaps in the editor exactly like data-flow ops do. Apply the same size + gap stepping to panel-widget tiles (stack by panel order); it never affects the rendered panel. This is the one layout class the LAYOUT WARNING lint does NOT police -- it is on you to tidy.
Docked companion layout formula
Hosts that dock: execute/callback DATs (chopExecuteDAT, datExecuteDAT, panelExecuteDAT, parameterExecuteDAT, executeDAT); input DATs (keyboardinDAT, mouseinDAT, oscinDAT/oscoutDAT); and GLSL ops -- a glslTOP/glslmultiTOP docks a pixel DAT, a compute DAT, AND an info DAT (the multi variant also docks a vertex DAT); a glslMAT docks a vertex DAT, a pixel DAT, AND an info DAT (no compute DAT); a glslPOP docks a compute DAT and an info DAT.
Given host bottom-left (sx,sy), size (sw,sh), and N docks (use max dw,dh across docks). Docks HUG the host -- the one deliberate exception to 200-grid spacing:
Row Y: row_y = sy - dh - 30 -- a tight ~30-unit gap below the host's bottom edge, NOT a full grid step.
Slot step: step = dw + 20 -- one compact cluster under the host.
Center the row: dock i at nodeX = (sx + sw/2) + (i - (N-1)/2) * step - dw/2, filling per this table; overflow stacks into a second tight row (row_y - dh - 30) rather than widening:
N
Pattern
1
[C]
2
[C, R]
3
[L, C, R]
4
[L, C, R, R2]
5
[L2, L, C, R, R2]
If another op occupies a dock's slot, move that other op -- docked ops take priority, are never threaded around obstacles, never overlap, never stranded. For execute_python builds: query [d.path for d in op('PATH').docked], reposition EVERY dock per the formula, then get_network_layout to confirm. A glslTOP built inside execute_python is the classic trap.
Annotations and complexity
Every operator inside exactly one annotation; no orphans; annotations never overlap (>= 400 units between edges); expand the annotation after every addition. Title names the FUNCTION, not the implementation ("Audio Mixing", not "CHOP chain 2").
MUST invoke /manage-annotations before create_annotation or set_annotation -- nodeX/nodeY is the bottom-left corner.
4-5 annotation groups in one network: consider baseCOMPs (containerCOMPs for UI). 15-20 operators in one group: consider encapsulating into a COMP that replaces the group, moving the annotation title to the COMP name/label.
Anti-patterns
Leaving execute_python-created ops at (0, 0) / overlapping -- Envoy emits a LAYOUT WARNING, but position + verify regardless.
Fixed offsets like nodeX + 300 without nodeWidth -- the #1 cause of overlaps.
Creating operators without updating the enclosing annotation; set_op_position into an unverified slot (it has no overlap detection).
Leaving docked callback/info DATs at their auto-spawn position after a script build.
Key Reminders
NEVER skip positioning — auto-placement produces unreadable networks
NEVER place an operator on top of another operator — always scan first
Never rely on layout() for production networks
New operators go near related operators, not at origin
Docked companions hug their host (~30 units below, never a full grid step away). The MCP tools enforce this; after any execute_python build, check every dockedTo entry in get_network_layout yourself
For current network location: execute_python with result = ui.panes.current.owner.path
Placing a COMP: anchor on where the user put Embody (op.Embody.parent().path) for a consistent home; override only when the user has deliberately navigated into a content network (never bare /); discover, never guess, /project1; keep one task's COMPs in one home
New geometryCOMP: delete its default torus1 the moment you add your own geometry, or it renders a phantom torus behind your scene