Aviary
Transports

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-sqs

How 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:

the step + the call
// 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:

  1. Dispatch → the engine sends the RemoteTask JSON to the step's own tasks queue, durable-tasks-payments.charge-card (<prefix>-tasks-<name>[@<partition>]). One queue per step name, so a payments.refund worker never sees payments.charge-card tasks.
  2. Run → a worker registered for that name long-polls durable-tasks-payments.charge-card, runs its payments.charge-card handler, then sends the StepResult JSON to the shared results queue, durable-results (<prefix>-results) — every worker replies on the same one.
  3. Resume → the engine long-polls durable-results, matches the result to the suspended ctx.remote by stepId, checkpoints it, and the workflow continues.
 engine ──RemoteTask──▶ durable-tasks-payments.charge-card ──poll──▶ worker
   ▲                                                                    │
   └────────── poll ────── durable-results ◀──StepResult────────────────┘
engine side
// resolves durable-tasks-<name> + durable-results by name (auto-creates them if autoCreate)
new SqsTransport({ client: new SQSClient({ region: 'us-east-1' }), autoCreate: true });
worker side
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

OptionDescription
clientAn @aws-sdk/client-sqs SQSClient.
groupThe queue token this instance serves — required to register handle()s; must match the dispatched step's sanitized name (+ @partition, if set).
prefixNamespaces the fallback <prefix>-tasks-* / <prefix>-results names. Default durable.
autoCreateCreate the by-name queues (durable-tasks-<group>, durable-results) if missing. Ignored for queues you supply via taskQueueUrl / resultsQueueUrl — those must already exist.
taskQueueUrl / resultsQueueUrlReuse 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.

On this page