I have created a custom property

class CSGOriginProperty(bpy.types.PropertyGroup):
    origin = FloatVectorProperty(subtype="XYZ", default = (0.0,0.0,0.0))
    size = FloatVectorProperty(subtype="XYZ", default = (1.0,1.0,1.0))

    def template_layout(self, layout):
        row = layout.row(align=True)
        row.prop(self, "origin", text="")
        row = layout.row(align=True)
        row.prop(self, "size", text="")

And it is then used in a node

class CSGOriginNode(Node, CSGNodeTree):
    """CSG Origin Node"""
    bl_idname = "CSGOriginNode"
    bl_label = "Origin"
    bl_icon = "OBJECT_DATA"

    def init(self, context):
        self.outputs.new("CSGOriginSocket", "Output")

    def uda(self, context):
        if self.outputs['Output']:
            self.outputs['Output'].default_value = self.OriginProperty

    OriginProperty = bpy.props.PointerProperty(type=CSGOriginProperty, name="OriginProperty", update=uda)

    def draw_buttons(self, context, layout):

Normally updating the property would trigger the uda function, but in the case of custom properties using the PointerProperty it would seem this is not the case. Is there a workaround for this?

  • $\begingroup$ Suggest using the method outlined in this answer to have one update method, for each property in the group. (and know which pointer.property was updated as well) $\endgroup$
    – batFINGER
    Commented Feb 28, 2017 at 13:31
  • $\begingroup$ How does this work with multiple classes having different callbacks? This solution only seems to be able to call a single hardcoded callback $\endgroup$
    – LAK132
    Commented Feb 28, 2017 at 14:43

2 Answers 2


Major edit:

After re-reading batFINGER's answer, I have figured out another way to do this for Bender 2.79+. One of the key points batFINGER highlights is that you can work out which node is the parent by checking all nodes and seeing if the property is a member of one of them.

Instead of creating a factory though, I created this function that iterates through a node tree and tries to find the node with an attribute that is prop:

def find_prop_node(tree, prop):
    if tree is not None and prop is not None:
        for node in tree.nodes:
            for attr in dir(node):
                if getattr(node, attr) == self:
                    return node
    return None

Now all the property has to do to find its parent node is call find_prop_node from its update function:

class COProperty(PropertyGroup):
    tree = PointerProperty(name="tree", type=NodeTree)
    call = StringProperty(name="call", default="update")

    def callback(self, tree, func):
        self.tree = tree # node.id_name
        self.call = func

    def update(self, context):
        node = find_prop_node(self.tree, self)
        if hasattr(node, self.call):
            getattr(node, self.call)(context)

    origin = FloatProperty(default=1.0, update=update)
    size = FloatProperty(default=1.0, update=update)

    def template_layout(self, layout):
        row = layout.row(align=True)
        row.prop(self, "origin", text="Origin")
        row = layout.row(align=True)
        row.prop(self, "size", text="Size")

The custom Node code now looks like this (note self.id_data instead of self.name):

class CONode(Node, CONodeTree):
    """CO Node"""
    bl_idname = "CONode"
    bl_label = "Origin"
    bl_icon = "OBJECT_DATA"

    def uda(self, context):

    coProp = PointerProperty(type=COProperty, name="coProp")

    def init(self, context):
        self.coProp.callback(self.id_data, "uda")

Because this method isn't reliant on the node's name it isn't susceptible to breaking after a node is renamed. Iterating though the entire node tree might be slower, however.

By adding 2 StringPropertys (parent and call) to the PropertyGroup which store the name of the node that owns it and the function it should call, it can do getattr(self.id_data.nodes[self.parent], self.call)(context):

class COProperty(PropertyGroup):

    parent = StringProperty()
    call = StringProperty(default = "uda")

    def uda(self, context):
        if self.parent in self.id_data.nodes:
            if hasattr(self.id_data.nodes[self.parent], self.call):
                getattr(self.id_data.nodes[self.parent], self.call)(context)

    def callback(self, name, call):
        self.parent = name
        self.call = call

    origin = FloatProperty(default = 1.0, update=uda)
    size = FloatProperty(default = 1.0, update=uda)

To set it up in the node simply call self.coProp.callback(self.name, "uda") in the init (and in update if the name of the node can change):

class CONode(Node, CONodeTree):
    """CO Node"""
    bl_idname = "CONode"
    bl_label = "Origin"
    bl_icon = "OBJECT_DATA"

    def uda(self, context):

    coProp = PointerProperty(type=COProperty, name="coProp")

    def init(self, context):
        self.coProp.callback(self.name, "uda")
  • 1
    $\begingroup$ Good code! Be aware though, that changing the name of said node would break this code entirely. So keep a close eye on your nodes name. $\endgroup$
    – Teck-freak
    Commented Nov 28, 2017 at 8:42

As an addendum to my comment, here's the custom node template (in full so can test), showing a number of ways to set up update callbacks.

