Scaffold a Blender add-on as an Extensions Platform package: blender_manifest.toml, modular file layout, register_classes_factory, symmetric register/unregister. Use when the user starts a new add-on or extension, writes or fixes blender_manifest.toml, asks which layout or registration pattern to use on Blender 4.5 LTS or 5.x, or has classes that fail to register or unregister cleanly. Targets 5.2 LTS with 4.5 LTS fallback.
Installs into .claude/skills of the current project.
Are you the author of Addon Scaffolding?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/tmhsdigital-addon-scaffolding)
---
name: addon-scaffolding
description: "Scaffold a Blender add-on as an Extensions Platform package: blender_manifest.toml, modular file layout, register_classes_factory, symmetric register/unregister. Use when the user starts a new add-on or extension, writes or fixes blender_manifest.toml, asks which layout or registration pattern to use on Blender 4.5 LTS or 5.x, or has classes that fail to register or unregister cleanly. Targets 5.2 LTS with 4.5 LTS fallback."
standards-version: 1.10.0
---
# 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_info` format 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-readable `name`)
- **Target Blender versions** (defaults to `blender_version_min = "4.5.0"` so the add-on works on 4.5 LTS, 5.1 and 5.2 LTS)
- **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
```toml
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 = "Your Name <you@your-domain.example>"
type = "add-on"
blender_version_min = "4.5.0"
license = ["SPDX:GPL-3.0-or-later"]
copyright = ["2026 TMHSDigital"]
```
Field rules that AIs commonly get wrong:
- `tagline` is hard-capped at 64 characters and **must not end with a period**. Blender's manifest validator rejects both.
- `id` must be a valid Python identifier with no double underscore and no leading or trailing underscore: `my_tool` passes; `com.acme.tool`, `_tool`, `tool_` and `my__tool` fail `blender --command extension validate` (`pkg_idname_is_valid_or_error` in the bundled `blender_ext.py`, same rules on 4.5 and 5.2). The directory name does not need to match the `id` but conventionally does.
- `license` and `copyright` are **arrays of strings**, not bare strings.
- `blender_version_min` is 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_version` is `"1.0.0"` for the current manifest schema. Do not bump it.
Optional but commonly wanted:
```toml
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` maps each capability the add-on uses to a human-readable reason; the valid keys are `files`, `network`, `clipboard`, `camera` and `microphone`. It is a **declaration** shown to users and reviewers on the extensions platform, not a sandbox: Blender does not restrict what add-on Python can do, with or without it. Declare everything the code actually uses. For network access, also honor `bpy.app.online_access` (the user's "Allow Online Access" preference) and do nothing online when it is `False`.
## 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 types
```
Keep `__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.
```python
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_factory` returns a `(register, unregister)` pair that loops over `classes` in 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 corresponding `PropertyGroup` class is unregistered. Hence `del bpy.types.Scene.my_addon` precedes `_unregister_classes()`.
- Class registration order matters: `PropertyGroup` subclasses must come before any class that references them via `PointerProperty`.
## Working example: minimal __init__.py
```python
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
1. **Emitting only `bl_info` and skipping `blender_manifest.toml`**. New add-ons must ship as Extensions for the official extensions platform. `bl_info` may appear alongside the manifest as a fallback for backward compatibility, but the manifest is the source of truth.
2. **Manual register/unregister loops with the wrong direction**:
```python
# 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_factory` and let it own the order.
3. **Forgetting to delete bound properties before unregister**. Trying to unregister `MY_ADDON_PG_settings` while `bpy.types.Scene.my_addon` still points to it will hard-fail on Blender restart and leak.
4. **Overlong tagline or trailing period in tagline**. The validator rejects both.
5. **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
`blender_manifest.toml` alone covers 4.2 and later, including 4.5 LTS and 5.2 LTS: with `blender_version_min = "4.5.0"`, a `bl_info` dict adds nothing. Keep `bl_info` alongside the manifest only if the add-on must also install on 4.1 or earlier, or through the legacy add-on folder / "Install legacy Add-on" path. Blender prefers the manifest when both are present.
```python
bl_info = {
"name": "My Addon",
"blender": (4, 5, 0),
"version": (0, 1, 0),
"category": "Mesh",
}
```
## Related
- See the `extension-addon-template` template under [`templates/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/templates) for a ready-to-copy version of this layout.
- See the `operators`, `ui-panels`, and `custom-properties` skills for the contents of each submodule.
- See rules `target-extensions-platform-format` and `type-annotate-props-and-defend-context`.
<!-- examples:begin (generated by scripts/build_examples_index.py from examples/skills.json; do not edit) -->
## Runnable examples
No runnable example proves this skill yet (tracked in [#378](https://github.com/TMHSDigital/Blender-Developer-Tools/issues/378)).
<!-- examples:end -->
## 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