Skip to content
Back to skills

Geometry Nodes Python

ASecurity

Build Geometry Nodes trees from Python: node_groups.new, tree.interface sockets, nodes by RNA identifier, links, Repeat and for-each zones, Bundles, and applying the tree as a NODES modifier including the 5.1 vs 5.2 modifier input API. Use when the user generates a GeometryNodeTree in a script, sets modifier inputs from Python, or hits 'Node type ... undefined' or socket identifier errors. Targets 5.2 LTS with 4.5 LTS fallback.

  • 16 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added September 23, 2026
developmentpythonnodegitapi

Works with

  • api

Security analysis

A100/100

Scanned October 5, 2026

npx -y skills add TMHSDigital/Blender-Developer-Tools --skill geometry-nodes-python --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Geometry Nodes Python?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Geometry Nodes Python
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tmhsdigital-geometry-nodes-python/badge)](https://www.skillsdirectory.com/skills/tmhsdigital-geometry-nodes-python)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: geometry-nodes-python
description: "Build Geometry Nodes trees from Python: node_groups.new, tree.interface sockets, nodes by RNA identifier, links, Repeat and for-each zones, Bundles, and applying the tree as a NODES modifier including the 5.1 vs 5.2 modifier input API. Use when the user generates a GeometryNodeTree in a script, sets modifier inputs from Python, or hits 'Node type ... undefined' or socket identifier errors. Targets 5.2 LTS with 4.5 LTS fallback."
standards-version: 1.10.0
---

# Geometry Nodes in Python

## Trigger

Use this skill when the user:

- Wants to build a Geometry Nodes tree from a script rather than the editor
- Mentions `GeometryNodeTree`, `node_groups.new`, `tree.interface`, `tree.links.new`
- Needs to apply a generated GN tree as a modifier on an object
- Asks about Bundles, Repeat Zones, or for-each Element zones from Python

## High-level shape

The Python pattern for building a Geometry Nodes tree has four phases:

1. Create the node group and its interface (input and output sockets).
2. Instantiate nodes by their exact RNA bl_idname.
3. Wire nodes with `tree.links.new`.
4. Add a NODES modifier on the target object that points at the group.

```python
import bpy


def build_displace_tree():
    tree = bpy.data.node_groups.new(name="MyDisplace", type='GeometryNodeTree')

    tree.interface.new_socket(
        name="Geometry",
        in_out='INPUT',
        socket_type='NodeSocketGeometry',
    )
    tree.interface.new_socket(
        name="Strength",
        in_out='INPUT',
        socket_type='NodeSocketFloat',
    )
    tree.interface.new_socket(
        name="Geometry",
        in_out='OUTPUT',
        socket_type='NodeSocketGeometry',
    )

    group_in = tree.nodes.new('NodeGroupInput')
    group_out = tree.nodes.new('NodeGroupOutput')
    set_pos = tree.nodes.new('GeometryNodeSetPosition')
    noise = tree.nodes.new('ShaderNodeTexNoise')
    multiply = tree.nodes.new('ShaderNodeMath')
    multiply.operation = 'MULTIPLY'

    group_in.location = (-400, 0)
    set_pos.location = (200, 0)
    group_out.location = (600, 0)
    noise.location = (-200, -200)
    multiply.location = (0, -100)

    tree.links.new(group_in.outputs["Geometry"], set_pos.inputs["Geometry"])
    tree.links.new(set_pos.outputs["Geometry"], group_out.inputs["Geometry"])

    tree.links.new(noise.outputs["Fac"], multiply.inputs[0])
    tree.links.new(group_in.outputs["Strength"], multiply.inputs[1])
    tree.links.new(multiply.outputs[0], set_pos.inputs["Offset"])

    return tree


def attach_tree_to_object(obj, tree):
    mod = obj.modifiers.new(name="MyDisplace", type='NODES')
    mod.node_group = tree
    return mod


tree = build_displace_tree()
obj = bpy.context.active_object
mod = attach_tree_to_object(obj, tree)
```

## Creating the node group

```python
tree = bpy.data.node_groups.new(name="MyDisplace", type='GeometryNodeTree')
```

The `type` argument is the RNA `bl_idname` of the tree class. The Geometry Nodes tree type is `'GeometryNodeTree'`. Other useful types:

| Type | Used for |
| --- | --- |
| `'GeometryNodeTree'` | Geometry Nodes |
| `'ShaderNodeTree'` | Material shaders |
| `'CompositorNodeTree'` | Compositor |

## Interface sockets (5.x model)

Pre-4.0 used the legacy `inputs` and `outputs` collections. **5.x uses `tree.interface`**, which is unified across input and output sockets and supports panels, descriptions, and default values.

```python
tree.interface.new_socket(
    name="Strength",
    in_out='INPUT',
    socket_type='NodeSocketFloat',
)
```

`in_out` is `'INPUT'` or `'OUTPUT'`. `socket_type` is the RNA name of the socket class:

| socket_type | Carries |
| --- | --- |
| `'NodeSocketGeometry'` | Geometry (mesh, curve, instances, volume) |
| `'NodeSocketFloat'` | Single float |
| `'NodeSocketInt'` | Single int |
| `'NodeSocketBool'` | Single bool |
| `'NodeSocketVector'` | 3-float vector |
| `'NodeSocketColor'` | RGBA color |
| `'NodeSocketString'` | String |
| `'NodeSocketObject'` / `'NodeSocketCollection'` / `'NodeSocketImage'` / `'NodeSocketMaterial'` | Datablock pointers |
| `'NodeSocketBundle'` (5.0+) | A bundle of multiple typed sockets in one connection |

Set defaults and ranges after creation:

```python
strength_socket = tree.interface.new_socket(
    name="Strength",
    in_out='INPUT',
    socket_type='NodeSocketFloat',
)
strength_socket.default_value = 1.0
strength_socket.min_value = 0.0
strength_socket.max_value = 10.0
strength_socket.description = "Displacement amount along normals"
```

## Instantiating nodes

```python
node = tree.nodes.new('GeometryNodeSetPosition')
```

The argument is the exact RNA `bl_idname` of the node class. A few you'll reach for often:

| Node | bl_idname |
| --- | --- |
| Group Input | `NodeGroupInput` |
| Group Output | `NodeGroupOutput` |
| Set Position | `GeometryNodeSetPosition` |
| Position (input) | `GeometryNodeInputPosition` |
| Normal (input) | `GeometryNodeInputNormal` |
| Math | `ShaderNodeMath` |
| Vector Math | `ShaderNodeVectorMath` |
| Mix | `ShaderNodeMix` |
| Noise Texture | `ShaderNodeTexNoise` |
| Mesh to SDF Grid (4.3+) | `GeometryNodeMeshToSDFGrid` |
| Grid to Mesh (meshes an SDF/grid) | `GeometryNodeGridToMesh` |
| Volume to Mesh (meshes a volume geometry) | `GeometryNodeVolumeToMesh` |
| Repeat Input / Output | `GeometryNodeRepeatInput`, `GeometryNodeRepeatOutput` |
| For Each Element Input / Output (4.3+) | `GeometryNodeForeachGeometryElementInput`, `GeometryNodeForeachGeometryElementOutput` |

To list all available Geometry node types in your Blender version:

```python
import bpy
# bpy.types registers node classes lazily, so GeometryNode.__subclasses__()
# returns [] until something touches them. dir(bpy.types) lists them all.
for name in sorted(n for n in dir(bpy.types) if n.startswith("GeometryNode")):
    print(name)
```

## Wiring nodes

```python
tree.links.new(from_socket, to_socket)
```

You can address sockets by name or by index:

```python
tree.links.new(group_in.outputs["Geometry"], set_pos.inputs["Geometry"])
tree.links.new(noise.outputs["Fac"], multiply.inputs[0])  # by index
```

Index is reliable for Math nodes whose two value inputs share the name `'Value'`.

## Setting input defaults on a node

For nodes that have inputs without an incoming link:

```python
multiply = tree.nodes.new('ShaderNodeMath')
multiply.operation = 'MULTIPLY'
multiply.inputs[0].default_value = 0.5
```

Some nodes have `bpy.props`-like attributes for their mode (e.g. `multiply.operation = 'MULTIPLY'`, `mix.data_type = 'VECTOR'`, `noise.noise_dimensions = '3D'`). Inspect with `dir(node)` if you're not sure.

## Applying as a NODES modifier

```python
mod = obj.modifiers.new(name="MyDisplace", type='NODES')
mod.node_group = tree
```

To set the tree's input values per-modifier (each modifier has its own copies of the tree's exposed inputs), look up the socket **identifier**, then branch on `bpy.app.version`. The dict form is correct on 4.5 LTS and 5.1. It is **removed** in 5.2: `mod[identifier] = value` raises `TypeError: bpy_struct[key] = val: id properties not supported for this type` rather than silently no-opping. 5.2+ uses the RNA properties added on `NodesModifier`.

