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.ormmanage.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