NetBox Plugin Development
Your knowledge of NetBox plugin APIs may be outdated. Base classes, mixins, template tags, and registration patterns change between NetBox releases. Prefer retrieval over pre-trained knowledge.
Retrieval Sources
| Source | URL / Method | Use for |
|---|---|---|
| Plugin development docs | https://netboxlabs.com/docs/netbox/plugins/development/ |
Plugin framework reference |
| NetBox repo | https://github.com/netbox-community/netbox |
Base classes, mixins, current APIs |
| NetBox release notes | https://netboxlabs.com/docs/netbox/release-notes/ |
Breaking changes, new plugin features |
| Example plugins | https://github.com/netbox-community |
Community plugin patterns |
Build plugins that extend NetBox with custom models, views, APIs, and UI elements. Target: NetBox 4.5–4.6 / Python 3.12–3.14. Note the Django split: NetBox 4.5 runs on Django 5.2, NetBox 4.6 on Django 6.0 — code, migrations, and third-party deps must be Django 6.0-compatible when targeting 4.6+.
For REST API client patterns (pagination, filtering, tokens), see netbox-api-integration.
Quick Reference — Key Imports
# Models
from netbox.models import NetBoxModel # base for all plugin models
from netbox.models import PrimaryModel # adds description + comments (4.5+)
from netbox.models import OrganizationalModel # name/slug/description/comments (4.5+)
# Views
from netbox.views.generic import (ObjectListView, ObjectView, ObjectEditView,
ObjectDeleteView, BulkImportView, BulkEditView, BulkDeleteView)
from utilities.views import register_model_view, ViewTab
# Forms
from netbox.forms import NetBoxModelForm, NetBoxModelBulkEditForm
from netbox.forms import NetBoxModelFilterSetForm, NetBoxModelImportForm
# Tables
from netbox.tables import NetBoxTable, columns
# FilterSets
from netbox.filtersets import NetBoxModelFilterSet
from django_filters import FilterSet # for @register_filterset
# REST API
from netbox.api.serializers import NetBoxModelSerializer
from netbox.api.viewsets import NetBoxModelViewSet
from netbox.api.routers import NetBoxRouter
# Navigation
from netbox.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
1. Plugin Structure & Setup
Package Layout
netbox_myplugin/
├── __init__.py # PluginConfig + config variable
├── version.py # __version__ = '1.0.0'
├── models.py # Django models (NetBoxModel subclasses)
├── views.py # UI views
├── tables.py # django-tables2 table classes
├── forms.py # Model, bulk edit, filter, import forms
├── filtersets.py # FilterSet classes
├── navigation.py # Menu items (auto-discovered)
├── search.py # Search indexes (auto-discovered)
├── graphql/ # Strawberry GraphQL schema
│ ├── schema.py # Query class (auto-discovered)
│ ├── types.py
│ └── filters.py
├── api/
│ ├── urls.py # NetBoxRouter config
│ ├── views.py # API ViewSets
│ └── serializers.py
├── templates/netbox_myplugin/
│ └── myplugin_model.html # Detail view templates
├── template_content.py # Template extensions (auto-discovered)
└── tests/
├── test_models.py
├── test_views.py
├── test_api.py
└── test_forms.py
PluginConfig (__init__.py)
from netbox.plugins import PluginConfig
from .version import __version__
class MyPluginConfig(PluginConfig):
name = 'netbox_myplugin'
verbose_name = 'My Plugin'
description = 'Adds custom functionality to NetBox'
version = __version__
author = 'Your Name'
author_email = 'you@example.com'
base_url = 'myplugin' # URL prefix under /plugins/
min_version = '4.5.0'
max_version = '4.6.99' # span 4.5–4.6; use .99 to allow patch releases
default_settings = {'feature_x': True}
required_settings = []
def ready(self):
super().ready()
from . import signals # deferred imports to avoid circular deps
config = MyPluginConfig # MUST be module-level variable named 'config'
Auto-discovery: NetBox automatically discovers modules at navigation.menu,
navigation.menu_items, template_content.template_extensions, search.indexes,
graphql.schema, and more. No manual registration needed for these.
2. Models
Subclass NetBoxModel for full feature support (tags, custom fields, change logging,
bookmarks, journaling, export templates, event rules, notifications).
from django.db import models
from django.urls import reverse
from netbox.models import NetBoxModel
class AccessList(NetBoxModel):
name = models.CharField(max_length=100)
device = models.ForeignKey(
to='dcim.Device',
related_name='%(app_label)s_access_lists', # avoid FK collisions
)
type = models.CharField(max_length=50, choices=AccessListTypeChoices)
class Meta:
ordering = ['name']
unique_together = ('device', 'name')
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('plugins:netbox_myplugin:accesslist', args=[self.pk])
4.5+:
PrimaryModel(description + comments) andOrganizationalModel(name/slug/description/comments, enforces unique name+slug) are now part of the plugins API. Use them when your model fits those patterns.
Key gotchas:
- Use
%(app_label)s_prefix onrelated_nameto avoid FK collisions across plugins - Reference core models as strings (
'dcim.Device') in ForeignKey fields - Always define
get_absolute_url()— followsplugins:<plugin_name>:<model_name>pattern - Permissions format:
netbox_myplugin.view_accesslist,.add_accesslist, etc.
See references/model-patterns.md for base class hierarchy, mixins, choices patterns, and migration tips.
3. Views
Use the @register_model_view decorator for standard CRUD views:
from netbox.views.generic import (
ObjectListView, ObjectView, ObjectEditView, ObjectDeleteView,
BulkImportView, BulkEditView, BulkDeleteView
)
from utilities.views import register_model_view, ViewTab
from .models import AccessList
from .tables import AccessListTable
from .forms import AccessListForm, AccessListFilterForm, AccessListBulkEditForm
from .filtersets import AccessListFilterSet
@register_model_view(AccessList, 'list')
class AccessListListView(ObjectListView):
queryset = AccessList.objects.all()
table = AccessListTable
filterset = AccessListFilterSet
filterset_form = AccessListFilterForm
@register_model_view(AccessList)
class AccessListView(ObjectView):
queryset = AccessList.objects.all()
@register_model_view(AccessList, 'edit')
class AccessListEditView(ObjectEditView):
queryset = AccessList.objects.all()
form = AccessListForm
@register_model_view(AccessList, 'delete')
class AccessListDeleteView(ObjectDeleteView):
queryset = AccessList.objects.all()
URL Configuration (urls.py)
from netbox.views.generic import get_model_urls
from . import models
urlpatterns = get_model_urls('netbox_myplugin', models)
Extending Core Model Views with Tabs
@register_model_view(Device, 'access_lists', path='access-lists')
class DeviceAccessListsView(ObjectChildrenView):
queryset = Device.objects.all()
child_model = AccessList
table = AccessListTable
tab = ViewTab(label='Access Lists', badge=lambda obj: obj.accesslists.count())
See references/views-and-api.md for all view classes and URL patterns.
4. REST API
Serializer
from netbox.api.serializers import NetBoxModelSerializer
from ..models import AccessList
class AccessListSerializer(NetBoxModelSerializer):
class Meta:
model = AccessList
fields = ('id', 'url', 'display', 'name', 'device', 'type',
'tags', 'custom_fields', 'created', 'last_updated')
brief_fields = ('id', 'url', 'display', 'name') # for nested representation
ViewSet
from netbox.api.viewsets import NetBoxModelViewSet
from ..models import AccessList
from ..filtersets import AccessListFilterSet
from .serializers import AccessListSerializer
class AccessListViewSet(NetBoxModelViewSet):
queryset = AccessList.objects.all()
serializer_class = AccessListSerializer
filterset_class = AccessListFilterSet
Router (api/urls.py)
from netbox.api.routers import NetBoxRouter
from . import views
router = NetBoxRouter()
router.register('access-lists', views.AccessListViewSet)
urlpatterns = router.urls
API URL naming: plugins-api:netbox_myplugin-api:accesslist-detail
(this matters for HyperlinkedIdentityField on serializers).
See references/views-and-api.md for GraphQL, nested serializers, and advanced API patterns.
5. Forms, Tables & FilterSets
Model Form
from netbox.forms import NetBoxModelForm
from utilities.forms.fields import DynamicModelChoiceField
from utilities.forms.rendering import FieldSet
from dcim.models import Device
from .models import AccessList
class AccessListForm(NetBoxModelForm):
device = DynamicModelChoiceField(queryset=Device.objects.all())
fieldsets = (
FieldSet('name', 'device', 'type'),
FieldSet('tags', name='Tags'),
)
class Meta:
model = AccessList
fields = ('name', 'device', 'type', 'tags')
Table
from netbox.tables import NetBoxTable, columns
class AccessListTable(NetBoxTable):
name = columns.LinkColumn()
device = columns.LinkColumn()
type = columns.ChoiceFieldColumn()
tags = columns.TagColumn()
class Meta(NetBoxTable.Meta):
model = AccessList
fields = ('pk', 'name', 'device', 'type', 'tags')
default_columns = ('name', 'device', 'type')
FilterSet
from netbox.filtersets import NetBoxModelFilterSet
from utilities.filters import register_filterset
from .models import AccessList
@register_filterset # 4.5+: enables lookup modifiers in UI
class AccessListFilterSet(NetBoxModelFilterSet):
class Meta:
model = AccessList
fields = ('name', 'device', 'type')
def search(self, queryset, name, value):
"""Required for ?q= parameter to work."""
return queryset.filter(name__icontains=value)
4.5+: The
@register_filtersetdecorator is required for lookup modifiers (e.g.,name__ic,device_id__n) to appear in the UI filter forms.
See references/forms-tables-filtersets.md for bulk edit forms, import forms, filter form widgets, and all field types.
6. Navigation
# navigation.py — auto-discovered
from netbox.plugins import PluginMenu, PluginMenuItem, PluginMenuButton
menu = PluginMenu(
label='My Plugin',
icon_class='mdi mdi-shield-lock',
groups=(
('Access Control', (
PluginMenuItem(
link='plugins:netbox_myplugin:accesslist_list',
link_text='Access Lists',
permissions=['netbox_myplugin.view_accesslist'],
buttons=(
PluginMenuButton(
link='plugins:netbox_myplugin:accesslist_add',
title='Add',
icon_class='mdi mdi-plus-thick',
permissions=['netbox_myplugin.add_accesslist'],
),
),
),
)),
),
)
7. Template Extensions
Two approaches — use ViewTab (modern) for tab-based extensions, or PluginTemplateExtension for injecting content into existing pages:
# template_content.py
from netbox.plugins import PluginTemplateExtension
class DeviceAccessInfo(PluginTemplateExtension):
models = ['dcim.device'] # list of strings, not model classes; None = global
def right_page(self):
return self.render('netbox_myplugin/inc/device_access.html', extra_context={
'access_lists': self.context['object'].accesslists.all()
})
template_extensions = [DeviceAccessInfo]
Available methods: head(), navbar(), list_buttons(), buttons(), alerts(),
left_page(), right_page(), full_width_page().
8. Testing
from django.test import TestCase
from utilities.testing import APITestCase
from .models import AccessList
class AccessListAPITest(APITestCase):
model = AccessList
def setUp(self):
self.device = Device.objects.create(name='test-device', ...)
self.access_list = AccessList.objects.create(
name='test-acl', device=self.device, type='standard'
)
def test_list(self):
url = reverse('plugins-api:netbox_myplugin-api:accesslist-list')
response = self.client.get(url, **self.header)
self.assertEqual(response.status_code, 200)
See references/testing-guide.md for fixtures, view tests, form tests, and CI setup.
9. Packaging
Use pyproject.toml (modern) or setup.py:
[project]
name = "netbox-myplugin"
version = "1.0.0"
dependencies = ["netbox>=4.5.0,<4.7"] # span 4.5–4.6
[project.entry-points."netbox.plugins"]
netbox_myplugin = "netbox_myplugin:config"
Note: The entry point key must match
PluginConfig.nameand point to the module-levelconfigvariable.
See references/packaging.md for versioning strategy, publishing to PyPI, and version compatibility matrix.
10. Common Gotchas
configmust be module-level in__init__.py— not inside a function- FK
related_namecollisions — prefix with%(app_label)s_when multiple plugins target the same core model - URL namespace — views:
plugins:<plugin>:<view>, API:plugins-api:<plugin>-api:<basename>-detail - Missing
search()on filterset —?q=won't work without it modelson TemplateExtension — must be list of'app.model'strings, not classes;None= global- Heavy imports in
__init__.py— useready()for signals and deferred imports (fixed in 4.5.2 but still best practice) max_versiontoo strict — use'4.6.99'(or your top minor's.99) not'4.5.0'to allow patches- GraphQL uses Strawberry since 4.0 — Graphene patterns will not work
@register_filterset(4.5+) — without it, lookup modifiers won't appear in UI- Permissions — format is
<plugin_name>.view_<model>,<plugin_name>.add_<model>, etc. Custom actions (4.6.0+): declare extra actions via your model'sMeta.permissions; NetBox auto-registers them as actions selectable in the ObjectPermission form (preferred over ad-hoc permission checks).
Version Notes
NetBox 4.6 (2026)
- Django 6.0 (was 5.2 in 4.5) — ensure code, migrations, and dependencies are Django 6.0-compatible; set
min_version='4.6.0'for any plugin using 4.6-only APIs below - Declarative view layouts / reusable UI components —
from netbox.ui import layout(Layout/Row/Column); the modern alternative to hand-written detail templates (requires NetBox ≥4.6.0) - Custom model actions / permissions via
Meta.permissions— declare them on your model'sMeta; NetBox auto-registers them as actions selectable in the ObjectPermission form - Custom serializer resolvers for
get_serializer_for_model()(4.6.2) — register a resolver to control serializer lookup - Security: ExportTemplate/ConfigTemplate
environment_paramsRCE (CVE-2026-29514) fixed in 4.6.1 — the allowlist blocksextensions/finalize/loader/bytecode_cache - Deprecations to avoid:
DEFAULT_ACTION_PERMISSIONS, legacy view actions, the internal registrymodelskey, the customquerystringtemplate tag, andOptionalLimitOffsetPagination
NetBox 4.5 (2026-01-06)
- Python 3.12+ required (dropped 3.10/3.11); Django 5.2
PrimaryModel,OrganizationalModel,NestedGroupModelnow in plugins API@register_filtersetdecorator enables UI lookup modifiersOwnerMixinavailable for object ownership- Breaking: GraphQL filter syntax requires lookup modifiers for IDs/enums
NetBox 4.4 (2025-09-02)
- Background job logging:
self.logger.info()in job methods ObjectActionfor custom individual/bulk operations on viewsregister_model_feature()for plugins to register custom model featuresArrayColumnsupport in tables