```python
ident = None
for item in tree.interface.items_tree:
    if item.in_out == 'INPUT' and item.name == "Strength":
        ident = item.identifier
        break

if bpy.app.version >= (5, 2, 0):
    getattr(mod.properties.inputs, ident).value = 2.5
else:
    mod[ident] = 2.5
```

Find identifiers via the interface (do not guess `"Input_2"` — current trees emit `Socket_N`):

```python
for item in tree.interface.items_tree:
    if item.in_out == 'INPUT':
        print(item.identifier, item.name, item.socket_type)
```

After writing a modifier input, tag the object and update the view layer before `evaluated_get`. Otherwise the depsgraph still holds the previous scale.

## Bundles (5.0+ official)

Bundles let one socket carry a set of typed values, similar to a struct. Useful for passing multiple related fields between subtrees.

The 5.x RNA names are `NodeCombineBundle` / `NodeSeparateBundle`.
`GeometryNodeCombineBundle` is the **4.5 experimental** id and raises
`RuntimeError: Node type GeometryNodeCombineBundle undefined` on 5.2.
Items live on `node.bundle_items` (not `items` — that is a dict method):

```python
comb = tree.nodes.new("NodeCombineBundle")
sep = tree.nodes.new("NodeSeparateBundle")
comb.bundle_items.new("GEOMETRY", "Mesh")
comb.bundle_items.new("FLOAT", "Scale")
sep.bundle_items.new("GEOMETRY", "Mesh")
sep.bundle_items.new("FLOAT", "Scale")
tree.links.new(comb.outputs["Bundle"], sep.inputs["Bundle"])
```

