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:

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:

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:

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

0-mcp by Stamatios Stamou Jr — github.com/ssjunior/0-mcp