# Create Operator

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

- Skill: `dylanroscover/create-operator` (Agent Skill)
- Install (CLI): `npx skillmds@latest add dylanroscover/create-operator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dylanroscover/create-operator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Marketing & Growth
- Author: dylanroscover (https://skillmd.com/u/dylanroscover)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/dylanroscover/create-operator

---

<!-- Generated by Embody/Envoy - Do not remove this comment -->

# Create Operator Workflow

Follow these steps every time you create operators via MCP:

1. **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 `/project1` (it may be renamed, and there may be more than one).
    - **Keep one task's COMPs together** under the SAME parent, grouped in one container / annotation. Don't split related COMPs across levels.
2. **Discover the target network**: `query_network` on the target parent to confirm it exists and see existing operators
3. **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)
4. **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.
5. **Create each operator**: `create_op` with the desired type and name
6. **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.
7. **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.
8. **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.
9. **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` section  OP-Reference Parameter Values.
10. **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).
11. **Verify errors**: `get_op_errors` with `recurse=true` to check for errors and warnings. Fix all errors before considering the task complete
12. **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. (TDXN 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.
- TD's `COMP.layout()` -- overlapping, unreadable results.
- 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