Separate item **names** must match Combine. A linked tree whose Separate
looks up `Geom` instead of `Mesh` evaluates empty.

Do not assert that the bundle nodes exist. Assert evaluated geometry
against a closed form, and do not stop at a vert count — a cube that
never entered the bundle is still 8 verts. Second axis: unpacked Scale /
Offset bbox. Third: a packed Float stored as a named attribute.

4.5 LTS: the old `GeometryNodeCombineBundle` RNA exists behind
`preferences.experimental.use_bundle_and_closure_nodes` (default **off**).
With the flag off, evaluation is empty. With it on, the same closed form
lands. Official / CI contract is 5.0+; skip 4.5 rather than flipping
experimental preferences.

```python
bundle_socket = tree.interface.new_socket(
    name="Surface Data",
    in_out='OUTPUT',
    socket_type='NodeSocketBundle',
)
```

## Zone pairing (Repeat / For Each Element)

Repeat and For Each Element are **paired** input/output nodes. Creating both
and linking sockets is not enough: call `pair_with_output` on the input node.
Unpaired Repeat Input has no Geometry sockets (only Iterations). Unpaired
For Each evaluates to empty geometry (`Cannot evaluate node group` on 4.5).

```python
rin = tree.nodes.new("GeometryNodeRepeatInput")
rout = tree.nodes.new("GeometryNodeRepeatOutput")
rin.pair_with_output(rout)  # creates the Geometry items on both nodes
rin.inputs["Iterations"].default_value = 3

fin = tree.nodes.new("GeometryNodeForeachGeometryElementInput")
fout = tree.nodes.new("GeometryNodeForeachGeometryElementOutput")
fin.pair_with_output(fout)
fout.domain = "POINT"
```

