Development conventions
Use these conventions when changing the plugin. For environment setup and the contribution process, see Contributing. The NetBox internals reference describes the framework contracts that need particular care during upgrades.
Code organization
Keep implementation in netbox_scripts/ and use the existing topic packages.
Forms are grouped by type, then topic. Add related code to those modules rather
than introducing a new layer for a small change. Discuss larger reorganizations
in the issue before starting.
| Area | Location |
|---|---|
| Models and their exports | models/<topic>.py and models/__init__.py |
| Serializers | api/serializers/<topic>.py |
| UI views and templates | views/<topic>.py and templates/netbox_scripts/ |
| Detail panels | ui/panels.py |
| Object actions | object_actions.py, with templates in templates/netbox_scripts/buttons/ |
| Cross-model signal handlers | signals.py, registered during startup |
| Search registrations | search.py |
| Cross-model template extensions | template_content.py, when needed |
| Tests | tests/, mirroring the implementation layout |
Add exports where the package exposes them and keep __all__ alphabetized.
NetBox integration
Use NetBox's existing models, mixins, forms, serializers, filtersets and views where they fit. Keep plugin behavior in the plugin rather than monkey-patching NetBox. Record dependencies on undocumented interfaces in the internals ledger and its compatibility checker.
Register model views with @register_model_view and generate their routes with
get_model_urls(). Workflow views, such as migration passes, can use explicit
routes. Register REST viewsets through NetBoxRouter.
Use SimpleLayout for detail pages, with panels from ui/panels.py. Declare
supported actions on list views, detail views and tables. Removing a route alone
can leave a permission-driven button pointing to it.
Keep the live request off model instances and anything else a queued event or
job payload can carry. NetBox queues the instance itself in each object event,
and an EVENTS_PIPELINE consumer may pickle it. Read
netbox.context.current_request where a form needs the acting user.
scripts/check_cloud_compat.py flags a stored request, and the test
configuration pickles each queued event's object.
URLs, filters and permissions
URL segments should not repeat the plugin name. Use projects/ and
script-files/ beneath the configured base_url, not netbox-script-files/.
Reverse names come from the model, not the segment, so renaming a segment
changes no reverse() call.
Declare each foreign-key filter explicitly as
<field>_id = ModelMultipleChoiceFilter(field_name='<field>', ...) rather than
relying on Meta.fields to generate it. Set the filter form's model to the
model it filters, not to a related parent. Follow the existing
db_collation='natural_sort' pattern for code and identifier fields.
Permission checks use the netbox_scripts namespace. In Meta.permissions,
declare the bare action, such as run, not run_netboxscript. NetBox composes
the model-specific action when working with Object Permissions. See
Permissions for the actions and object constraints.
For object-scoped authorization, use has_perm(..., obj=...) or a restricted
queryset. An objectless permission check does not establish access to the
particular row being changed.
GraphQL filter inputs use typed enums for choice fields. Object types expose the stored strings.
Model changes
Preserve each model's documented identity and field-ownership rules. For
ScriptProject, key, source_type and storage_key are immutable after
creation. Normalize data_path with the shared validator.
QuerySet.update() and raw SQL bypass model guards. Code using either must
preserve canonical values and the relevant invariants itself. Consult the
Project reference before changing those
write paths.
Database migrations
Include the migration with its model change and tests. Model-state changes need a migration even when they produce no SQL. Discuss destructive changes, nullability changes and large-table indexes before implementing them.
Initial schema
The first beta can regenerate the migration history into one 0001_initial.py,
and it ships no upgrade path from an alpha. The alpha release notes say so.
Coordinate regeneration with a maintainer and agree how to handle development
databases that have already applied it. Do not assume those databases can be
discarded.
Apart from that one reset, do not regenerate migration history already distributed in a release. Schema changes after publication need an upgrade path from the published migrations.
NetBox dependency pins
The initial migration deliberately uses these NetBox v4.6.0 dependency heads, even though the plugin's supported runtime floor is newer:
dependencies = [
('core', '0024_job_notifications'),
('extras', '0138_customfieldchoiceset_choice_colors'),
('users', '0016_default_ordering_indexes'),
]
Review generated dependencies rather than accepting the heads selected by a newer development checkout. Preserve these entries when regenerating the initial migration. They describe schema dependencies, not the plugin's supported NetBox version range.
Resolve inherited-field differences rather than accepting a persistent
makemigrations --check failure.
Data migrations
Use historical models through apps.get_model() rather than importing current
model classes. Keep data migrations fast and safe to retry. Required
ContentType rows may not exist before post_migrate runs, so obtain or create
them explicitly with the historical model's get_or_create().
Testing
Use Django's test runner and real models for database behavior. Exercise views
and APIs through NetBox's test client rather than mocking persistence. Use
transactional tests for concurrency and transaction boundaries. A sequential
TestCase alone does not demonstrate that an interleaving is safe.
Keep tests in the same commit as the implementation they cover. Report the NetBox ref, command, configuration and result, including skipped or unrun checks. Distinguish actual integration tests from checks that replace a framework or persistence boundary. Contributing covers setup and test-service isolation.
Query-count baselines
netbox_scripts/tests/query_counts.json tracks the newest pinned NetBox release
in .github/workflows/test.yml. Investigate changed counts before updating it.
Compare the same test on an unchanged plugin tree to distinguish a plugin
regression from an upstream change.
For an intentional update, select that exact pinned release, run the suite
serially with UPDATE_QUERY_COUNTS=1, and review the JSON diff. Do not combine
this with --parallel or regenerate against the moving main or feature
branches.
Code style and documentation
Prefer readable code and NetBox's established patterns. Ruff's configuration
lives in pyproject.toml, including 120-character lines, single quotes and LF
line endings. Do not add a separate ruff.toml.
Hook configuration belongs in .pre-commit-config.yaml. Keep tool settings in
pyproject.toml where the tool supports it.
Docstrings describe the caller's contract. Use one line when it is enough, and add detail for return values, exceptions or required transaction and resource scope. Explain a non-obvious line beside that line. Put cross-cutting rationale in the relevant documentation page. A docstring that only repeats its identifier needs a useful description instead.
Avoid section-banner comments. Preserve the syntax of cloud-compat: ok markers
because the compatibility checker reads them. An exemption needs a reason, not
just a way to silence a check.
Write documentation for its audience and keep warnings explicit. Put setup instructions in the contribution guide, operational behavior in the relevant user or administrator page, and integration details in the internals reference. Link to shared explanations instead of maintaining competing copies.
Compatibility
Breaking API changes require at least one minor release of deprecation warning before removal. Discuss incompatible changes first rather than assuming the alpha label waives that rule.
When changing NetBox support, update the plugin's version bounds,
COMPATIBILITY.md and CI refs together. Check whether the Python requirement also
needs to change, and run the relevant tests and internals checks against the
new target.
Preserve applicable third-party notices and follow the contribution licensing guidance. Review licensing and repository-specific changes before regenerating files from a scaffold.