Cross-ecosystem interop
Run NestJS, Adonis, and Python workers on one durable control plane. The BullMQ transport is the shared wire, so a step dispatched by a NestJS engine can execute on a Python or Adonis worker and flow back.
@dudousxd/nestjs-durable, the Adonis port (@adonis-agora/durable), and the Python durable-worker client all speak the same wire protocol, byte-for-byte. A single control plane — in any of the three runtimes — can dispatch work to workers written in any of the others. Your NestJS engine can run a workflow whose steps execute on a Python worker; an Adonis control plane can hand a step to a NestJS worker.
This is proven with live bidirectional interop tests, not just a shared spec: a NestJS control plane dispatching to a store-less Adonis worker, and an Adonis control plane dispatching to a real Python durable-worker — across BullMQ major versions (bullmq 5.x on Node, 2.x on Python), results flowing back correctly.
The shared wire: BullMQ
Interop rides the BullMQ transport. The control-plane pub/sub channel and every cross-process DTO match field-for-field across the three SDKs; the transport adds the shared task/result queues and worker registry:
| Purpose | Channel | Mechanism |
|---|---|---|
| Task dispatch (step + workflow) | ${P}-tasks-${token} | BullMQ queue |
| Step result | ${P}-results | queue |
| Workflow decision | ${P}-decisions | queue |
| Control broadcast | ${P}-control | Redis pub/sub |
| Worker liveness | ${P}-worker-heartbeat:${token}:${instance} | Redis key, EX 35 |
| Worker descriptor | ${P}-worker-descriptor:${token}:${instance} | Redis key |
where P is the effective prefix and the routing token is <name>@<tenant> when a partition is set. Serialization is plain JSON with the DTO as the job data — the worker discriminates step vs workflow by shape. Dates are epoch-ms on cross-process DTOs (ISO only where nested in an EngineEvent/WorkflowRun), and BullMQ priority is inverted the same way on every side. The handshake descriptor's descriptorHash even agrees three-way (Python folds UTF-16 code units to match JS charCodeAt), so negotiation is cross-SDK.
Running a polyglot fleet
-
NestJS control plane — configure the BullMQ transport and dispatch a step by name:
DurableModule.forRoot({ topology: { role: 'control-plane' }, store, transport: new BullMQTransport({ connection: { host: '127.0.0.1', port: 6379 } }), }) -
Python worker — install the client (
pip install durable-worker[redis]) and register a handler under the same name your workflow calls withctx.step('py-echo', ...).run_redis_workerconsumes the same Redis queues the BullMQ transport dispatches to:import asyncio from durable_worker import Worker from durable_worker.redis_runner import run_redis_worker worker = Worker() @worker.step("py-echo") async def py_echo(data): return {"echoed": data} async def main(): await run_redis_worker(worker) await asyncio.Event().wait() asyncio.run(main()) -
Start a run. The
ctx.step('py-echo')task lands on${P}-tasks-py-echo, the Python worker consumes and executes it, publishes on${P}-results, and the NestJS run resumes with the Python-computed output. See Python workers for the full client.
Point a store-less Adonis tenant worker (@adonis-agora/durable, node ace durable:worker) at the same Redis your NestJS control plane dispatches over, registering the handler names your workflows call. The Adonis worker advertises its descriptor, the NestJS control plane negotiates it as routable, and steps it dispatches execute on the Adonis worker with the result flowing back.
Version safety across runtimes
Because every worker advertises a descriptor and the control plane negotiates before dispatching, a polyglot fleet is version-safe: an incompatible protocol major is refused loudly, an optional capability a Python worker lacks routes that work only to workers that have it, and a run requiring a capability no live worker provides parks blocked until one appears — never a silent cross-language hang.
Learn more
- Python workers — the
durable-workerclient (PyPI), authoring steps and coordinator-driven workflows in Python. - BullMQ / Redis transport — the shared wire in detail.
- Handshake & negotiation — the compatibility layer that makes a mixed-version fleet safe.
Handshake & negotiation
How a mixed fleet stays version-safe — workers advertise a capability descriptor, the control plane negotiates compatibility, and work routes only to workers that can run it. Runs park blocked instead of hanging.
Patterns
A cookbook of common durable-workflow shapes — human approval, webhook confirmation, batch fan-out, cron digests, booking sagas, rate-limited APIs, and long-lived pollers — each a short, self-contained recipe with a link to the full page.