For Each Element has two Geometry outputs. `outputs["Geometry"]` is the **main**
passthrough (the input mesh). Generated meshes live on `Generation_0`. Wiring
Group Output to the main socket is the vacuous tree: nodes exist and are
linked, the zone does not iterate into the result.

Do not assert that the zone nodes exist. Assert evaluated topology against a
closed form (cube verts = `8 × (1 + N)` for a Repeat that joins one cube per
iteration; `8 × P` for a For Each over P points). Count alone can still pass
if N+1 cubes are Joined at the origin — also assert per-iteration positions.

## Detecting Geometry Nodes feature support

```python
import bpy

def has_bundles():
    major, _minor, _patch = bpy.app.version
    return major >= 5

def has_for_each_element():
    # The For Each Element zone shipped in Blender 4.3, so it is available on
    # the whole 4.5 LTS / 5.x supported range. (Bundles, by contrast, are 5.0+.)
    return bpy.app.version >= (4, 3, 0)
```

## Common AI mistakes

1. **Using `tree.inputs.new` / `tree.outputs.new`** (legacy 3.x API). 5.x uses `tree.interface.new_socket(...)` exclusively for new code.

2. **Wrong RNA name for nodes**:

   ```python
   tree.nodes.new('GeometrySetPosition')  # WRONG, missing 'Node' prefix
   tree.nodes.new('GeometryNodeSetPosition')  # RIGHT
   ```

   When in doubt, list `[n for n in dir(bpy.types) if n.startswith("GeometryNode")]` and grep. (`bpy.types.GeometryNode.__subclasses__()` returns an empty list: the classes are created lazily.)

3. **Linking nodes by name when both inputs share a name** (`Value`, `Vector`, `Geometry`) and getting the wrong one. Use indices or the named inputs of the parent node.

4. **Setting modifier inputs by display name, or using the 5.1 dict form on 5.2**:

   ```python
   mod["Strength"] = 2.5  # WRONG on every version — that's the display name
   mod["Socket_1"] = 2.5  # RIGHT on 4.5 / 5.1; TypeError on 5.2
   getattr(mod.properties.inputs, "Socket_1").value = 2.5  # RIGHT on 5.2+; AttributeError on 4.5 / 5.1
   ```

5. **Forgetting to assign `mod.node_group`** after creating the modifier. The modifier exists but does nothing.

6. **Building the tree without group input/output nodes**. The tree's interface sockets only matter once you have `NodeGroupInput` and `NodeGroupOutput` instances connected to actual nodes inside the tree.

7. **Creating Repeat / For Each input and output nodes without `pair_with_output`**. Unpaired zones do not iterate. For Each: wiring Group Output to the main `Geometry` socket ships the input mesh and looks linked.

8. **`tree.nodes.new("GeometryNodeCombineBundle")` on 5.x**. That id is 4.5 experimental. 5.x is `NodeCombineBundle` / `NodeSeparateBundle`. `bundle_items.new("FLOAT", "Name")` — `node.items` is a dict method, not the collection.

## Worked example: replicate the "Mesh to SDF then Grid to Mesh" pipeline

An SDF grid is meshed with **Grid to Mesh** (`GeometryNodeGridToMesh`), not **Volume to
Mesh**. The `Mesh to SDF Grid` output is a *grid* socket; `Volume to Mesh` takes a *volume
geometry* socket (what `Mesh to Volume` produces), so wiring the SDF grid into it is an
invalid connection that silently yields no geometry. `Grid to Mesh` has the matching grid
input. For an SDF the surface is at distance 0, so use `threshold=0.0`.

