Step-by-step checklist for adding a new field to an existing NetBox model, covering all required touch points (model, migration, validation, serializer, forms, filterset, table, panel/template, search, GraphQL, tests, docs). Use when the user asks to add a field or attribute to an existing model.
Step-by-step checklist for adding a new field to an existing NetBox model, covering all required touch points (model, migration, validation, serializer, forms, filterset, table, panel/template, search, GraphQL, tests, docs). Use when the user asks to add a field or attribute to an existing model.
Adding a Field to an Existing NetBox Model
Adding a field to an existing model touches many files. The scope depends on the field type and how it will be used. Work through the checklist below in order — each section builds on the previous.
Before You Start
Determine upfront:
Field type: scalar (CharField, IntegerField, etc.), FK/M2M, GenericForeignKey, or a special type like JSONField
Nullable/optional? Most new fields should be blank=True, null=True unless there's a strong reason otherwise
Searchable? Should it appear in global search results?
Filterable? Should it be exposed in the FilterSet?
Displayable in list view? Should it be a column in the object table?
Displayable in detail view? Should it appear in the detail panel?
Set DEVELOPER = True in configuration.py if the command is blocked.
For FK fields, also run:
python netbox/manage.py migrate
before continuing, so the DB is in sync for manual testing.
3. Update the API Serializer
The serializer lives under netbox/<app>/api/serializers_/ (note the trailing underscore — it's a directory of submodules star-imported by serializers.py). Find the submodule that owns the model and edit the serializer there.
Simple field: just add the field name to fields in Meta:
classMeta:
fields = [..., 'new_field', ...]
FK field: add a single serializer field with nested=True. NetBox does not use a separate _id companion field — the framework accepts a primary key (or brief object) when writing:
new_field = forms.CharField(required=False)
# or for FK:
related_thing = DynamicModelChoiceField(queryset=..., required=False)
nullable_fields = ('new_field', 'related_thing') # if it can be set to null
Add to fieldsets and Meta.fields here too.
4c. Bulk import form — bulk_import.py
If the field should be importable via CSV, add it to the import form:
4d. Filter form — filtersets.py (the forms version)
The base class should match the model's base (PrimaryModelFilterSetForm, OrganizationalModelFilterSetForm, NestedGroupModelFilterSetForm, or NetBoxModelFilterSetForm). Add the new entries to the existing fieldsets and declare the filter field:
The detail view display is controlled by a panel class (not an HTML template), defined under netbox/<app>/ui/panels.py.
Find the panel for the model and add a new attribute declaration:
from netbox.ui import attrs, panels
classMyModelPanel(panels.ObjectAttributesPanel):
existing_field = attrs.TextAttr('existing_field')
new_field = attrs.TextAttr('new_field') # simple text
related_thing = attrs.RelatedObjectAttr('related_thing', linkify=True) # FK
status = attrs.ChoiceAttr('status') # choice field with badge
is_active = attrs.BooleanAttr('is_active') # boolean
color = attrs.ColorAttr('color') # color swatch
Available attr types (from netbox.ui.attrs):
Class
Use for
TextAttr
Plain text / CharField
NumericAttr
Numbers, optionally with a unit
ChoiceAttr
Choice fields (renders a colored badge)
BooleanAttr
Boolean fields
ColorAttr
Color hex fields
RelatedObjectAttr
Direct ForeignKey
NestedObjectAttr
ForeignKey on a nested/hierarchical model (e.g. region.parent)
RelatedObjectListAttr
ManyToMany or reverse FK list
GenericForeignKeyAttr
GenericForeignKey
DateTimeAttr
DateTimeField
TimezoneAttr
Timezone fields
AddressAttr
Address text (optionally with map link)
TemplatedAttr
Custom per-field HTML template
If the model uses a legacy HTML template (under netbox/templates/<app>/) rather than a declarative panel, add a <tr> row to the relevant <table> in that template instead.
8. Update the SearchIndex (if applicable)
File:netbox/<app>/search.py
If the new field should be indexed for global search, add it to the model's SearchIndex:
@register_searchclassMyModelIndex(SearchIndex):
model = models.MyModel
fields = (
('name', 100),
('new_field', 300), # add here with an appropriate weight
('description', 500),
('comments', 5000),
)
Weight guide: lower = higher search priority. Name fields ~100, short descriptors ~300–500, long-form comments ~5000.
9. Update GraphQL
Filter — graphql/filters.py
Add a filter field to the model's Filter class:
@strawberry_django.filter_type(models.MyModel, lookups=True)classMyModelFilter(PrimaryModelFilter):
# simple field (lookups=True auto-generates eq/icontains/etc.)
new_field: StrFilterLookup[str] | None = strawberry_django.filter_field()
# FK field:
related_thing: Annotated['RelatedThingFilter', strawberry.lazy('<app>.graphql.filters')] | None = strawberry_django.filter_field()
related_thing_id: ID | None = strawberry_django.filter_field()
Type — graphql/types.py
For simple fields, fields='__all__' on the type decorator will pick up the new field automatically. No change needed unless:
The field is in an exclude list on the type — remove it.
The field requires a custom type annotation (e.g. a lazy FK reference or a special scalar):
Add the new field to the model's documentation page. Include:
The field name and description
Valid values (for choice fields)
Any constraints or dependencies
Summary Checklist
#
File(s)
Action
1
models/<module>.py
Add field; add to clone_fields; add clean() validation
2
(user runs)
makemigrations <app> -n <name> --no-header
3
api/serializers_/<module>.py
Add field to fields; for FK use a single Serializer(nested=True) field (no _id companion)
4a
forms/model_forms.py
Add to fieldsets and Meta.fields
4b
forms/bulk_edit.py
Add as optional; add to nullable_fields if nullable
4c
forms/bulk_import.py
Add if CSV-importable
4d
forms/filtersets.py
Add filter field and to fieldsets
5
filtersets.py
Add to FilterSet; add FK + FK_id pair; update search()
6
tables/<module>.py
Add column; add to Meta.fields; update default_columns
7
<app>/ui/panels.py
Add attr to the model's panel class
8
search.py
Add to SearchIndex fields tuple with appropriate weight
9
graphql/filters.py, types.py
Add filter field; update type if excluded or needs custom annotation
10
tests/test_*.py
Update filterset, API, view, and model tests
11
docs/models/<app>/<model>.md
Document the new field
Common Gotchas
FilterSets need explicit _id variants for FK fields — Meta.fields does not auto-generate them. (This is FilterSet-only — API serializers do not add a parallel _id field; see below.)
Serializer FK fields use nested=True, not a parallel _id. Older code that defines both foo = NestedFooSerializer(read_only=True) and foo_id = serializers.PrimaryKeyRelatedField(...) is the legacy pattern; new code uses a single foo = FooSerializer(nested=True, ...) field.
Migrations must be generated, not written manually. If makemigrations is blocked, ensure DEVELOPER = True is set in configuration.py.
List views and API serializers don't need manual prefetch_related() — this is handled dynamically. Only add explicit prefetches in a viewset if required for a custom endpoint.
clone_fields must be declared explicitly on the model. Fields not in this list are not copied when cloning an object.
brief_fields on serializers is explicit — just listing a field in Meta.fields does not include it in brief/nested representations.
Panel attrs, not HTML templates — new models use ObjectAttributesPanel subclasses in <app>/ui/panels.py. Only fall back to editing templates/<app>/ HTML files if the model predates the declarative layout system.
GraphQL fields='__all__' picks up simple new fields automatically; only explicit overrides needed for FKs, excluded fields, or special scalars.
No ruff format on existing files — use ruff check only.
References
Real example (adding FK filter field): git show 87b17ff26 — adds profile/profile_id to the Module filterset, filter form, table, template, and tests
Real example (adding a JSONField): git show 5f802bb18 — adds choice_colors to CustomFieldChoiceSet across model, forms, filterset, serializer, GraphQL, and tests
Panel attrs reference: netbox/netbox/ui/attrs.py
Panel classes: netbox/<app>/ui/panels.py
Base filterset classes: netbox/netbox/filtersets.py