| name | geometry-nodes-python |
|---|---|
| description | Programmatically construct Geometry Nodes trees in Blender 5.x via bpy.data.node_groups, interface socket creation, node instantiation by RNA name, link wiring, and applying as a NODES modifier. Includes Bundles for grouped sockets. |
| standards-version | 1.10.0 |
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
The Python pattern for building a Geometry Nodes tree has four phases:
- Create the node group and its interface (input and output sockets).
- Instantiate nodes by their exact RNA bl_idname.
- Wire nodes with
tree.links.new. - Add a NODES modifier on the target object that points at the group.
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)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 |
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.
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:
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"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:
import bpy
for cls in bpy.types.GeometryNode.__subclasses__():
print(cls.bl_idname)tree.links.new(from_socket, to_socket)You can address sockets by name or by index:
tree.links.new(group_in.outputs["Geometry"], set_pos.inputs["Geometry"])
tree.links.new(noise.outputs["Fac"], multiply.inputs[0]) # by indexIndex is reliable for Math nodes whose two value inputs share the name 'Value'.
For nodes that have inputs without an incoming link:
multiply = tree.nodes.new('ShaderNodeMath')
multiply.operation = 'MULTIPLY'
multiply.inputs[0].default_value = 0.5Some 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.
mod = obj.modifiers.new(name="MyDisplace", type='NODES')
mod.node_group = treeTo set the tree's input values per-modifier (each modifier has its own copies of the tree's exposed inputs):
mod["Input_2"] = 2.5 # the "Strength" input via its identifierModifier inputs are addressed by their socket identifier, not their display name. Find identifiers via:
for item in tree.interface.items_tree:
if item.in_out == 'INPUT':
print(item.identifier, item.name, item.socket_type)In the 5.x interface, item.identifier is what you key into mod[...] with.
Bundles let one socket carry a set of typed values, similar to a struct. Useful for passing multiple related fields between subtrees:
bundle_socket = tree.interface.new_socket(
name="Surface Data",
in_out='OUTPUT',
socket_type='NodeSocketBundle',
)You then build a bundle inside the tree using the Combine Bundle node (GeometryNodeCombineBundle) with named slots, and unpack it with Separate Bundle (GeometryNodeSeparateBundle).
Bundles are 5.0+ only; on 4.5 LTS you fall back to multiple separate sockets.
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)-
Using
tree.inputs.new/tree.outputs.new(legacy 3.x API). 5.x usestree.interface.new_socket(...)exclusively for new code. -
Wrong RNA name for nodes:
tree.nodes.new('GeometrySetPosition') # WRONG, missing 'Node' prefix tree.nodes.new('GeometryNodeSetPosition') # RIGHT
When in doubt, list
bpy.types.GeometryNode.__subclasses__()and grep. -
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. -
Setting modifier inputs by display name:
mod["Strength"] = 2.5 # WRONG, that's the display name mod["Input_2"] = 2.5 # RIGHT, that's the identifier
-
Forgetting to assign
mod.node_groupafter creating the modifier. The modifier exists but does nothing. -
Building the tree without group input/output nodes. The tree's interface sockets only matter once you have
NodeGroupInputandNodeGroupOutputinstances connected to actual nodes inside the tree.
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.
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 treeaddon-scaffoldingfor shipping a tree-building script as part of an extensionmesh-editing-and-bmeshfor reading the modifier-applied result via depsgraph
bpy.data.node_groups: https://docs.blender.org/api/current/bpy.types.BlendDataNodeTrees.htmlbpy.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/