NetBox Custom Scripts
Your knowledge of NetBox custom scripts may be outdated. The Script class API, variable types, and job framework evolve between NetBox releases. Prefer retrieval over pre-trained knowledge.
Retrieval Sources
| Source | URL / Method | Use for |
|---|---|---|
| Custom scripts docs | https://netboxlabs.com/docs/netbox/customization/custom-scripts/ |
Script class, variables, scheduling |
| NetBox repo | https://github.com/netbox-community/netbox |
Script framework source code |
| NetBox MCP server | If configured — verify object state before/after script execution | Testing script output |
FIRST: Verify Environment
Custom scripts run inside NetBox. Confirm your NetBox instance is running and you can access the scripts endpoint:
curl -s -H "Authorization: Bearer $NETBOX_TOKEN" "$NETBOX_URL/api/extras/scripts/" | python -m json.tool
You should see a list of installed scripts. Scripts are loaded from SCRIPTS_ROOT (default: /opt/netbox/netbox/scripts/).
Use this skill when writing or debugging custom scripts that run inside NetBox. For plugin development (new models, views, APIs), see netbox-plugin-development.
Quick Reference
Minimal Script
from extras.scripts import Script, StringVar, ObjectVar
from dcim.models import Site
class HelloWorld(Script):
class Meta:
name = "Hello World"
description = "A minimal example script"
commit_default = False
site = ObjectVar(model=Site)
def run(self, data, commit):
self.log_success(f"Selected site: {data['site']}", data['site'])
return "Done"
Log Methods
| Method | Level | Side Effect |
|---|---|---|
self.log_debug(msg, obj) |
DEBUG | — |
self.log_info(msg, obj) |
INFO | — |
self.log_success(msg, obj) |
SUCCESS | — |
self.log_warning(msg, obj) |
WARNING | — |
self.log_failure(msg, obj) |
FAILURE | Sets self.failed = True |
Both msg and obj are optional. Markdown is supported in messages. If obj has get_absolute_url(), it becomes a clickable link in the UI.
Common Imports
from extras.scripts import Script, AbortScript
from extras.scripts import (
StringVar, TextVar, IntegerVar, BooleanVar, ChoiceVar,
MultiChoiceVar, ObjectVar, MultiObjectVar, FileVar,
IPAddressVar, IPAddressWithMaskVar, IPNetworkVar,
DateVar, DateTimeVar, DecimalVar,
)
from dcim.models import Device, Site, Interface, Rack, Region
from ipam.models import IPAddress, Prefix, VLAN, VRF
from tenancy.models import Tenant
from circuits.models import Circuit
Script Structure
Meta Attributes
| Attribute | Default | Purpose |
|---|---|---|
name |
Class name | Display name in UI |
description |
'' |
Description shown in UI |
commit_default |
True |
Default state of commit checkbox |
scheduling_enabled |
True |
Allow scheduled/recurring execution |
job_timeout |
RQ_DEFAULT_TIMEOUT |
Max runtime in seconds |
field_order |
Declaration order | Tuple of field names |
fieldsets |
None |
Grouped field layout (overrides field_order) |
Fieldsets
Group related fields in the UI:
class Meta:
fieldsets = (
('Source', ('region', 'site')),
('Configuration', ('vlan_id', 'description')),
)
Module-Level Ordering
Control display order when a file has multiple scripts:
script_order = (ScriptA, ScriptB, ScriptC)
Form Variables
All variables support: label, description, default, required (default True), widget.
| Variable | Key Options | Notes |
|---|---|---|
StringVar |
min_length, max_length, regex |
Single-line text |
TextVar |
— | Multi-line textarea |
IntegerVar |
min_value, max_value |
|
BooleanVar |
— | Always optional internally |
ChoiceVar |
choices (list of (value, label)) |
Blank choice auto-added |
MultiChoiceVar |
choices |
Multiple selection |
ObjectVar |
model, query_params, null_option, selector, quick_add |
Dynamic API-backed dropdown. selector (4.5+) shows an advanced object-picker; quick_add (4.6.2+) adds an inline "create new" button |
MultiObjectVar |
Same as ObjectVar |
Multiple objects |
FileVar |
— | File upload (only available during execution) |
IPAddressVar |
— | IPv4/IPv6 without mask |
IPAddressWithMaskVar |
— | IP with prefix (e.g., 192.168.1.1/24) |
IPNetworkVar |
min_prefix_length, max_prefix_length |
Network prefix |
DateVar / DateTimeVar |
— | Date/time pickers |
DecimalVar |
min_value, max_value, max_digits, decimal_places |
See references/script-variables.md for full details.
Dynamic Filtering with ObjectVar
Reference other form fields using $ prefix in query_params:
region = ObjectVar(model=Region)
site = ObjectVar(model=Site, query_params={'region_id': '$region'})
rack = ObjectVar(model=Rack, query_params={'site_id': '$site'})
The UI dynamically filters each dropdown based on the parent selection.
ORM Access & Change Logging
Scripts have full Django ORM access. Three critical rules:
1. Always call full_clean() before save()
device = Device(name='new-device', site=site, role=role, device_type=dtype)
device.full_clean() # Validates model constraints — skipping risks data corruption
device.save()
2. Call snapshot() before modifying existing objects
device = Device.objects.get(name='existing')
device.snapshot() # Required for change log to show a diff
device.status = 'active'
device.full_clean()
device.save()
3. Use _changelog_message for context (optional)
device._changelog_message = 'Bulk status update via maintenance script'
device.save()
See references/orm-patterns.md for query optimization and bulk patterns.
Transaction Handling
All script execution is wrapped in transaction.atomic(). This means:
commit=True: Changes persist if the script completes without errorcommit=False: All DB changes are rolled back afterrun()completes (dry-run mode)- Any exception: All changes are rolled back regardless of commit setting
- Scripts cannot partially commit — it's all or nothing
Clean Abort
from utilities.exceptions import AbortScript
if critical_error:
raise AbortScript("Clear error message")
AbortScript logs the message as a failure and rolls back all changes — no stack trace in the output.
log_failure Does NOT Abort
Calling self.log_failure() sets self.failed = True (shows warning in UI) but does not stop execution. Use AbortScript to actually halt.
Execution Model
Scripts run as background jobs via Django-RQ:
- UI/API submission → job queued to Redis → RQ worker executes
- CLI:
python manage.py runscript --commit --user admin module.ClassName - Scheduling: Set
schedule_atfor future execution,intervalfor recurrence - Timeout: Controlled by
Meta.job_timeoutor globalRQ_DEFAULT_TIMEOUT
NetBox 4.6 background-job changes: completion notifications are disabled for scripts running in the background (4.6) — don't rely on a UI notification to signal a scheduled run finished; check job status instead. NetBox 4.6.2 also prevents duplicate scheduled background jobs (re-submitting an already-queued recurring job no longer stacks duplicates).
See references/execution-model.md for job lifecycle details.
Job States
PENDING → RUNNING → COMPLETED | ERRORED | FAILED
Or SCHEDULED → PENDING → ...
Validation Reports (Test Methods)
Methods named test_* are auto-detected and run as validation checks:
class CablingAudit(Script):
class Meta:
name = "Cabling Audit"
def test_console_connections(self):
for device in Device.objects.filter(status='active'):
ports = ConsolePort.objects.filter(device=device)
for port in ports:
if port.connected_endpoints:
self.log_success(None, port) # Count success without message
else:
self.log_failure(f"No console connection: {port}", device)
If run() is not overridden, NetBox executes pre_run() → all test_* methods → post_run().
NetBox 4.0+: Reports were merged into scripts. Legacy report log signature
(obj, message)still works but is deprecated — use(message, obj).
Anti-Patterns
| Mistake | Consequence | Fix |
|---|---|---|
Skip full_clean() |
Data integrity violations | Always validate before save |
Skip snapshot() |
Change log shows no diff | Call before modifying existing objects |
Expect log_failure to stop |
Script continues running | Use AbortScript to halt |
| Long-running script | Killed by timeout | Set Meta.job_timeout |
Import from netbox.dcim.models |
ImportError | Use from dcim.models import ... |
Assume FileVar persists |
File only available during execution | Process/copy file in run() |
Rely on self.request.user in CLI |
Gets NetBoxFakeRequest |
Handle gracefully |
| Module name conflicts | Import collisions | Avoid naming scripts like installed packages |
Script File Management
- Scripts are Python files in
SCRIPTS_ROOT(default:$INSTALL/netbox/scripts/) - Upload via UI (Admin > Custom Scripts) or place files directly
- Each file becomes a
ScriptModule; classes auto-discovered - Removing a class from a file soft-deletes its DB record if job history exists (
is_executable=False) - Storage backend configurable via Django
STORAGES(supports S3) - Scripts can also be synced from a remote DataSource (git/S3); on NetBox 4.6.2+ remote-source scripts are validated on sync, so a malformed script is caught at sync time rather than first execution