Run Django management commands from the admin — with auto-generated forms, pluggable task runners, and a unified execution log.
Documentation | PyPI | Changelog
@register_commanddecorator — register any management command with metadata- Auto-generated forms — argparse arguments become Django form fields automatically
- Widget & form customisation — override widgets per-argument (
widget=onadd_argument), supply a customFormclass, or use per-parameter field overrides in the decorator - Built-in file fields —
FileOrPathField(upload or server path),FileField,ImageField - Pluggable runners — Django Tasks (default), Celery, sync, or custom
- Execution log — every run is stored as a
CommandExecutionrecord - Stop running commands — a "Stop" button (graceful, via the output
heartbeat) that becomes "Force Stop" (hard kill) while the command keeps
running; Celery uses
revoke(terminate=True), django-q2 signals the task's worker PID. Shown only while the execution is running. - Terminal output — stdout/stderr render in an embedded xterm.js terminal
(vendored, no CDN): progress bars display as in a real terminal. Output is
stored as append-only 512 KB parts, so live updates cost only the new
characters and arrive every 250 ms regardless of output size; a cursor-based
delta endpoint transfers just the delta, sealed parts are served
browser-cacheable (immutable), and retention is capped per field at 500 MB
(
ADMIN_RUNNER_MAX_OUTPUT) by pruning the oldest parts. Terminal size is configurable (ADMIN_RUNNER_TERM_COLS/ADMIN_RUNNER_TERM_ROWS, default 120×40) and exported asCOLUMNS/LINESso output layout is deterministic. - Permission control — per-command permission requirements (superuser, Django perms, or a list)
- Model attachment — show a "Run" button on any model's admin change-list via
models=[...] - Scheduling — interactive "Add schedule" admin UI (cron / interval / one-off)
and declarative
@register_command(schedule=…)schedules, materialized into the backend's native periodic tasks (django-q2; see the scheduling guide) - Unfold support — auto-detected, uses Unfold templates and widgets when available
pip install django-admin-runnerAdd to INSTALLED_APPS:
INSTALLED_APPS = [
...
"django_admin_runner",
]Run migrations:
python manage.py migrate# myapp/management/commands/my_command.py
from django.core.management.base import BaseCommand
from django_admin_runner import register_command
@register_command(group="Maintenance", permission="myapp.change_mymodel")
class Command(BaseCommand):
help = "Does something useful"
def add_arguments(self, parser):
parser.add_argument("--count", type=int, default=10)
parser.add_argument("--dry-run", action="store_true")
def handle(self, *args, **options):
self.stdout.write(f"Running {options['count']} times")Visit /admin/django_admin_runner/commandexecution/commands/ to run your commands.
Override how individual parameters render — right inside add_arguments:
from django_admin_runner import FileOrPathField, register_command
@register_command(group="Import")
class Command(BaseCommand):
def add_arguments(self, parser):
# File upload OR server-side path text field
parser.add_argument("--source", widget=FileOrPathField(), default="data.csv")
# Swap to a textarea
parser.add_argument("--notes", widget=forms.Textarea(attrs={"rows": 3}))
# Image upload (requires Pillow)
parser.add_argument("--photo", widget=forms.ImageField(required=False))Or provide a fully custom form class:
@register_command(form_class=MyImportForm)
class Command(BaseCommand):
...See the Widget & form customisation docs for the full reference.
Registered commands can be scheduled — periodically (cron expression or fixed interval) or once at a specific time. Schedules are backend-abstracted:
| Runner | Schedule kinds |
|---|---|
django-q2 |
cron, interval, one-off |
celery, django (tasks), sync, rq |
not supported (action hidden) |
When the active runner supports scheduling, an Add schedule action appears next to Run on the command pages. The creation form combines the command's argparse-generated parameter form with a schedule section (kind picker with dynamic fields, label, enabled toggle). Existing schedules are edited in a change view with Parameters and Schedule tabs; saving re-validates the kwargs against the command's current definition (stale options surface an error, never run with wrong args) and re-materializes the native backend schedule. Disabling keeps the row but stops runs; deleting removes both. A global Schedules overview lists every schedule with its next run.
from django_admin_runner import CronSchedule, IntervalSchedule, register_command
@register_command(
group="Maintenance",
schedule=[
CronSchedule("0 3 * * *", name="nightly-full"),
IntervalSchedule(15, name="quick-incremental", kwargs={"limit": 10}),
],
)
class Command(BaseCommand):
...The sync materializes declarations as source=code schedule rows
keyed by (command_name, name) (a single declaration defaults its name to
the command name; lists require explicit unique names so reordering never
rewires schedules). The registry wins for the schedule spec, the database
wins for enabled — admins can pause a declarative schedule without a
deploy. Admin-created schedules are never touched by the sync.
RegisteredCommand and source=code schedule rows materialize when the
registry is synced — automatically after migrations (post_migrate) and on
demand via python manage.py admin_runner_sync (manual runs, worker boot,
or re-sync after code changes). AppConfig.ready() never touches the
database, so django.setup() works without one (image builds running
collectstatic) and is safe with gunicorn --preload and pooled
connections.
Consumers that cannot run migrations during deploy can restore the legacy
startup sync with ADMIN_RUNNER_SYNC_ON_READY = True — not recommended
with preloaded workers or pooled connections.
# Install dependencies and pre-commit hooks
just install
# Run tests
just tests
# Run linters
just check
# Serve docs locally
just docsContributions are welcome! Please open an issue or pull request on GitHub.
MIT