mcpField
N
netbox-custom-objects

netbox-custom-objects

github.com/netboxlabsData & databases2151 installsUpdated 2w ago
Repository

About netbox-custom-objects

netbox-custom-objects is a reference skill for extending NetBox's data model without writing code. It explains how to define typed object types and fields, manage instances, and use REST, GraphQL, and portable schemas.

Key features

  1. 01Define custom object types and fields
  2. 02Validate values and set relationships
  3. 03Manage instances through REST
  4. 04Read custom objects with GraphQL
  5. 05Preview and apply portable schemas

Use cases

  1. 01Model DHCP scopes or contracts in NetBox
  2. 02Build integrations that exchange custom objects
  3. 03Provision object schemas in CI/CD
  4. 04Query objects linked to NetBox records

MCP support

The skill notes that a configured NetBox MCP server can discover existing custom object types and instances, while NetBox Platform MCP can manage types and instances. It does not itself expose MCP tools.

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/netboxlabs/skills --skill netbox-custom-objects

After installing, type/netbox-custom-objectsin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
api-workflow.md
field-type-guide.md
graphql-and-branching.md
modeling-patterns.md

Details

From the project README

NetBox Custom Objects

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

SourceURL / MethodUse for
Custom Objects docshttps://netboxlabs.com/docs/custom-objects/Overview, workflow, config context, config templates
Field attributeshttps://netboxlabs.com/docs/custom-objects/field-attributes/Per-type attributes
REST APIhttps://netboxlabs.com/docs/custom-objects/rest-api/Endpoints, payloads
GraphQLhttps://netboxlabs.com/docs/custom-objects/graphql/Query field naming, type mapping
Branchinghttps://netboxlabs.com/docs/custom-objects/branching/Version gate, branch behavior
Portable schemahttps://netboxlabs.com/docs/custom-objects/portable-schema/Export/preview/apply
Release noteshttps://github.com/netboxlabs/netbox-custom-objects/blob/main/docs/releases.mdWhat changed in the installed version
NetBox MCP serverIf configured — list existing custom object types and instancesSchema discovery
NetBox Platform MCPIf configured — full CRUD on custom objectsCreate 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
ConceptDescription
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 ObjectAn instance of a COT — like a row in the table.
Primary fieldThe 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 fieldA COTF with context: true — its value is shown alongside the object wherever another object references it (and in the REST _context key).
Group nameGroups related COTs in the navigation sidebar (on COT) or fields in forms (on COTF).
API Endpoints
EndpointPurpose
/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)
TypeDescriptionType-specific options
textShort textvalidation_regex
longtextMulti-line text (textarea)validation_regex
integerWhole number, 64-bit (v0.6+)validation_minimum, validation_maximum
decimalDecimal numbervalidation_minimum, validation_maximum
booleanTrue/false— (no unique)
dateISO 8601 date (YYYY-MM-DD)—
datetimeISO 8601 datetime—
urlURL plus optional link title (v0.7+: REST exposes <name> and <name>_title)validation_regex
jsonArbitrary JSON—
selectSingle choice from a choice setchoice_set (required)
multiselectMultiple choices from a choice setchoice_set (required)
objectReference to one objectapp_label+model or is_polymorphic+related_object_types_input; related_object_filter, related_name, on_delete_behavior
multiobjectReference to many objectssame 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 type value is coordinates.

What changed since v0.5.1
VersionChange
0.5.2related_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.0Branching: 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.1display_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:

PropertyNotes
nameInternal name: ^[a-z0-9]+(_[a-z0-9]+)*$ — lowercase, underscores, no leading/trailing/double underscores. Unique. Used by config templates.
slugURL-safe identifier. Used in the REST path, GraphQL field name, CUSTOM_VALIDATORS key, and cross-COT references. Unique.
verbose_name / verbose_name_pluralDisplay names. Default to title-cased name / verbose_name + "s".
group_nameOptional. Groups this type with others in the sidebar.
versionOptional 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:

