AWS SQS
The queue-backed transport on AWS SQS. Same RemoteTask/StepResult contract as BullMQ — tasks go to a per-group queue, results return on a shared queue — so Node and Python workers interoperate.
@dudousxd/nestjs-durable-transport-sqs carries steps over AWS SQS — the same role as the
BullMQ transport, for teams already on SQS instead of Redis.
pnpm add @dudousxd/nestjs-durable-transport-sqs @aws-sdk/client-sqsHow it works
Say a checkout workflow calls the payments.charge-card remote step with no partition set. The
task routes to a queue named after the step itself, so a worker registers under that same name to
consume it:
// chargeCard = remoteStep({ name: 'payments.charge-card', … })
const { chargeId } = await ctx.remote(chargeCard, { orderId, amountCents });With the default durable prefix, that one call moves three messages across two queues:
- Dispatch → the engine sends the
RemoteTaskJSON to the step's own tasks queue,durable-tasks-payments.charge-card(<prefix>-tasks-<name>[@<partition>]). One queue per step name, so apayments.refundworker never seespayments.charge-cardtasks. - Run → a worker registered for that name long-polls
durable-tasks-payments.charge-card, runs itspayments.charge-cardhandler, then sends theStepResultJSON to the shared results queue,durable-results(<prefix>-results) — every worker replies on the same one. - Resume → the engine long-polls
durable-results, matches the result to the suspendedctx.remotebystepId, checkpoints it, and the workflow continues.
engine ──RemoteTask──▶ durable-tasks-payments.charge-card ──poll──▶ worker
▲ │
└────────── poll ────── durable-results ◀──StepResult────────────────┘// resolves durable-tasks-<name> + durable-results by name (auto-creates them if autoCreate)
new SqsTransport({ client: new SQSClient({ region: 'us-east-1' }), autoCreate: true });const transport = new SqsTransport({ client, group: 'payments.charge-card' });
transport.handle('payments.charge-card', async ({ orderId, amountCents }) => ({
chargeId: await charge(orderId, amountCents),
}));group is the queue token this instance polls — set it to the exact step name it serves (suffixed
@<partition> if the step declares one). Handling a second step name needs a second
SqsTransport instance with its own matching group; each instance polls exactly one queue.
Reusing queues you already provisioned? Skip name resolution by passing their URLs directly:
new SqsTransport({
client,
group: 'payments.charge-card',
taskQueueUrl: () => 'https://sqs.us-east-1.amazonaws.com/1234567890/orders-charge-card',
resultsQueueUrl: 'https://sqs.us-east-1.amazonaws.com/1234567890/orders-results',
});SQS is at-least-once — a task whose worker crashes before deleting it is redelivered after the visibility timeout — so keep handlers idempotent.
Options
| Option | Description |
|---|---|
client | An @aws-sdk/client-sqs SQSClient. |
group | The queue token this instance serves — required to register handle()s; must match the dispatched step's sanitized name (+ @partition, if set). |
prefix | Namespaces the fallback <prefix>-tasks-* / <prefix>-results names. Default durable. |
autoCreate | Create the by-name queues (durable-tasks-<group>, durable-results) if missing. Ignored for queues you supply via taskQueueUrl / resultsQueueUrl — those must already exist. |
taskQueueUrl / resultsQueueUrl | Reuse existing queues instead of resolving by name (a literal URL or a resolver). |
The message body is the documented RemoteTask / StepResult JSON — identical to BullMQ — so a
Python worker on the same queues interoperates.
BullMQ / Redis
The queue-backed transport for cross-process and cross-language steps. Each step name gets its own tasks queue; results return on a shared results queue. Run one instance engine-side, one per worker.
SQL (database)
A broker-less, DBOS-style transport — remote steps are rows in the database you already run. Workers claim tasks with SELECT … FOR UPDATE SKIP LOCKED. The table + claim contract is documented so Node and Python workers share it.