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.
1. Install
Section titled “1. Install”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.
2. Mint an agent token
Section titled “2. Mint an agent token”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.
3. Configure settings
Section titled “3. Configure settings”Add the following to your .env:
Z4J_BRAIN_URL=http://localhost:7700 # http:// to a loopback host is fine for devZ4J_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 locallyZ4J_AGENT_NAME=fragmaster-web # optional, displayed in dashboardThen 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 failed4. Restart
Section titled “4. Restart”./manage.py runserver # devcelery -A myproject worker -l info -E # worker (separate process)# orsystemctl restart gunicorn # prodThe 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.
Expected log output
Section titled “Expected log output”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.
5. Verify
Section titled “5. Verify”Run the doctor first. It checks local configuration, buffer access, and the network/transport path:
python manage.py z4j_doctorIf 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 isCELERY_BEAT_SCHEDULE. Staticapp.conf.beat_scheduleentries are read-only in z4j. Installingdjango-celery-beatexposes its database rows as writable, but z4j only checks that the models are importable; you must actually run Celery beat withdjango_celery_beat.schedulers:DatabaseScheduleror accepted edits will not fire.
Multi-engine (optional)
Section titled “Multi-engine (optional)”If you also use RQ in the same Django project:
pip install z4j-rq z4j-rqschedulerInstalling 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.
Auto-detect, in detail
Section titled “Auto-detect, in detail”The Celery app is located by trying these in order (first hit wins):
settings.CELERY_APP(object or"module:attr"/"module.attr"string)<root>.celery_apppackage-level attribute (cookiecutter convention - your project's__init__.pydoingfrom .celery import app as celery_app)<root>.apppackage-level attributecelery.current_app._get_current_object()(any configured app made current by import-time side-effect)<root>.celery.appsubmodule 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.
Troubleshooting
Section titled “Troubleshooting”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://...- useZ4J_BRAIN_URL=https://...for non-loopback hosts. For a trusted development network only, put"dev_mode": Trueinsettings.Z4J.Z4J_DEV_MODE=trueis 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 atask.sentevent from the web process; lifecycle capture belongs in the Celery worker. SettingCELERY_APPin 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_celeryimported, the four requiredZ4J_*values in the worker environment, and an argv shape its detector recognizes. The documentedcelery -A myproject worker ...works. Currently, value-taking global options placed beforeworker(for examplecelery -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.