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.

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

On this page