Skip to content

Quickstart - Django

This example assumes an existing Django 5 or 6 application with Celery 5.3 or newer. z4j-celery itself accepts Celery 5.2.2+, but the [celery] extra below also installs z4j-celerybeat, whose Celery floor is 5.3.

Terminal window
pip install z4j-django[celery]

The [celery] extra pulls the engine adapter AND celery-beat in one shot. Other engines follow the same pattern: [rq], [dramatiq], [huey], [arq], [taskiq]. Use [all] for every engine in one go.

Pip transitively pulls z4j-core and z4j-bare.

Open z4j dashboard, pick a project, navigate to /projects/{slug}/agents, and click new agent. The mint dialog returns two values, both shown ONCE:

  • token → goes into Z4J_TOKEN
  • hmac_secret → goes into Z4J_HMAC_SECRET

Copy both before closing the dialog. The bearer token is stored only as a hash and cannot be recovered. The HMAC value is derived per project from the brain master rather than stored as an agent-specific hash; agents minted in one project receive the same HMAC value.

Add the following to your .env:

Terminal window
Z4J_BRAIN_URL=http://localhost:7700 # http:// to a loopback host is fine for dev
Z4J_TOKEN=<token from step 2>
Z4J_HMAC_SECRET=<hmac_secret from step 2>
Z4J_PROJECT_ID=<project slug from URL> # any non-empty string works locally
Z4J_AGENT_NAME=fragmaster-web # optional, displayed in dashboard

Then in settings.py:

INSTALLED_APPS += [
"z4j_django",
]
Z4J = {
"brain_url": env("Z4J_BRAIN_URL"),
"token": env("Z4J_TOKEN"),
"hmac_secret": env("Z4J_HMAC_SECRET"),
"project_id": env("Z4J_PROJECT_ID"),
"agent_name": env("Z4J_AGENT_NAME", default=None),
}

The Z4J dictionary is worth keeping even when all values come from the environment: runtime configuration can resolve from env alone, but Django's manage.py check currently expects settings.Z4J to exist. No CELERY_APP setting is needed for the standard layout. If your celery.py lives somewhere unusual or auto-detect cannot find it, add a fallback:

CELERY_APP = "myproject.celery:app" # only if auto-detect failed
Terminal window
./manage.py runserver # dev
celery -A myproject worker -l info -E # worker (separate process)
# or
systemctl restart gunicorn # prod

The web and worker processes can each open a worker slot under the configured agent. A slot advertises its own engine and scheduler capabilities in hello. There is no fixed dashboard-population time; confirmation depends on the brain connection, the engine's first emitted event, and the dashboard socket.

runserver may emit no z4j INFO lines under Django's default logging, or several when your logging configuration exposes the z4j loggers. Do not use a line count as a health check.

With Celery INFO logging enabled, a successful worker bootstrap includes this message (Celery supplies its own timestamp/process prefix):

INFO: z4j worker bootstrap: agent runtime started (celery_app=..., framework=...)

The logger name is z4j.adapter.celery.worker_bootstrap. The message is INFO, so it disappears at Celery's default WARNING level even while the agent is running. Treat it as useful evidence when present, not proof of failure when absent. Verify the worker slot and a real task lifecycle in the brain.

Run the doctor first. It checks local configuration, buffer access, and the network/transport path:

Terminal window
python manage.py z4j_doctor

If gunicorn/uvicorn is your web server, run as the same user the service runs under (sudo -u www-data /srv/.../venv/bin/python manage.py z4j_doctor). The WebSocket probe returns after starting the background runtime rather than waiting for agent authentication, so all [OK] rows can still accompany a bad token. Confirm online state and check agent-auth warnings too.

Then in z4j dashboard:

  • Agents / Workers: when web and worker share a token they are worker slots under one agent, not separate agents. The Agents table renders the agent-level framework and engine summary but does not render scheduler adapters; inspect Workers and exercise the relevant process instead.
  • Tasks: enqueue and run a task, then confirm its lifecycle. Do not use a 100 ms timing promise as the acceptance criterion.
  • Schedules: with Celery's usual namespace="CELERY" configuration, the Django setting is CELERY_BEAT_SCHEDULE. Static app.conf.beat_schedule entries are read-only in z4j. Installing django-celery-beat exposes its database rows as writable, but z4j only checks that the models are importable; you must actually run Celery beat with django_celery_beat.schedulers:DatabaseScheduler or accepted edits will not fire.

If you also use RQ in the same Django project:

Terminal window
pip install z4j-rq z4j-rqscheduler

Installing the packages does not make the Django web process discover RQ. RQ has no Celery-style worker signal, so import and call its bootstrap from a module that the RQ worker loads:

from z4j_rq import register_worker_bootstrap
register_worker_bootstrap()

Give that worker Z4J_BRAIN_URL, Z4J_TOKEN, Z4J_PROJECT_ID, and Z4J_HMAC_SECRET in its environment. register_worker_bootstrap() adds the RQ engine only. A later install_agent(schedulers=[...]) call in the same process does not add a scheduler: the agent runtime is a process-wide singleton, and a second install returns the runtime that won first without merging adapters.

If rq-scheduler runs in a separate process, install its adapter once in that process with engines=[] and schedulers=[RqSchedulerAdapter(scheduler=scheduler)]. If the RQ engine and scheduler genuinely share one process, do not call register_worker_bootstrap(). Construct RqEngineAdapter and RqSchedulerAdapter, then pass both to one install_agent(engines=[...], schedulers=[...]) call. See the rq-scheduler guide for the combined example.

The Celery app is located by trying these in order (first hit wins):

  1. settings.CELERY_APP (object or "module:attr" / "module.attr" string)
  2. <root>.celery_app package-level attribute (cookiecutter convention - your project's __init__.py doing from .celery import app as celery_app)
  3. <root>.app package-level attribute
  4. celery.current_app._get_current_object() (any configured app made current by import-time side-effect)
  5. <root>.celery.app submodule attribute

<root> is tried in order: ROOT_URLCONF head, WSGI_APPLICATION head, ASGI_APPLICATION head, BASE_DIR.name. The vast majority of layouts hit one of these.

  • hmac_secret is required - the HMAC value is missing. This message does not diagnose whether the bearer token is a setup token; a wrong non-empty token fails later at agent authentication.
  • refusing plain ws:// connection to ws://... - use Z4J_BRAIN_URL=https://... for non-loopback hosts. For a trusted development network only, put "dev_mode": True in settings.Z4J. Z4J_DEV_MODE=true is ignored by the Django resolver (although the Celery bootstrap currently reads it separately).
  • Close code 4002 - it displaces an older connection only when the same (agent_id, worker_id) reconnects. Distinct processes normally have distinct worker IDs and may share one token, up to the configured per-agent cap.
  • no Celery app located in this process - normal when the web process has no Celery app. z4j does not emit a task.sent event from the web process; lifecycle capture belongs in the Celery worker. Setting CELERY_APP in every web worker also starts a broker-event monitor there, so do not add it merely to silence this line.
  • Worker boots quietly - the auto-bootstrap needs z4j_celery imported, the four required Z4J_* values in the worker environment, and an argv shape its detector recognizes. The documented celery -A myproject worker ... works. Currently, value-taking global options placed before worker (for example celery -b URL -A myproject worker) can make the detector skip; use the documented shape or install the bare runtime explicitly.
  • Schedules read-only or accepted but never firing - static beat schedules are read-only. For database writes, install django-celery-beat and configure the running beat process to use its DatabaseScheduler; package importability alone is not enough.

See troubleshooting for more.