Production-tested patterns for creating robust Nuke node trees programmatically. Use for avoiding auto-connection issues, dynamic positioning, expression masks, and gradient blending.
Scanned 9/3/2026
Install to Claude Code
npx -y skills add sideshowroberto/vfx-agent-toolkit --skill nuke-node-tree-patterns --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Nuke Node Tree Patterns?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/sideshowroberto-nuke-node-tree-patterns)More formats (shields.io, HTML) on the badges page.
---
name: nuke-node-tree-patterns
description: Production-tested patterns for creating robust Nuke node trees programmatically. Use for avoiding auto-connection issues, dynamic positioning, expression masks, and gradient blending.
allowed-tools: Read,Write,Bash
---
# Nuke Node Tree Patterns
**Version:** 1.0.0
**Last Updated:** 2026-01-21
**Status:** Production-ready
**Dependencies:** Nuke Python API
---
Production-tested patterns for creating robust, complex node trees programmatically in Nuke.
## Purpose
This skill provides battle-tested patterns for Nuke Python automation, learned from building production tools like the auto-tiling system. These patterns solve common issues like unwanted auto-connections, positioning problems, and blending artifacts.
## Core Patterns
### 1. Avoiding Auto-Connection Issues
**Problem:** `nuke.createNode()` auto-connects to selected nodes, causing unexpected connections.
**Solution:** Use `nuke.nodes.X()` and manage connections explicitly.
```python
# [FAIL] BAD: Auto-connects unpredictably
transform = nuke.createNode('Transform', inpanel=False)
transform.setInput(0, input_node) # May already be connected!
# [OK] GOOD: Explicit control
for node in nuke.allNodes():
node.setSelected(False) # Deselect all first
transform = nuke.nodes.Transform() # Create without auto-connection
transform.setXYpos(x, y) # Position first
transform.setInput(0, input_node) # Then connect explicitly
```
**Key Rules:**
1. **Always deselect** all nodes before creation: `for node in nuke.allNodes(): node.setSelected(False)`
2. **Use `nuke.nodes.X()`** instead of `nuke.createNode('X')`
3. **Position before connecting**: `setXYpos()` then `setInput()`
4. **Connect explicitly**: Never rely on auto-connection
### 2. Dynamic Node Positioning
**Problem:** Hardcoded positions don't work when input nodes move.
**Solution:** Calculate positions relative to input node.
```python
# [FAIL] BAD: Hardcoded positions
base_x = 0
base_y = 0
# [OK] GOOD: Dynamic from input
input_x = input_node.xpos()
input_y = input_node.ypos()
base_x = input_x + 200 # Offset to the right
base_y = input_y + 100 # Offset below
```
**Typical Offsets:**
- Horizontal spacing between tiles: 292px
- Vertical spacing (small): 43px
- Vertical spacing (before Expression): 97px
- Dot row below input: 200px
### 3. Horizontal Tile Distribution Pattern
**Problem:** Creating multiple parallel branches cleanly.
**Solution:** Horizontal dot distribution row.
```python
# Create horizontal row of dots for distribution
dots = []
dot_y = input_y + 200
tile_spacing = 292
for i in range(tile_count):
for node in nuke.allNodes():
node.setSelected(False)
dot = nuke.nodes.Dot()
dot.setName(f'Dot{i+1}')
dot_x = input_x + (i * tile_spacing)
dot.setXYpos(dot_x, dot_y)
if i == 0:
dot.setInput(0, input_node)
else:
dot.setInput(0, dots[-1]) # Chain from previous
dots.append(dot)
# Now create vertical stacks below each dot
for i, dot in enumerate(dots):
tile_x = dot.xpos()
# Create vertical stack at tile_x...
```
**Result:** Clean horizontal layout, Nuke-standard pattern.
### 4. Vertical Node Stacks
**Problem:** Maintaining consistent spacing in vertical node chains.
**Solution:** Calculate Y positions explicitly.
```python
# Define vertical spacing constants
NODE_SPACING_SMALL = 43
NODE_SPACING_LARGE = 97
# Calculate positions for vertical stack
base_y = dot_y + 40
y_transform = base_y
y_reformat = y_transform + NODE_SPACING_SMALL
y_noop = y_reformat + NODE_SPACING_SMALL
y_expression = y_noop + NODE_SPACING_LARGE # Larger gap
y_premult = y_expression + NODE_SPACING_SMALL
y_inverse = y_premult + 55
# Create nodes at calculated positions
transform.setXYpos(tile_x, y_transform)
reformat.setXYpos(tile_x, y_reformat)
noop.setXYpos(tile_x, y_noop)
expression.setXYpos(tile_x, y_expression)
premult.setXYpos(tile_x, y_premult)
inverse_transform.setXYpos(tile_x, y_inverse)
```
### 5. Expression-Based Gradient Masks
**Problem:** Need seamless blending without destroying detail.
**Solution:** Position-based gradient masks using Expression nodes.
```python
def create_gradient_mask(tile_x, tile_y, grid_x, grid_y, tile_size, overlap):
"""
Creates gradient mask that fades at interior edges, sharp at frame edges.
"""
for node in nuke.allNodes():
node.setSelected(False)
expr = nuke.nodes.Expression()
expr.setName(f'TileMask_{tile_x}_{tile_y}')
expr_parts = []
# X direction: leftmost, middle, rightmost pattern
if tile_x == 0:
expr_parts.append(f'smoothstep(0, {overlap}, x)')
elif tile_x == grid_x - 1:
expr_parts.append(f'smoothstep({tile_size}, {tile_size - overlap}, x)')
else:
expr_parts.append(f'smoothstep(0, {overlap}, x)')
expr_parts.append(f'smoothstep({tile_size}, {tile_size - overlap}, x)')
# Y direction: same pattern
if tile_y == 0:
expr_parts.append(f'smoothstep(0, {overlap}, y)')
elif tile_y == grid_y - 1:
expr_parts.append(f'smoothstep({tile_size}, {tile_size - overlap}, y)')
else:
expr_parts.append(f'smoothstep(0, {overlap}, y)')
expr_parts.append(f'smoothstep({tile_size}, {tile_size - overlap}, y)')
expression = ' * '.join(expr_parts)
# Output to mask channel
expr['expr3'].setValue(expression)
expr['channel3'].setValue('mask') # NOT 'mask.a'
expr['channel0'].setValue('none')
expr['channel1'].setValue('none')
expr['channel2'].setValue('none')
return expr
```
**Key Points:**
- Output to `'mask'` channel (not `'mask.a'`)
- Frame edges stay sharp (leftmost/rightmost tiles)
- Interior edges blend smoothly (middle tiles)
- Works for any grid size and aspect ratio
### 6. Applying Masks with Premult
**Problem:** How to apply gradient masks without destroying original alpha.
**Solution:** Premult node with mask.a as alpha.
```python
# After Expression mask node
premult = nuke.nodes.Premult()
premult.setName(f'Premult_{tile_x}_{tile_y}')
premult['channels'].setValue('all')
premult['alpha'].setValue('mask.a') # Use mask channel as alpha
premult.setInput(0, expression_node)
```
**Chain:**
```
ML Node output
v
Expression (creates mask channel)
v
Premult (multiply all by mask.a)
v
Result: Blended contribution
```
### 7. Merge Node Configuration
**Problem:** Standard merge doesn't work for masked tiles.
**Solution:** Use 'plus' operation with rgba output.
```python
merge = nuke.nodes.Merge2()
merge.setName(f'Merge_Tile_{tile_x}_{tile_y}')
merge['operation'].setValue('plus') # NOT 'over'
merge['output'].setValue('rgba') # Explicit rgba output
merge.setInput(0, accumulated_tiles)
merge.setInput(1, current_tile)
```
**Why 'plus':**
- Tiles are pre-masked (via Premult)
- Masks ensure proper weighting (sum to 1.0 in overlaps)
- Plus operation adds weighted contributions correctly
### 8. Reformat Pattern: "to box"
**Problem:** Setting format by string can fail or default to root.
**Solution:** Use 'to box' type with explicit dimensions.
```python
# [FAIL] BAD: Format string may not exist
reformat['format'].setValue('2048 2048 0 0 2048 2048 1') # May default to root
# [OK] GOOD: Explicit dimensions via "to box"
reformat = nuke.nodes.Reformat()
reformat['type'].setValue('to box')
reformat['box_width'].setValue(2048)
reformat['box_height'].setValue(2048)
reformat['box_fixed'].setValue(True)
reformat['resize'].setValue('none')
```
**Use cases:**
- Cropping to tile size
- Restoring original dimensions after processing
- Any dimension change without scaling
### 9. Complete Node Creation Template
**Full pattern combining all best practices:**
```python
def create_node_safely(node_type, name, x, y, **kwargs):
"""
Create a node with all best practices applied.
"""
# 1. Deselect all
for node in nuke.allNodes():
node.setSelected(False)
# 2. Create node via nuke.nodes.X()
node = getattr(nuke.nodes, node_type)()
# 3. Set name
node.setName(name)
# 4. Position BEFORE connecting
node.setXYpos(x, y)
# 5. Set parameters
for key, value in kwargs.items():
if key != 'input': # Handle inputs separately
node[key].setValue(value)
# 6. Connect input last (if provided)
if 'input' in kwargs:
node.setInput(0, kwargs['input'])
return node
# Usage:
transform = create_node_safely(
'Transform',
'Transform_Tile_0_0',
x=100, y=200,
translate=[64, 64],
center=[2880, 2880],
input=input_node
)
```
## Common Pitfalls
### [FAIL] Pitfall 1: Using createNode() with inpanel=False
```python
# Still auto-connects!
node = nuke.createNode('Transform', inpanel=False)
```
**Fix:** Use `nuke.nodes.Transform()` instead.
### [FAIL] Pitfall 2: Connecting Before Positioning
```python
node.setInput(0, input_node) # Connect first
node.setXYpos(100, 200) # Position after
```
**Fix:** Always position before connecting.
### [FAIL] Pitfall 3: Forgetting to Deselect
```python
# If nodes are selected, createNode still connects
node = nuke.nodes.Transform() # May still auto-connect!
```
**Fix:** Always deselect all nodes first.
### [FAIL] Pitfall 4: Expression Output to Wrong Channel
```python
expr['channel3'].setValue('mask.a') # Wrong!
```
**Fix:** Use `'mask'` not `'mask.a'`
### [FAIL] Pitfall 5: Wrong Merge Operation
```python
merge['operation'].setValue('over') # Doesn't work for masked tiles
```
**Fix:** Use `'plus'` for pre-masked tiles.
## Integration with NukeMCPLogger
For MCP-based tools, wrap with logging:
```python
from nuke_mcp_logger import NukeMCPLogger
def create_complex_tree():
log = NukeMCPLogger(session_name="NodeTreeCreation")
try:
# Create nodes with patterns above
transform = create_node_safely(...)
log.increment_stat("nodes_created")
reformat = create_node_safely(...)
log.increment_stat("nodes_created")
log.success(f"Created {count} nodes successfully")
return log.get_results()
except Exception as e:
log.error("Node creation failed", e)
return log.get_results()
```
## Performance Considerations
1. **Deselection overhead**: Minimal, worth it for reliability
2. **Explicit positioning**: No performance impact, improves clarity
3. **Expression masks**: Single eval, cached - very efficient
4. **Premult vs blur**: Premult is faster (no kernel processing)
## Validation Checklist
Before deploying node creation code:
- [ ] Uses `nuke.nodes.X()` not `createNode()`
- [ ] Deselects all before each node creation
- [ ] Positions before connecting
- [ ] Connects inputs explicitly
- [ ] Expression masks output to 'mask' channel
- [ ] Premult uses 'mask.a' as alpha
- [ ] Merge uses 'plus' for masked content
- [ ] Reformat uses "to box" type
- [ ] Dynamic positioning from input nodes
- [ ] NukeMCPLogger integration (if MCP tool)
## Real-World Example
From the auto-tiling tool, complete tile branch creation:
```python
# 1. Deselect
for node in nuke.allNodes():
node.setSelected(False)
# 2. Calculate positions
y_transform = base_y
y_reformat = y_transform + 43
y_noop = y_reformat + 43
y_expression = y_noop + 97
y_premult = y_expression + 43
y_inverse = y_premult + 55
# 3. Transform
transform = nuke.nodes.Transform()
transform.setName(f'Transform_Tile_{tile_x}_{tile_y}')
transform['translate'].setValue([translate_x, translate_y])
transform['center'].setValue([image_width / 2, image_height / 2])
transform.setXYpos(tile_x_pos, y_transform)
transform.setInput(0, input_node)
# 4. Reformat
reformat = nuke.nodes.Reformat()
reformat.setName(f'Reformat_Tile_{tile_x}_{tile_y}')
reformat['type'].setValue('to box')
reformat['box_width'].setValue(tile_size)
reformat['box_height'].setValue(tile_size)
reformat['box_fixed'].setValue(True)
reformat['resize'].setValue('none')
reformat['black_outside'].setValue(True)
reformat.setXYpos(tile_x_pos, y_reformat)
reformat.setInput(0, transform)
# 5. NoOp placeholder
placeholder = nuke.nodes.NoOp()
placeholder.setName(f'MLNode_Tile_{tile_x}_{tile_y}')
placeholder['label'].setValue(f'** SWAP FOR ML NODE **\\nTile [{tile_x},{tile_y}]')
placeholder['tile_color'].setValue(0xff9900ff)
placeholder.setXYpos(tile_x_pos, y_noop)
placeholder.setInput(0, reformat)
# 6. Expression mask
expression = create_gradient_mask(tile_x, tile_y, grid_x, grid_y, tile_size, overlap)
expression.setXYpos(tile_x_pos, y_expression)
expression.setInput(0, placeholder)
# 7. Premult
premult = nuke.nodes.Premult()
premult.setName(f'Premult_{tile_x}_{tile_y}')
premult['channels'].setValue('all')
premult['alpha'].setValue('mask.a')
premult.setXYpos(tile_x_pos, y_premult)
premult.setInput(0, expression)
# 8. InverseTransform
inverse_transform = nuke.nodes.Transform()
inverse_transform.setName(f'InverseTransform_Tile_{tile_x}_{tile_y}')
inverse_transform['translate'].setValue([translate_x, translate_y])
inverse_transform['center'].setValue([image_width / 2, image_height / 2])
inverse_transform['invert_matrix'].setValue(True)
inverse_transform.setXYpos(tile_x_pos, y_inverse)
inverse_transform.setInput(0, premult)
```
## Constitutional Compliance
This skill follows VFX Skill Constitution principles:
- **Article I (General Purpose):** Patterns work for any node tree, any project
- **Article IV (Independent Testing):** Each pattern tested in production
- **Article V (Official Patterns):** Uses standard Nuke Python API correctly
- **Article VI (Context Efficiency):** Documents reusable patterns, not full scripts
## References
- Developed from: nuke-tiling-tool debugging session (2026-01-21)
- Tested in production on a commercial spot
- NukeMCPLogger: `~/.nuke\nuke_mcp_logger.py`
## Version History
- **1.0.0** (2026-01-21): Initial release
- Auto-connection avoidance patterns
- Dynamic positioning
- Expression-based gradient masks
- Premult masking pattern
- Merge configuration for masked content
- Reformat "to box" pattern
---
*These patterns enable robust, production-ready Nuke automation.*
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!