Run scripts without manage.py
Cron jobs, ETL pipelines, batch operations, Jupyter notebooks, REPL sessions — any context that needs the Django ORM without serving an HTTP request. One import bootstraps everything.
The pattern
from zeromcp import orm # noqa ← `zeromcp/orm` subpackage; importing it bootstraps Django
from myapp.models import Invoice
invoices = Invoice.objects.filter(status='open')
for inv in invoices:
inv.recalculate()
inv.save()Importing zeromcp.orm runs django.setup() as an import side-effect. The result: settings are loaded, apps are registered, models are ready — no manage.py, no five-line bootstrap copy-pasted to every script, no extra dependencies.
When the same module is imported inside a fully-initialised Django process, django.setup() is a no-op. The same code works as a script and as a Django module.
Configuration
By default, zeromcp.orm reads the standard Django environment variable:
DJANGO_SETTINGS_MODULE=myproject.settings python my_script.py
If unset, it falls back to a project-level default. The default lookup order is:
DJANGO_SETTINGS_MODULEenv var (standard Django)ZEROMCP_DEFAULT_SETTINGSenv var (recommended for project-wide config)settings.settingsas a final fallback
For projects using the settings/settings.py package layout (e.g. the layout this framework grew up with), no configuration is needed. For other layouts, set the project default once:
# .env (loaded by your runtime) ZEROMCP_DEFAULT_SETTINGS=myproject.settings
Every script picks it up automatically.
Common use cases
Cron jobs
# scripts/daily_invoice_close.py
from zeromcp import orm # noqa
from datetime import date
from myapp.models import Invoice
for inv in Invoice.objects.filter(due_date=date.today(), status='open'):
inv.close()
inv.notify()Runs from cron, systemd, or any scheduler. No Django runserver, no HTTP layer.
ETL pipelines
# scripts/import_external_payments.py
from zeromcp import orm # noqa
import csv
from myapp.models import Payment
with open('payments.csv') as fh:
for row in csv.DictReader(fh):
Payment.objects.update_or_create(
external_id=row['id'],
defaults={'amount': row['amount'], 'paid_at': row['date']},
)Jupyter / notebook exploration
# First cell
from zeromcp import orm # noqa
from myapp.models import User
# Now query freely
users = User.objects.filter(active=True).values('email', 'plan')REPL session
$ python >>> from zeromcp import orm >>> from myapp.models import Subscription >>> Subscription.objects.filter(churn_risk__gte=0.8).count() 47
Why not manage.py?
Both work. The difference is friction:
- manage.py requires writing a management command — subclass
BaseCommand, register undermanagement/commands/, defineadd_argumentsandhandle, then invoke aspython manage.py mycommand. Good for stable commands you run regularly. - zeromcp.orm skips that — useful for one-off scripts, exploratory work, ad-hoc operational fixes, and notebooks where management commands are overkill.
Use both. Management commands for stable interfaces. zeromcp.orm for everything else.
Things to keep in mind
Import order matters
from zeromcp import orm runs django.setup() as a side-effect. That call has to happen before you import any Django model. So this works:
import os, csv # stdlib — fine from zeromcp import orm # noqa ← bootstraps Django from myapp.models import Invoice # ← models OK now
And this breaks (model imported before bootstrap):
from myapp.models import Invoice # ← AppRegistryNotReady error from zeromcp import orm # too late
Cold-start cost
Each script run pays the full Django startup once: load settings, register every INSTALLED_APPS, build the app registry. On a typical project that's 100–500 ms.
For most scripts (cron jobs, ETL, ad-hoc fixes) this is invisible. Where it adds up:
- Cron jobs running every minute or faster — startup time becomes a meaningful slice of total runtime.
- Serverless / Lambda — every invocation cold-starts. Consider keeping warm instances or batching work.
This is Django behaviour, not specific to zeromcp.orm — manage.py runscript pays the same cost.
Be explicit in production
The settings.settings fallback is convenient during local development. In production you usually want to be able to read a script and know exactly which settings module loaded — so always set one of:
DJANGO_SETTINGS_MODULE=myproject.settings.production # or ZEROMCP_DEFAULT_SETTINGS=myproject.settings.production
Set it in the systemd unit, the cron entry, the Dockerfile, or the .env the runtime loads — wherever your other secrets live.
See also
- Quickstart — bring up your first resource
- Multi-tenant — switch tenants programmatically inside scripts
0-mcp by Stamatios Stamou Jr — github.com/ssjunior/0-mcp