PropertyPurposeNotes
primaryUse this field's value as the object's display nameOne per type; display_expression on the COT overrides it
contextShow this value as extra context wherever the object is referencedAppears in REST as _context; surfaced in global search (v0.7+)
requiredField must have a valueEnforced on create and update (API and UI)
uniqueValue must be unique across all instancesNot allowed on boolean, multiobject, coordinates
weightDisplay ordering (lower = higher)Default: 100
group_nameVisual grouping in formsFields with same group_name appear together
search_weightSearch ranking (100=high, 500=medium, 1000=low, 0=excluded)Default: 500
filter_logicloose (substring), exact, disabledDefault: loose; affects the REST filter for text-like fields
ui_visible / ui_editablealways/if-set/hidden and yes/no/hiddenHidden fields are still writable via the API
defaultDefault value, stored as JSONREST: 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_cloneableInclude when cloning objectsDefault: false
deprecated, deprecated_since, scheduled_removalMark a field read-only during a migration grace periodPEP 440 strings; consumed by portable schema
schema_idRead-only stable id used by portable schemaAuto-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, or protect — what happens to the custom object when the referenced object is deleted. object fields only; ignored on multiobject.
  • 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/model are 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 to icontains; set filter_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> and custom_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.
  • coordinates fields 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.

PluginCOT / field changes on a branchInstance writes on a branchSchema apply on a branch
v0.6+Branch-aware: tracked, merged, revertible (field adds/renames/deletes, type deletes)Allowed and tracked like any NetBox objectAllowed; DDL lands in the branch schema
v0.4–v0.5Written straight to main, invisible in the branch diffRejectedBlocked

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 gain local_context_data (REST-writable JSON) and a Config Context tab. Source ConfigContexts are aggregated by field-naming convention: a non-polymorphic object field named site, tenant, role, platform, location, device_type, or cluster pointing 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

  1. Deletion is destructive: deleting a COT drops the table; deleting a COTF drops the column. Irreversible.
  2. No bulk create/update/delete on the instance API — POSTing an array fails; iterate per object (or use the UI bulk import/edit).
  3. Validation is enforced on API writes (v0.5+): required, validation_regex, validation_minimum/maximum, plus CUSTOM_VALIDATORS keyed netbox_custom_objects.<cot-slug>.
  4. Max types: 50 by default (max_custom_object_types; 0/None disables).
  5. 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.
  6. Uniqueness cannot be enforced on boolean, multiobject, or coordinates fields.
  7. Name format: COT and COTF names match ^[a-z0-9]+(_[a-z0-9]+)*$.
  8. Immutable after creation: is_polymorphic + allowed types; config_context_enabled; a field's type cannot change to/from multi-column types (coordinates, url).
  9. CSV import populates only the URL value and cannot set <name>_title or coordinates backing columns.
  10. 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-patternWhy it failsDo this instead
"type": "location" for a lat/long fieldNot 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 RESTdefault 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 arraysLoop per object; expect 201 each
Assuming instance writes on a branch are rejectedTrue only for v0.4–v0.5On v0.6+ they are tracked in the branch; check /api/status/ plugin version
Copying the exempt_models snippet onto a v0.6+ installSplits schema (main) from instances (branch); v0.6+ tests run without itLeave COT/COTF unexempted unless your version's docs say otherwise
GraphQL dhcp_scopes_list / dhcpScopesListRoot fields are prefixed and snake_casecustom_objects_dhcp_scopes_list
Sending GraphQL mutations or filters:Not in the 0.7.0 schemaREST for writes and server-side filters
display_expression that references a field by label or with a typoUndefined names render empty; render errors fall back silently to the primary field; API does no syntax checkReference field names; guard with {% if field %}; test in the UI form
Config template custom_objects.dhcp-scopesResolved by internal name, and hyphens aren't valid Jinjacustom_objects.dhcp_scope (or custom_objects['123foo'])
PATCHing config_context_enabledCreate-onlyRecreate the type, or leave as-is
on_delete_behavior on a multiobject fieldIgnoredOnly meaningful on object fields
Naming a field owner, contacts, or local_context_dataReserved (v0.6+ added these)Pick another name (owning_team, …)
CUSTOM_VALIDATORS['netbox_custom_objects.dhcp_scope']Key is the slugnetbox_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_typeAttribute access converts _ to -nb.plugins.custom_objects.endpoint("my_type")
Relying on GraphiQL autocomplete after adding a fieldExplorer caches the schema per page loadClick "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/url multi-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

Resources

Comments

Markdown supported