Your knowledge of Custom Objects may be outdated. Field types, relationship options, branching behavior, and API surface change between plugin releases (0.5 → 0.7 added polymorphic fields, portable schema, branching, GraphQL, config context,
coordinates,display_expression). Prefer retrieval over pre-trained knowledge.
Retrieval Sources
| Source | URL / Method | Use for |
|---|---|---|
| Custom Objects docs | https://netboxlabs.com/docs/custom-objects/ |
Overview, workflow, config context, config templates |
| Field attributes | https://netboxlabs.com/docs/custom-objects/field-attributes/ |
Per-type attributes |
| REST API | https://netboxlabs.com/docs/custom-objects/rest-api/ |
Endpoints, payloads |
| GraphQL | https://netboxlabs.com/docs/custom-objects/graphql/ |
Query field naming, type mapping |
| Branching | https://netboxlabs.com/docs/custom-objects/branching/ |
Version gate, branch behavior |
| Portable schema | https://netboxlabs.com/docs/custom-objects/portable-schema/ |
Export/preview/apply |
| Release notes | https://github.com/netboxlabs/netbox-custom-objects/blob/main/docs/releases.md |
What changed in the installed version |
| NetBox MCP server | If configured — list existing custom object types and instances | Schema discovery |
| NetBox Platform MCP | If configured — full CRUD on custom objects | Create types, manage instances |
FIRST: Verify Connectivity and Version
# Plugin installed? (404 = not installed; 403 = token lacks custom_objects permissions)
curl -s -H "Authorization: Bearer $NETBOX_TOKEN" "$NETBOX_URL/api/plugins/custom-objects/custom-object-types/" | python -m json.tool
# Which plugin version? (core /api/status/ lists installed plugins and versions)
curl -s -H "Authorization: Bearer $NETBOX_TOKEN" "$NETBOX_URL/api/status/" | python -c "import sys,json; d=json.load(sys.stdin); print(d['netbox-version'], d['plugins'].get('netbox_custom_objects'))"
Version note: this skill targets plugin v0.7.x (latest v0.7.0, 2026-09-15), which supports NetBox 4.5.2 through 4.7.x. Behaviors that differ by plugin version are tagged inline (
v0.5+,v0.6+,v0.6.1+,v0.7+); features that need NetBox 4.7 are tagged NetBox 4.7+. Always check the installed version first — the branching model changed completely in 0.6.0.
Extend the NetBox data model without writing code. Custom Objects let administrators define new object types with typed fields, validation rules, and relationships — through the UI, REST API, GraphQL (read), or a portable schema document.
When to Use This Skill
- Creating new object types in NetBox (e.g., DHCP Scopes, Contracts, Applications)
- Adding fields to custom object types (text, numeric, object references, coordinates, etc.)
- Building integrations that create or consume custom objects via REST, GraphQL, or pynetbox
- Automating custom object type provisioning (portable schema export/preview/apply) in CI/CD
- Querying linked objects (reverse lookups from core objects to custom objects)
- Using custom objects in branches, config contexts, or device config templates
Quick Reference
Core Concepts
| Concept | Description |
|---|---|
| Custom Object Type (COT) | A new model definition — like a database table. Has name (internal), slug (URL/API path), display names, fields. |
| Custom Object Type Field (COTF) | A column on a COT — typed, with validation, ordering, grouping. |
| Custom Object | An instance of a COT — like a row in the table. |
| Primary field | The COTF whose value becomes the object's display name (default name is <Type> <id>). |
| Display expression (v0.6.1+) | Jinja2 template on the COT (display_expression) that composes the display name from several fields; overrides the primary field when set. |
| Context field | A COTF with context: true — its value is shown alongside the object wherever another object references it (and in the REST _context key). |
| Group name | Groups related COTs in the navigation sidebar (on COT) or fields in forms (on COTF). |
API Endpoints
| Endpoint | Purpose |
|---|---|
/api/plugins/custom-objects/custom-object-types/ |
CRUD on type definitions (?slug= filter works, v0.5.2+) |
/api/plugins/custom-objects/custom-object-type-fields/ |
CRUD on field definitions |
/api/plugins/custom-objects/{slug}/ |
CRUD on instances (one endpoint per type, keyed by slug) |
/api/plugins/custom-objects/linked-objects/ |
Reverse lookups: which custom objects reference a given object |
/api/plugins/custom-objects/schema/preview/ |
Diff a portable schema document against the live DB (read-only) |
/api/plugins/custom-objects/schema/apply/ |
Apply a portable schema document (atomic; needs add+change on COT) |
/graphql/ (v0.6+) |
Read custom objects as custom_objects_<slug> / custom_objects_<slug>_list |
Field Types (14)
| Type | Description | Type-specific options |
|---|---|---|
text |
Short text | validation_regex |
longtext |
Multi-line text (textarea) | validation_regex |
integer |
Whole number, 64-bit (v0.6+) | validation_minimum, validation_maximum |
decimal |
Decimal number | validation_minimum, validation_maximum |
boolean |
True/false | — (no unique) |
date |
ISO 8601 date (YYYY-MM-DD) |
— |
datetime |
ISO 8601 datetime | — |
url |
URL plus optional link title (v0.7+: REST exposes <name> and <name>_title) |
validation_regex |
json |
Arbitrary JSON | — |
select |
Single choice from a choice set | choice_set (required) |
multiselect |
Multiple choices from a choice set | choice_set (required) |
object |
Reference to one object | app_label+model or is_polymorphic+related_object_types_input; related_object_filter, related_name, on_delete_behavior |
multiobject |
Reference to many objects | same as object minus on_delete_behavior; no unique |
coordinates (v0.6+) |
Latitude/longitude pair; REST exposes <name>_latitude and <name>_longitude (both-or-neither) |
— (no unique, no default) |
The release notes call the coordinates type "location"; the API
typevalue iscoordinates.
What changed since v0.5.1
| Version | Change |
|---|---|
| 0.5.2 | related_name works on polymorphic fields (reverse accessor); ?slug= filter on COT list; tags on POST/PATCH now persist; select fields display labels not raw keys; null accepted for optional object fields |
| 0.6.0 | Branching: COTs, fields, and instances are branch-aware (gated: NetBox ≥ 4.6.2, netbox-branching ≥ 1.0.4); GraphQL read support; config context support (config_context_enabled); Quick Add on object/multiobject form fields; contacts; ownership (owner); 64-bit integers; coordinates type |
| 0.6.1 | display_expression; NetBox 4.7+ config-template access (custom_objects.<name>, '<name>' | custom_objects); bulk-edit "Set null"; ?id= and lookup suffixes (__gt, __ic, …) fixed on instance filters; NetBox 4.7 support |
| 0.7.0 | "Custom Objects" related tab on every referenced object (permission-filtered); URL link title; schema preview/apply accept and return YAML; context fields shown in global search |
Creating a Custom Object Type
Step 1: Create the Type
POST /api/plugins/custom-objects/custom-object-types/
{
"name": "dhcp_scope",
"slug": "dhcp-scopes",
"description": "DHCP address scopes",
"verbose_name": "DHCP Scope",
"verbose_name_plural": "DHCP Scopes",
"group_name": "IPAM Extensions",
"version": "1.0.0",
"display_expression": "{{ scope_name }}{% if vlan %} (VLAN {{ vlan }}){% endif %}",
"config_context_enabled": false
}
Type properties:
| Property | Notes |
|---|---|
name |
Internal name: ^[a-z0-9]+(_[a-z0-9]+)*$ — lowercase, underscores, no leading/trailing/double underscores. Unique. Used by config templates. |
slug |
URL-safe identifier. Used in the REST path, GraphQL field name, CUSTOM_VALIDATORS key, and cross-COT references. Unique. |
verbose_name / verbose_name_plural |
Display names. Default to title-cased name / verbose_name + "s". |
group_name |
Optional. Groups this type with others in the sidebar. |
version |
Optional PEP 440 string; used by the portable schema feature. |
display_expression (v0.6.1+) |
Optional Jinja2 template rendered with field values by name. Undefined fields render as empty strings; a render error silently falls back to the primary field. The UI form validates Jinja syntax; the REST API does not. |
config_context_enabled (v0.6+) |
Create-only. Adds local_context_data to every instance and a Config Context tab. Cannot be toggled later (API returns a validation error). |
Step 2: Add Fields
POST /api/plugins/custom-objects/custom-object-type-fields/
{
"custom_object_type": 1,
"name": "scope_name",
"label": "Scope Name",
"type": "text",
"required": true,
"unique": true,
"primary": true,
"validation_regex": "^[a-zA-Z0-9_-]+$"
}
Key field properties:
| Property | Purpose | Notes |
|---|---|---|
primary |
Use this field's value as the object's display name | One per type; display_expression on the COT overrides it |
context |
Show this value as extra context wherever the object is referenced | Appears in REST as _context; surfaced in global search (v0.7+) |
required |
Field must have a value | Enforced on create and update (API and UI) |
unique |
Value must be unique across all instances | Not allowed on boolean, multiobject, coordinates |
weight |
Display ordering (lower = higher) | Default: 100 |
group_name |
Visual grouping in forms | Fields with same group_name appear together |
search_weight |
Search ranking (100=high, 500=medium, 1000=low, 0=excluded) | Default: 500 |
filter_logic |
loose (substring), exact, disabled |
Default: loose; affects the REST filter for text-like fields |
ui_visible / ui_editable |
always/if-set/hidden and yes/no/hidden |
Hidden fields are still writable via the API |
default |
Default value, stored as JSON | REST: send the JSON value directly ("default": "active", "default": 86400, "default": true). UI form: type a JSON literal (strings need quotes). Not allowed on coordinates. |
is_cloneable |
Include when cloning objects | Default: false |
deprecated, deprecated_since, scheduled_removal |
Mark a field read-only during a migration grace period | PEP 440 strings; consumed by portable schema |
schema_id |
Read-only stable id used by portable schema | Auto-assigned |
Step 3: Create Instances
POST /api/plugins/custom-objects/dhcp-scopes/
{
"scope_name": "office-floor-2",
"ip_range": 5,
"vlan": 100,
"tags": [{"name": "production"}],
"owner": 3
}
The endpoint path uses the COT's slug (not name). Every instance also carries id, url, display, owner (v0.6+, NetBox ownership — omit if unused), tags, created, last_updated, plus _context when the type has context fields and local_context_data when config context is enabled.
Object Reference Fields
Reference core NetBox objects, plugin models, or other custom objects using object and multiobject fields.
Referencing Core Objects
{
"custom_object_type": 1,
"name": "site",
"label": "Site",
"type": "object",
"app_label": "dcim",
"model": "site",
"related_name": "dhcp_scopes",
"on_delete_behavior": "protect"
}
Common app_label.model combinations: dcim.device, dcim.site, dcim.rack, dcim.interface, ipam.prefix, ipam.ipaddress, ipam.vlan, ipam.iprange, tenancy.tenant, circuits.circuit, virtualization.virtualmachine.
on_delete_behavior(v0.5+):set_null(default),cascade, orprotect— what happens to the custom object when the referenced object is deleted.objectfields only; ignored onmultiobject.related_name: reverse accessor on the target (e.g.site.dhcp_scopes.all()in export/config templates). Works on polymorphic fields too (v0.5.2+). Must be unique per target type.- The response echoes
related_object_type(read-only nested{id, app_label, model});app_label/modelare write-only.
Referencing Other Custom Objects
Set app_label to "custom-objects" and model to the target COT's slug:
{
"custom_object_type": 2,
"name": "parent_scope",
"label": "Parent Scope",
"type": "object",
"app_label": "custom-objects",
"model": "dhcp-scopes"
}
Self-referential fields (a COT pointing to itself) are allowed. Circular references between different COTs are detected and rejected with a 400 — including through polymorphic fields (v0.5.2+).
Polymorphic Reference Fields
An object/multiobject field can reference multiple object types (v0.5+). Set is_polymorphic: true and list allowed types in related_object_types_input instead of app_label/model:
{
"custom_object_type": 9,
"name": "linked_resource",
"label": "Linked Resource",
"type": "object",
"is_polymorphic": true,
"related_object_types_input": [
{"app_label": "dcim", "model": "device"},
{"app_label": "custom-objects", "model": "servers"}
]
}
Write an instance value as a dict: {"app_label": "dcim", "model": "device", "object_id": 7} (content_type_id and id are accepted aliases, so a read representation round-trips). For polymorphic multiobject, pass a list of such dicts. is_polymorphic and the allowed types are immutable after creation — delete and recreate the field to change them.
Filtering Object Selections
Narrow the UI drop-down with related_object_filter (a query_params dict):
{"type": "object", "app_label": "dcim", "model": "device", "related_object_filter": {"status": "active"}}
(v0.6+) Object and multiobject form fields support NetBox Quick Add, so users can create the referenced object inline.
Linked Objects (Reverse Lookups)
Find all custom objects that reference a given object (core or custom):
GET /api/plugins/custom-objects/linked-objects/?object_type=dcim.device&object_id=42
{
"count": 1,
"results": [
{
"custom_object_type": {"id": 1, "name": "server", "slug": "servers"},
"field_name": "primary_device",
"object": {"id": 7, "display": "web-server-01"}
}
]
}
Both parameters are required. In the UI (v0.7+), every referenced object gets a Custom Objects tab listing the same relationships across all types, with search, type/tag filters, and a count badge — filtered to the viewer's per-type view permissions (the tab hides when there is nothing they may see). max_multiobject_display (plugin config, default 3) caps how many multiobject targets show per row.
Filtering Instances
GET /api/plugins/custom-objects/dhcp-scopes/?scope_name=office # text: substring (filter_logic=loose)
GET /api/plugins/custom-objects/dhcp-scopes/?lease_time__gte=3600 # numeric/date lookups (v0.6.1+)
GET /api/plugins/custom-objects/dhcp-scopes/?site=5&site_id=5 # object fields: either form
GET /api/plugins/custom-objects/dhcp-scopes/?target_dcim_device=7 # polymorphic: <field>_<app>_<model>
GET /api/plugins/custom-objects/dhcp-scopes/?id=1&id=2&tag=production&q=office
- Text-like fields (
text,longtext,url,json) default toicontains; setfilter_logic: "exact"to get exact matching plus NetBox's suffix lookups (__ic,__isw,__n, …).filter_logic: "disabled"removes the filter. - Standard NetBox params (
q,tag,created__gte,limit/offset,ordering) apply. See/api/schema/swagger-ui/for the generated filter list per type.
GraphQL (v0.6+, read-only)
query {
custom_objects_dhcp_scopes_list { id display scope_name site { id name region { name } } }
custom_objects_dhcp_scopes(id: 42) { id display }
}
- Root fields are
custom_objects_<slug>andcustom_objects_<slug>_list, with non-identifier characters in the slug replaced by_. Bare slug names (dhcp_scopes_list) do not exist. - Relationship fields resolve to the target's native NetBox GraphQL type (traversable); polymorphic fields are unions — use
... on DeviceType { }fragments. coordinatesfields and URL titles are not exposed in GraphQL; use REST.- Mutations and server-side field filters are not present in the 0.7.0 schema — write and filter via REST.
- Details: references/graphql-and-branching.md.
Branching
The model flipped in v0.6.0. Check the installed plugin version before advising.
| Plugin | COT / field changes on a branch | Instance writes on a branch | Schema apply on a branch |
|---|---|---|---|
| v0.6+ | Branch-aware: tracked, merged, revertible (field adds/renames/deletes, type deletes) | Allowed and tracked like any NetBox object | Allowed; DDL lands in the branch schema |
| v0.4–v0.5 | Written straight to main, invisible in the branch diff | Rejected | Blocked |
Version gate (v0.6+): when netbox_branching is in PLUGINS, the plugin requires NetBox ≥ 4.6.2 and netbox-branching ≥ 1.0.4; otherwise startup fails with system check netbox_custom_objects.E001 / E002. Without branching installed, the normal compatibility matrix (NetBox 4.5.2–4.7.x) applies. Install with pip install "netboxlabs-netbox-custom-objects[branching]" to pull the branching floor. On NetBox 4.7 use netbox-branching 1.2.x; on 4.4–4.6 use 1.1.x (see netbox-branching).
exempt_models warning: the published branching doc page still shows the pre-0.6 PLUGINS_CONFIG['netbox_branching']['exempt_models'] snippet (exempting customobjecttype / customobjecttypefield) and the "instance writes are disallowed" text. The v0.6+ integration is tested with no exemptions. Exempting the schema models on v0.6+ routes type/field changes to main while instances live in the branch — do not carry the snippet forward unless the release notes for your version require it. Verify against the running version.
More: references/graphql-and-branching.md.
Config Context and Config Templates
- Config context (v0.6+): create the COT with
config_context_enabled: true. Instances gainlocal_context_data(REST-writable JSON) and a Config Context tab. Source ConfigContexts are aggregated by field-naming convention: a non-polymorphicobjectfield namedsite,tenant,role,platform,location,device_type, orclusterpointing at the matching model feeds that dimension. A type with none of these fields gets only its local data (global contexts are not applied). - Config templates (v0.6.1+, NetBox 4.7+): reference custom objects from device/VM config templates by the COT's internal
name(not slug):
{% for iface in custom_objects.ospf_interface.filter(device=device) %}
interface {{ iface.name }}
ip ospf area {{ iface.area }}
{% endfor %}
{# equivalent filter form: #}
{% for peer in 'bgp_peer' | custom_objects %}...{% endfor %}
{# names starting with a digit: #} {% for o in custom_objects['123foo'].all() %}...{% endfor %}
Unknown names log a warning and render as an empty, chainable stand-in (no rows, no error). On NetBox < 4.7 the custom_objects variable is simply absent.
Portable Schema (v0.5+)
Export COT definitions as YAML/JSON, version them, and apply to other instances. POST schema/preview/ returns a diff (is_new, has_destructive_changes, field_changes[]); POST schema/apply/ with {"allow_destructive": false, "schema": {...}} applies atomically, returning 409 destructive_changes when column drops are present and not allowed. Both accept Content-Type: application/yaml or application/json (YAML v0.7+) and return JSON unless Accept: application/yaml. Fields match by stable schema_id; deletions need a removed_fields tombstone. Full workflow: references/api-workflow.md.
Programmatic Access (Scripts & Plugins)
Documented pattern for custom scripts and plugins:
from netbox_custom_objects.models import CustomObjectType
DHCPScope = CustomObjectType.objects.get(name="dhcp_scope").get_model()
scope = DHCPScope.objects.filter(scope_name="office-floor-2").first()
The returned class behaves like a standard Django model (filter, create, update, delete, annotate). With pynetbox 7.8+, register CustomObjectsExtension so JSON columns round-trip and per-type endpoints return typed records — example in references/api-workflow.md.
Inherited NetBox Features
Every custom object automatically gets: tags, bookmarks, journaling, change logging, contacts (v0.6+), ownership (v0.6+), list/detail views, import/export, global search (per-field search_weight), event rules and notifications, custom links, cloning, NetBox custom fields pointing at the type, REST filtering/pagination/ETags (NetBox 4.6+), and object permissions.
Important Constraints
- Deletion is destructive: deleting a COT drops the table; deleting a COTF drops the column. Irreversible.
- No bulk create/update/delete on the instance API — POSTing an array fails; iterate per object (or use the UI bulk import/edit).
- Validation is enforced on API writes (v0.5+):
required,validation_regex,validation_minimum/maximum, plusCUSTOM_VALIDATORSkeyednetbox_custom_objects.<cot-slug>. - Max types: 50 by default (
max_custom_object_types;0/Nonedisables). - Reserved field names (32):
id,pk,tags,owner,contacts,local_context_data,created,last_updated,custom_object_type,custom_field_data,bookmarks,journal_entries,model,objects,save,delete,clean,clone,snapshot, … — the API error lists them all. - Uniqueness cannot be enforced on
boolean,multiobject, orcoordinatesfields. - Name format: COT and COTF names match
^[a-z0-9]+(_[a-z0-9]+)*$. - Immutable after creation:
is_polymorphic+ allowed types;config_context_enabled; a field's type cannot change to/from multi-column types (coordinates,url). - CSV import populates only the URL value and cannot set
<name>_titleor coordinates backing columns. - Minimum NetBox: v0.5.x–v0.7.x require NetBox 4.5.2+; v0.6.1+ for 4.7.x. Branching adds the 4.6.2 / 1.0.4 floor.
Anti-Patterns
| Anti-pattern | Why it fails | Do this instead |
|---|---|---|
"type": "location" for a lat/long field |
Not a valid type; the value is coordinates (release notes use the word "location") |
"type": "coordinates", then write <name>_latitude + <name>_longitude together |
"default": "\"active\"" via REST |
default is JSON; the escaped form stores a string containing quotes |
"default": "active" (UI form is where you type the quoted literal) |
POSTing a JSON array to /custom-objects/<slug>/ |
The endpoint does not accept bulk arrays | Loop per object; expect 201 each |
| Assuming instance writes on a branch are rejected | True only for v0.4–v0.5 | On v0.6+ they are tracked in the branch; check /api/status/ plugin version |
Copying the exempt_models snippet onto a v0.6+ install |
Splits schema (main) from instances (branch); v0.6+ tests run without it | Leave COT/COTF unexempted unless your version's docs say otherwise |
GraphQL dhcp_scopes_list / dhcpScopesList |
Root fields are prefixed and snake_case | custom_objects_dhcp_scopes_list |
Sending GraphQL mutations or filters: |
Not in the 0.7.0 schema | REST for writes and server-side filters |
display_expression that references a field by label or with a typo |
Undefined names render empty; render errors fall back silently to the primary field; API does no syntax check | Reference field names; guard with {% if field %}; test in the UI form |
Config template custom_objects.dhcp-scopes |
Resolved by internal name, and hyphens aren't valid Jinja |
custom_objects.dhcp_scope (or custom_objects['123foo']) |
PATCHing config_context_enabled |
Create-only | Recreate the type, or leave as-is |
on_delete_behavior on a multiobject field |
Ignored | Only meaningful on object fields |
Naming a field owner, contacts, or local_context_data |
Reserved (v0.6+ added these) | Pick another name (owning_team, …) |
CUSTOM_VALIDATORS['netbox_custom_objects.dhcp_scope'] |
Key is the slug | netbox_custom_objects.dhcp-scopes |
Referencing another COT with "model": "table3model" |
Internal name; removed in v0.5 | "app_label": "custom-objects", "model": "<target-slug>" |
pynetbox nb.plugins.custom_objects.my_type for slug my_type |
Attribute access converts _ to - |
nb.plugins.custom_objects.endpoint("my_type") |
| Relying on GraphiQL autocomplete after adding a field | Explorer caches the schema per page load | Click "Re-fetch GraphQL schema"; the server is already current |
References
Load on demand:
- references/field-type-guide.md — Load when configuring fields: per-type attributes,
coordinates/urlmulti-column behavior, validation matrix, choice sets. - references/api-workflow.md — Load when building an end-to-end REST integration: full DHCP-scope walkthrough, filtering, error table, portable schema preview/apply, pynetbox extension.
- references/graphql-and-branching.md — Load when reading via GraphQL or running Custom Objects alongside netbox-branching: field naming, type mapping, unions, version gate, what is/isn't branch-aware, config context aggregation rules.
- references/modeling-patterns.md — Load when designing a new type: lookup tables, bridges, hierarchies, display expressions, config-context conventions.
Related Skills
- netbox-data-modeling — Check whether a built-in model already fits before creating a custom object
- netbox-api-integration — Auth, pagination, bulk patterns, pynetbox basics
- netbox-branching — Branch lifecycle, version lines (1.1.x for NetBox ≤ 4.6, 1.2.x for 4.7)
- netbox-plugin-development — When custom objects aren't enough
- netbox-custom-scripts — Using custom objects in scripts
Resources
- GitHub Repository (source-available, NetBox Limited Use License)
- Release Notes
- Compatibility Matrix
- pynetbox Custom Objects extension