In your question, even if the update worked on the PointerProperty, self in the update method uda would be the object that has the updated property (from the update callback), not necessarily the instance of the class from which it is defined.

Bit of overkill, as the single "catch all" update method on the property group (update_prop) would suffice IMHO.

import bpy
from bpy.types import NodeTree, Node, NodeSocket
from bpy.props import FloatVectorProperty
# Implementation of custom nodes from Python

def ogpropclass(update_fn):
    def update_prop(prop):
        def x(self, context):
            print("Called update (%s)" % prop)
            node = None
            for n in self.id_data.nodes:
                if n.originProperty == self:
                    print("Node", n.name)
                    node = n
                    update_fn(node, context)
            print("Updated", prop, getattr(self, prop))

        return x

    class CSGOriginProperty(bpy.types.PropertyGroup):
        origin = FloatVectorProperty(subtype="XYZ", default=(0.0, 0.0, 0.0), update=update_prop("origin"))
        size = FloatVectorProperty(subtype="XYZ", default=(1.0, 1.0, 1.0), update=update_prop("size"))

        def template_layout(self, layout):
            row = layout.row(align=True)
            row.prop(self, "origin", text="")
            row = layout.row(align=True)
            row.prop(self, "size", text="")

    return CSGOriginProperty

# Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc.
class MyCustomTree(NodeTree):
    '''A custom node tree type that will show up in the node editor header'''
    bl_idname = 'CustomTreeType'
    bl_label = 'Custom Node Tree'
    bl_icon = 'NODETREE'

# Custom socket type
class MyCustomSocket(NodeSocket):
    # Description string
    '''Custom node socket type'''
    # Optional identifier string. If not explicitly defined, the python class name is used.
    bl_idname = 'CustomSocketType'
    # Label for nice name display
    bl_label = 'Custom Node Socket'

    # Enum items list
    my_items = [
        ("DOWN", "Down", "Where your feet are"),
        ("UP", "Up", "Where your head should be"),
        ("LEFT", "Left", "Not right"),
        ("RIGHT", "Right", "Not left")

    myEnumProperty = bpy.props.EnumProperty(name="Direction", description="Just an example", items=my_items, default='UP')

    # Optional function for drawing the socket input value
    def draw(self, context, layout, node, text):
        if self.is_output or self.is_linked:
            layout.prop(self, "myEnumProperty", text=text)

    # Socket color
    def draw_color(self, context, node):
        return (1.0, 0.4, 0.216, 0.5)

class MyCustomTreeNode:
    def poll(cls, ntree):
        return ntree.bl_idname == 'CustomTreeType'

class MyCustomNode(Node, MyCustomTreeNode):
    '''A custom node'''
    bl_idname = 'CustomNodeType'
    bl_label = 'Custom Node'
    bl_icon = 'SOUND'

    myStringProperty = bpy.props.StringProperty()
    myFloatProperty = bpy.props.FloatProperty(default=3.1415926)

    def update_fn(self, context):
        print("Called Node.update_fn", repr(self))
        return None

    originProperty = bpy.props.PointerProperty(type=ogpropclass(update_fn), name="OriginProperty")

    def init(self, context):
        self.inputs.new('CustomSocketType', "InPut")
        self.outputs.new('NodeSocketColor', "OutPut")

    # Copy function to initialize a copied node from an existing one.
    def copy(self, node):
        print("Copying from node ", node)

    # Free function to clean up on removal.
    def free(self):
        print("Removing node ", self, ", Goodbye!")

    # Additional buttons displayed on the node.
    def draw_buttons(self, context, layout):
        layout.label("Node settings")

    # Detail buttons in the sidebar.
    # If this function is not defined, the draw_buttons function is used instead
    def draw_buttons_ext(self, context, layout):
        layout.label("Node settings Ext")

    # Optional: custom label
    # Explicit user label overrides this, but here we can define a label dynamically
    def draw_label(self):
        return "I am a custom node"

import nodeitems_utils
from nodeitems_utils import NodeCategory, NodeItem

class MyNodeCategory(NodeCategory):
    def poll(cls, context):
        return context.space_data.tree_type == 'CustomTreeType'

# all categories in a list
node_categories = [
    # identifier, label, items list
    MyNodeCategory("SOMENODES", "Some Nodes", items=[
        # our basic node
    MyNodeCategory("OTHERNODES", "Other Nodes", items=[

        NodeItem("CustomNodeType", label="Node A", settings={
            "myStringProperty": repr("Lorem ipsum dolor sit amet"),
            "myFloatProperty": repr(1.0),
        NodeItem("CustomNodeType", label="Node B", settings={
            "myStringProperty": repr("consectetur adipisicing elit"),
            "myFloatProperty": repr(2.0),

def register():
    nodeitems_utils.register_node_categories("CUSTOM_NODES", node_categories)

def unregister():

if __name__ == "__main__":

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .