Aviary
Cluster

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:

PurposeChannelMechanism
Task dispatch (step + workflow)${P}-tasks-${token}BullMQ queue
Step result${P}-resultsqueue
Workflow decision${P}-decisionsqueue
Control broadcast${P}-controlRedis 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

  1. 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 } }),
    })
  2. Python worker — install the client (pip install durable-worker[redis]) and register a handler under the same name your workflow calls with ctx.step('py-echo', ...). run_redis_worker consumes 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())
  3. 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.

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

On this page