addon-scaffolding · git:20261012.96b6076 · 2026-10-12 · sha256 15c9b2174f367b54

addon-scaffolding git:20261012.96b6076B

Immutable. This exact content never changes and is served at /api/v1/blob/15c9b2174f367b54.

---
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**. Unregistering `MY_ADDON_PG_settings` while `bpy.types.Scene.my_addon` still points to it does not raise: the class goes away and the pointer stays registered, pointing at a type that no longer exists. Reading that property afterwards returned an empty stub on 4.5.14 and **crashed Blender 5.2.1** (`EXCEPTION_ACCESS_VIOLATION`). Delete the pointer first, then unregister the classes in reverse. Witness: [`examples/addon-register-lifecycle/`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/addon-register-lifecycle) (`--classes-first` leaves the pointer behind, exit 6).

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

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.

- [`addon-register-lifecycle`](https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/addon-register-lifecycle): Check-only witness for the first things every add-on author writes. Falsify: `--drop-manifest` (exit 3).

<!-- 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