@register_command¶
from django_admin_runner import register_command
@register_command(
name=None,
*,
group=None,
permission="superuser",
params=None,
exclude_params=None,
models=None,
widgets=None,
form_class=None,
display_name=None,
timeout=None,
max_retries=0,
flush_interval=None,
schedule=None,
)
class Command(BaseCommand):
...
Parameters¶
name¶
Command name as it appears in the admin. Defaults to the module's filename
(e.g. import_books for management/commands/import_books.py).
group¶
Group label used to organise the command list. Defaults to the app label.
permission¶
Controls who can see and run the command.
| Value | Behaviour |
|---|---|
"superuser" (default) |
Only Django superusers |
"myapp.change_mymodel" |
Users with that Django permission |
["perm.a", "perm.b"] |
Users who hold all listed permissions (AND) |
Superusers always bypass permission checks regardless of this setting.
params¶
Allowlist of argparse dest names. Only these arguments appear in the form.
exclude_params¶
Denylist of argparse dest names. These are excluded from the form.
models¶
List of Django model classes. A Run link for this command is added to each
model's admin change-list page when CommandRunnerModelAdminMixin is used.
widgets¶
Per-parameter widget or field overrides at the decorator level.
Keys are argument dest names. Values can be:
- A
Widgetinstance — keeps the auto-detected field type but replaces its HTML widget. - A
Fieldinstance — replaces the auto-detected field entirely (widget and validation), e.g.FileOrPathField(),forms.FileField(),forms.ImageField().
from django import forms
from django_admin_runner import register_command, FileOrPathField
@register_command(
widgets={
"source": FileOrPathField(), # file upload OR server path
"notes": forms.Textarea(attrs={"rows": 4}), # swap to textarea widget
"photo": forms.ImageField(required=False), # image upload
}
)
class Command(BaseCommand):
...
User-specified widgets are never replaced by the Unfold auto-styling.
Use add_argument(widget=...) inside add_arguments for the highest-priority
override (see Widget customisation).
form_class¶
A fully custom Form class. When set, form_from_command returns it directly
— all auto-generation, params/exclude_params filtering, widgets
overrides, and Unfold widget replacement are bypassed.
from django import forms
from django_admin_runner import register_command
class ImportForm(forms.Form):
source = forms.CharField(label="File path", initial="data.csv")
dry_run = forms.BooleanField(required=False, label="Dry run")
limit = forms.IntegerField(required=False, min_value=0, initial=0)
def clean_limit(self):
v = self.cleaned_data["limit"]
if v < 0:
raise forms.ValidationError("Must be ≥ 0.")
return v
@register_command(form_class=ImportForm)
class Command(BaseCommand):
...
display_name¶
Human-readable name shown in the admin command list. Defaults to the command
name with underscores replaced by spaces and title-cased (e.g. "import_books"
becomes "Import Books").
timeout¶
Maximum execution time in seconds before the task runner kills the task.
When None (default), falls back to the cluster-level timeout setting
(e.g. Q_CLUSTER["timeout"] for django-q2).
django-q2 only
Per-task timeout is supported by the django-q2 runner. Other runners (Celery, Django Tasks, sync) ignore this setting.
max_retries¶
Maximum number of retry attempts on failure. 0 (default) means no retry —
the task runs once and the failure is final.
Per-task retry control depends on the runner's task backend:
| Runner | Per-task retry | Notes |
|---|---|---|
| Celery | Yes | Passes max_retries to apply_async() |
| django-q2 | No | Logs a warning and ignores. Use Q_CLUSTER["max_attempts"] instead |
| Django Tasks | No | Not yet supported by the backend |
| Sync | N/A | Tasks run inline |
@register_command(max_retries=3) # retry up to 3 times (Celery only)
class Command(BaseCommand):
...
django-q2 users
For django-q2, set max_attempts in Q_CLUSTER to control retries
globally. The runner will log a warning if a command has max_retries > 0
to remind you it's being ignored.
flush_interval¶
Output flush cadence in seconds for this command — how often the worker
persists newly written stdout/stderr (and checks the stop flag). Overrides
the global ADMIN_RUNNER_FLUSH_INTERVAL (default 0.5). Lower it for
progress-bar-heavy commands so live output updates more frequently.
schedule¶
Declarative schedule(s) for this command — a single Schedule instance
(e.g. CronSchedule("15 4 * * *")) or a sequence of them. A single
declaration defaults its name to the command name; sequences must declare
explicit, unique names. The sync materializes them as source=code
rows: the registry wins for the spec, the database wins for enabled
(admins can pause without a deploy). See the Scheduling guide.
from django_admin_runner import CronSchedule, IntervalSchedule, register_command
@register_command(
schedule=[
CronSchedule("0 3 * * *", name="nightly-full"),
IntervalSchedule(15, name="quick-incremental", kwargs={"limit": 10}),
],
)
class Command(BaseCommand):
...
Backend support
Scheduling is materialized into the backend's native periodic-task system — currently django-q2 only. On other runners the declaration is inert (rows are still synced, but nothing fires).
Per-argument options in add_arguments¶
hidden=True¶
Pass hidden=True to add_argument to exclude that specific argument from the
form while keeping it in the command's argparse parser.
def add_arguments(self, parser):
parser.add_argument("--public", help="Shown in the form")
parser.add_argument("--internal", hidden=True, help="Not shown in the form")
widget=¶
Pass a Widget or Field instance as widget= to override how a specific
argument is rendered in the form. This is the highest-priority override — it
takes precedence over the widgets dict on the decorator.
from django_admin_runner import FileOrPathField
def add_arguments(self, parser):
# Field instance — replaces the auto-detected field entirely
parser.add_argument(
"--source",
widget=FileOrPathField(),
default="data.csv",
help="Upload a file or enter a server path",
)
# Widget instance — keeps CharField validation, swaps the HTML widget
parser.add_argument(
"--notes",
widget=forms.Textarea(attrs={"rows": 4}),
help="Free-text notes",
)
See Widget customisation for the full priority order and available field types.