Scaffold a Blender add-on against the Extensions Platform format with blender_manifest.toml, modular file layout, and the register_classes_factory pattern. Targets Blender 5.2 LTS with 4.5 LTS fallback.
Install
npx skillscat add tmhsdigital/blender-developer-tools/addon-scaffolding Install via the SkillsCat registry.
Addon Scaffolding (Extensions Platform)
Trigger
Use this skill when the user:
- Wants to start a new Blender add-on
- Mentions
bl_info,blender_manifest.toml, "extension", "register_classes_factory" - Is migrating an existing add-on from the legacy
bl_infoformat to the Extensions Platform - Is unsure which file layout or registration pattern to use for Blender 4.5+ or 5.x
Required inputs
- Add-on name and short id (snake_case for the manifest
id, human-readablename) - Target Blender versions (defaults to
blender_version_min = "4.5.0"so the add-on works on 4.5 LTS and 5.1) - Maintainer string (
Name <email@example.com>form) - License SPDX identifier (
SPDX:MIT,SPDX:GPL-2.0-or-later, etc.)
The Extensions Platform format (5.x default)
Since 4.2 and required in 5.x for new submissions to the official extensions site, add-ons ship as Extensions. An Extension is a directory containing a blender_manifest.toml plus Python modules.
blender_manifest.toml required fields
schema_version = "1.0.0"
id = "my_addon"
version = "0.1.0"
name = "My Add-on"
tagline = "Short description, max 64 chars, no trailing period"
maintainer = "TMHSDigital <contact@example.com>"
type = "add-on"
blender_version_min = "4.5.0"
license = ["SPDX:MIT"]
copyright = ["2026 TMHSDigital"]Field rules that AIs commonly get wrong:
taglineis hard-capped at 64 characters and must not end with a period. Blender's manifest validator rejects both.idmust be a valid Python identifier ([A-Za-z_][A-Za-z0-9_]*). The directory name does not need to match theidbut conventionally does.licenseandcopyrightare arrays of strings, not bare strings.blender_version_minis the floor. Setting it to"4.5.0"lets the same code run on 4.5 LTS and any 5.x. Setting it to"5.0.0"cuts off LTS users with no warning.schema_versionis"1.0.0"for the current manifest schema. Do not bump it.
Optional but commonly wanted:
website = "https://github.com/TMHSDigital/your-repo"
tags = ["Mesh", "Geometry Nodes"]
permissions = { network = "Optional, only used to fetch updates" }tags must come from Blender's known tag list. permissions is a dict of granted-permission to human-readable reason; if omitted, the extension is sandboxed against network, files, clipboard, and camera.
File layout
The conventional layout for a non-trivial add-on:
my_addon/
blender_manifest.toml
__init__.py # registration entry point only
operators.py # Operator subclasses
panels.py # Panel subclasses
properties.py # PropertyGroup subclasses, PointerProperty bindings
utils.py # Pure helpers, no bpy imports beyond typesKeep __init__.py small. It registers and unregisters classes; everything else lives in submodules.
The register_classes_factory pattern
This is the canonical 5.x registration pattern. It eliminates the loop boilerplate that AIs frequently bug.
import bpy
from bpy.utils import register_classes_factory
from . import operators, panels, properties
classes = (
properties.MyAddonProperties,
operators.MESH_OT_my_addon_action,
panels.VIEW3D_PT_my_addon,
)
_register_classes, _unregister_classes = register_classes_factory(classes)
def register():
_register_classes()
bpy.types.Scene.my_addon = bpy.props.PointerProperty(type=properties.MyAddonProperties)
def unregister():
del bpy.types.Scene.my_addon
_unregister_classes()Why this pattern:
register_classes_factoryreturns a(register, unregister)pair that loops overclassesin the right direction (forward for register, reverse for unregister) and skips the wrong-order foot-gun.- Properties bound to types (
bpy.types.Scene.my_addon = ...) must be deleted before the correspondingPropertyGroupclass is unregistered. Hencedel bpy.types.Scene.my_addonprecedes_unregister_classes(). - Class registration order matters:
PropertyGroupsubclasses must come before any class that references them viaPointerProperty.
Working example: minimal init.py
import bpy
from bpy.utils import register_classes_factory
class MY_ADDON_PG_settings(bpy.types.PropertyGroup):
intensity: bpy.props.FloatProperty(name="Intensity", default=1.0, min=0.0, max=10.0)
class MESH_OT_my_addon_action(bpy.types.Operator):
bl_idname = "mesh.my_addon_action"
bl_label = "My Addon Action"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None and context.active_object.type == 'MESH'
def execute(self, context):
obj = context.active_object
settings = context.scene.my_addon
self.report({'INFO'}, f"Ran on {obj.name} with intensity {settings.intensity:.2f}")
return {'FINISHED'}
class VIEW3D_PT_my_addon(bpy.types.Panel):
bl_label = "My Addon"
bl_idname = "VIEW3D_PT_my_addon"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "My Addon"
def draw(self, context):
layout = self.layout
settings = context.scene.my_addon
layout.prop(settings, "intensity")
layout.operator("mesh.my_addon_action")
classes = (
MY_ADDON_PG_settings,
MESH_OT_my_addon_action,
VIEW3D_PT_my_addon,
)
_register_classes, _unregister_classes = register_classes_factory(classes)
def register():
_register_classes()
bpy.types.Scene.my_addon = bpy.props.PointerProperty(type=MY_ADDON_PG_settings)
def unregister():
del bpy.types.Scene.my_addon
_unregister_classes()Common AI mistakes
Emitting only
bl_infoand skippingblender_manifest.toml. New add-ons must ship as Extensions for the official extensions platform.bl_infomay appear alongside the manifest as a fallback for backward compatibility, but the manifest is the source of truth.Manual register/unregister loops with the wrong direction:
# WRONG: same direction in unregister, classes referencing each other will fail for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) # missing reversed()Use
register_classes_factoryand let it own the order.Forgetting to delete bound properties before unregister. Trying to unregister
MY_ADDON_PG_settingswhilebpy.types.Scene.my_addonstill points to it will hard-fail on Blender restart and leak.Overlong tagline or trailing period in tagline. The validator rejects both.
Hardcoding
blender_version_min = "5.0.0"without reason. Cuts off the LTS user base. Default to"4.5.0"unless you genuinely require a 5.x-only API.
Compatibility paths
For libraries you want to be installable on both 4.5 LTS and 5.1, also keep bl_info for 4.5 fallback even though new submissions to the extensions platform require the manifest. Blender prefers the manifest when both are present.
bl_info = {
"name": "My Addon",
"blender": (4, 5, 0),
"version": (0, 1, 0),
"category": "Mesh",
}Related
- See the
extension-addon-templatetemplate undertemplates/for a ready-to-copy version of this layout. - See the
operators,ui-panels, andcustom-propertiesskills for the contents of each submodule. - See rules
target-extensions-platform-formatandtype-annotate-props-and-defend-context.
References
- Extensions Platform manual: https://docs.blender.org/manual/en/latest/advanced/extensions/index.html
bpy.utils.register_classes_factory: https://docs.blender.org/api/current/bpy.utils.html- Manifest reference: https://docs.blender.org/manual/en/latest/advanced/extensions/getting_started.html