Plugin development
An Orbit plugin is a small package (or app module) that configures one or more panels — brand, hooks, resources, middleware — without editing every consumer’s panel file.
Subclass Plugin and implement two hooks:
register— mutate the panel early (brand, colors, resources, …)boot— run at mount time for side effects (render hooks, routes, discovery)
That split keeps config changes separate from “do something when the panel mounts.”


Anatomy
Section titled “Anatomy”from typing import Any
from almasix.orbit.panels.hooks import Pluginfrom almasix.orbit.panels.panel import Panel
class AcmeBrandingPlugin(Plugin): def __init__(self) -> None: super().__init__("acme-branding")
def register(self, panel: Panel) -> None: """Mutate panel config before routes mount.""" panel.brand_name("Acme Admin") panel.favicon("vendor/acme/favicon.svg") panel.primary("#0ea5e9")
def boot(self, panel: Panel) -> None: """Side effects at mount — hooks, discovery, etc.""" panel.render_hook( "panels::styles.after", lambda **_ctx: ' <link rel="stylesheet" href="/0.x/vendor/acme/acme.css" />\n', )Lifecycle when the app boots:
- Your provider builds a
Paneland calls.plugin(AcmeBrandingPlugin())(or.plugins([...])). mount_registered_panels→panel.run_plugins():- every plugin’s
register(panel) - legacy callable plugins (if any)
- every plugin’s
boot(panel) - every
.boot_usingcallback
- every plugin’s
mount_panelloads discovery paths and registers routes.
Orbit does not auto-discover plugins from app/orbit/{id}/plugins/ (or anywhere else). Keep plugin modules next to the panel if you like, but always wire them with .plugin(...) / .plugins([...]) in panel.py.
Legacy callables still work:
panel.plugin(lambda p: p.sidebar_collapsible())Prefer Plugin subclasses for anything you publish.
Using a plugin in an app
Section titled “Using a plugin in an app”from almasix.orbit import Panel, PanelRegistryfrom almasix.providers import ServiceProviderfrom my_orbit_plugin import AcmeBrandingPluginfrom app.orbit.resources.post_resource import PostResource
class OrbitPanelProvider(ServiceProvider): def boot(self) -> None: panel = Panel.make("admin").path("admin").plugin(AcmeBrandingPlugin()).resources( [PostResource] ) self.app.make(PanelRegistry).register(panel)Multiple plugins:
panel.plugins( [ AcmeBrandingPlugin(), BillingPlugin(), ])Order matters: register runs in list order, then boot in the same order.
Scaffold a third-party plugin
Section titled “Scaffold a third-party plugin”Do not start from a blank folder. Smith (or python -m almasix.orbit) writes a publishable package layout, a Plugin subclass, a smoke test, and draft marketplace YAML you can submit later:
smith make:orbit-plugin AuditLog --vendor=acme --author=jane# alias: smith orbit:plugin AuditLog --vendor=acme --author=jane
# Same generator without an Almasix app:python -m almasix.orbit plugin new AuditLog --vendor=acme --author=janeThat creates acme-orbit-audit-log/ with src/acme_orbit_audit_log/plugin.py, tests/test_plugin.py, and marketplace/*.yaml (status: draft). --listing-only writes just the YAML; --no-listing skips it; --paid stubs a checkout URL instead of a PyPI name.
Copy the YAML into the Orbit repository when the plugin is ready to list — Get listed.
Package layout (publishable)
Section titled “Package layout (publishable)”Minimal PyPI-ready layout:
acme-orbit-branding/ pyproject.toml README.md src/ acme_orbit_branding/ __init__.py # export AcmeBrandingPlugin plugin.py assets/ # optional CSS / favicon to publish[project]name = "acme-orbit-branding"version = "0.1.0"dependencies = [ "almasix-orbit>=0.4.0",]
[project.entry-points."almasix.providers"]# optional — only if the plugin should auto-boot as a ServiceProvider# acme_orbit = "acme_orbit_branding.provider:AcmeOrbitProvider"from acme_orbit_branding.plugin import AcmeBrandingPlugin
__all__ = ["AcmeBrandingPlugin"]Publish like any Python package (hatchling / setuptools, then twine upload). Consumers:
pip install acme-orbit-brandingDo not ship an almasix/__init__.py stub in your wheel — that overwrites the framework namespace (see issue #24). Only add packages under your own top-level name, or under almasix.orbit_plugins… if you intentionally extend Orbit’s namespace without a root init file.
Once the package installs, you can list it in the plugin marketplace so other people find it — free or paid. Get listed covers the registry entry and the pull request.
Optional: ServiceProvider auto-discovery
Section titled “Optional: ServiceProvider auto-discovery”If the plugin should register a panel (or several) without the app touching OrbitPanelProvider, expose an Almasix provider entry-point:
from almasix.orbit import Panel, PanelRegistryfrom almasix.providers import ServiceProviderfrom acme_orbit_branding.plugin import AcmeBrandingPlugin
class AcmeOrbitProvider(ServiceProvider): def boot(self) -> None: registry = self.app.make(PanelRegistry) existing = registry.get("admin") if existing is not None: existing.plugin(AcmeBrandingPlugin()) return panel = Panel.make("admin").path("admin").plugin(AcmeBrandingPlugin()) registry.register(panel)Most plugins should stay passive (.plugin(...) in the app) so hosts keep control of panel ids and paths.
Outside panels
Section titled “Outside panels”Plugin is panel-oriented, but the same package can export plain helpers for standalone forms/tables:
from almasix.orbit.forms import TextInput
def acme_title_field() -> TextInput: return TextInput.make("title").label("Title").required()Use those helpers from resources or from non-panel Conduit hosts. Render hooks and Panel.plugin only apply when a panel shell mounts.
Checklist
Section titled “Checklist”- Subclass
Plugin, uniqueget_id - Keep
registerpure (mutate panel); put I/O and hooks inboot - Scope render hooks to the panel id
- No
almasix/__init__.pyin the published wheel - Document the one-liner:
panel.plugin(YourPlugin()) - Add a smoke test that
run_pluginscallsregisterthenboot - Publish the package, then get it listed
Related
Section titled “Related”- Plugin marketplace — browse listings
- How the marketplace works
- Using a plugin
- Get listed
- Listing guidelines
- Paid vs free
- Render hooks — positions and scoping
- Panel configuration — fluent panel API
- Packages — Orbit’s own PyPI map