```python
import bpy


def build_remesh_via_sdf(voxel_size=0.05, threshold=0.0):
    tree = bpy.data.node_groups.new(name="SDFRemesh", type='GeometryNodeTree')

    tree.interface.new_socket(name="Geometry", in_out='INPUT', socket_type='NodeSocketGeometry')
    tree.interface.new_socket(name="Geometry", in_out='OUTPUT', socket_type='NodeSocketGeometry')

    grp_in = tree.nodes.new('NodeGroupInput')
    grp_out = tree.nodes.new('NodeGroupOutput')
    mesh_to_sdf = tree.nodes.new('GeometryNodeMeshToSDFGrid')
    grid_to_mesh = tree.nodes.new('GeometryNodeGridToMesh')

    mesh_to_sdf.inputs["Voxel Size"].default_value = voxel_size
    grid_to_mesh.inputs["Threshold"].default_value = threshold  # isosurface at the SDF zero-level

    grp_in.location = (-400, 0)
    mesh_to_sdf.location = (-150, 0)
    grid_to_mesh.location = (150, 0)
    grp_out.location = (400, 0)

    tree.links.new(grp_in.outputs["Geometry"], mesh_to_sdf.inputs["Mesh"])
    tree.links.new(mesh_to_sdf.outputs["SDF Grid"], grid_to_mesh.inputs["Grid"])
    tree.links.new(grid_to_mesh.outputs["Mesh"], grp_out.inputs["Geometry"])

    return tree
```

## Related

- `addon-scaffolding` for shipping a tree-building script as part of an extension
- Example `gn-zone-iterate` for Repeat / For Each pairing versus evaluated closed forms
- Example `gn-bundle-roundtrip` for Combine / Separate item-name round-trip (5.0+; skip 4.5)
- `mesh-editing-and-bmesh` for reading the modifier-applied result via depsgraph

<!-- examples:begin (generated by scripts/build_examples_index.py from examples/skills.json; do not edit) -->
## Runnable examples

Each example runs headless, asserts the contract, and exits non-zero when it breaks. Run one with `blender --background --python <script> --`; pass a falsifier flag to watch the check fail.

- [`gn-bundle-roundtrip`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-bundle-roundtrip): Combine Bundle / Separate Bundle packing a cube plus Scale, Offset, and Mark — witnessing `geometry-nodes-python` item-name round-trip, not tree structure. Blender 5.0+. Falsify: `--legacy-rna` (exit 7).
- [`gn-instance-grid`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-instance-grid): A generative Geometry Nodes tree — Mesh Grid → Instance on Points (a modeled keycap via Object Info) → Realize Instances → Set Shade Smooth → Set Material. Falsify: `--one-cell` (exit 4).
- [`gn-modifier-inputs`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-modifier-inputs): Per-modifier Geometry Nodes Float inputs on a shared tree — 4.5/5.1 write mod[identifier], 5.2 writes mod.properties.inputs.Socket_1.value. Falsify: `--same-height` (exit 7).
- [`gn-sdf-remesh`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-sdf-remesh): A Geometry Nodes SDF remesh (MeshToSDFGrid → GridToMesh at the SDF zero-level), with a Set Material node carrying the material through the remesh. Falsify: `--no-sdf` (exit 3).
- [`gn-sim-fountain`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-sim-fountain): A Simulation Zone only advances one step per consecutive frame_set. Falsify: `--unpair` (exit 3).
- [`gn-socket-rename`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-socket-rename): Compare and Random Value socket identifiers collapsed onto reused names in 5.2; enabled-name lookup wires on 4.5, 5.1, and 5.2 Falsify: `--legacy-ids` (exit 5).
- [`gn-zone-iterate`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/gn-zone-iterate): Repeat Zone and For Each Element only iterate after pair_with_output. Falsify: `--unpair-repeat` (exit 3).

<!-- examples:end -->

## References

- `bpy.data.node_groups`: https://docs.blender.org/api/current/bpy.types.BlendDataNodeTrees.html
- `bpy.types.NodeTreeInterface`: https://docs.blender.org/api/current/bpy.types.NodeTreeInterface.html
- Geometry Nodes types index: https://docs.blender.org/api/current/bpy.types.GeometryNode.html
- 5.x release notes (Bundles, Repeat Zones, For Each): https://developer.blender.org/

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.