# Queues & Messaging

## Queue

Use this for fire-and-forget commands where the client does not need a reply.

Use the [`Loops`](/workflows/docs/quickstart#loops) example as the baseline pattern.

## Request/response (using queue)

Use this when the caller needs a response from queued processing.

index.ts:

```ts
import { actor, queue, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";

export const requestResponseActor = actor({
  state: {
    handled: 0,
  },
  queues: {
    requests: queue<{ value: number }, { doubled: number }>(),
  },
  run: workflow(async (ctx) => {
    await ctx.loop("request-loop", async (loopCtx) => {
        const message = await loopCtx.queue.next("wait-request", {
          completable: true,
        });

        if (!message.complete) return;

        const doubled = await loopCtx.step("handle-request", async (step) => {
          step.state.handled += 1;
          return message.body.value * 2;
        });

        await message.complete({ doubled });
      });
  }),
});

export const registry = setup({ use: { requestResponseActor } });
```

client.ts:

```ts
import { createClient } from "rivetkit/client";
import type { registry } from "./index";

const client = createClient<typeof registry>("http://localhost:6420");
const handle = client.requestResponseActor.getOrCreate(["main"]);

const result = await handle.send(
  "requests",
  { value: 21 },
  { wait: true, timeout: 1_000 },
);

if (result.status === "completed") {
  const response = result.response as { doubled: number };
  console.log(response.doubled);
}
```

## Queue-driven worker

Use this when external systems enqueue work and the actor should process each item durably.

examples/docs/actors-workflows/queue-worker.ts:

```ts
import { actor, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";

type Job = { id: string; amount: number };

export const queueWorkerActor = actor({
  state: {
    processed: 0,
    totalAmount: 0,
  },
  run: workflow(async (ctx) => {
    await ctx.loop("worker-loop", async (loopCtx) => {
        const [message] = await loopCtx.queue.nextBatch("wait-job", {
          timeout: 30_000,
        });

        if (!message) return;
        const job = message.body as Job;

        await loopCtx.step("process-job", async (step) => {
          step.state.processed += 1;
          step.state.totalAmount += job.amount;
        });

      });
  }),
  actions: {
    getState: (c) => c.state,
  },
});

export const registry = setup({ use: { queueWorkerActor } });
```

## Request/response over queue (async RPC)

Use this when you want decoupled actor-to-actor communication with durable waits and explicit completion.

index.ts:

```ts
import { actor, queue, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";

type RequestMessage = { value: number };

export const requestResponseActor = actor({
  state: {
    handled: 0,
  },
  queues: {
    requests: queue<RequestMessage, { doubled: number }>(),
  },
  run: workflow(async (ctx) => {
    await ctx.loop("request-response-loop", async (loopCtx) => {
        const message = await loopCtx.queue.next("wait-request", {
          completable: true,
        });

        if (!message.complete) return;

        const doubled = await loopCtx.step("handle-request", async (step) => {
          step.state.handled += 1;
          return message.body.value * 2;
        });

        await message.complete({ doubled });
      });
  }),
});

export const registry = setup({ use: { requestResponseActor } });
```

client.ts:

```ts
import { createClient } from "rivetkit/client";
import type { registry } from "./index";

const client = createClient<typeof registry>("http://localhost:6420");
const handle = client.requestResponseActor.getOrCreate(["main"]);

const result = await handle.send("requests", { value: 21 }, { wait: true });

if (result.status === "completed") {
  const response = result.response as { doubled: number };
  console.log(response.doubled);
}
```

## Batch drainer

Use this when throughput matters and handling one message at a time is too expensive.

examples/docs/actors-workflows/batch-drainer.ts:

```ts
import { actor, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";

type MetricMessage = { value: number };

export const batchDrainerActor = actor({
  state: {
    pending: [] as number[],
    flushedBatches: 0,
    lastBatchTotal: 0,
  },
  run: workflow(async (ctx) => {
    await ctx.loop("drain-loop", async (loopCtx) => {
        const [message] = await loopCtx.queue.nextBatch("wait-metric", {
          timeout: 5_000,
        });

        const pendingCount = await loopCtx.step(
          "buffer-message",
          async (step) => {
            if (message) {
              step.state.pending.push((message.body as MetricMessage).value);
            }
            return step.state.pending.length;
          },
        );

        if (pendingCount < 5) return;

        await loopCtx.step("flush-batch", async (step) => flushBatch(step));

      });
  }),
  actions: {
    getState: (c) => c.state,
  },
});

function flushBatch(ctx: WorkflowStepContextOf<typeof batchDrainerActor>): void {
  const total = ctx.state.pending.reduce(
    (sum: number, value: number) => sum + value,
    0,
  );
  ctx.state.lastBatchTotal = total;
  ctx.state.flushedBatches += 1;
  ctx.state.pending = [];
}

export const registry = setup({ use: { batchDrainerActor } });
```

## Bounded drain + concurrency cap

Use this when inbound work can spike and you need predictable per-iteration limits.

examples/docs/actors-workflows/bounded-drain.ts:

```ts
import { actor, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";

type WorkMessage = { id: string; value: number };

const MAX_PER_ITERATION = 10;
const CONCURRENCY_LIMIT = 3;

async function processWork(value: number): Promise<number> {
  return value * 2;
}

async function runWithLimit<T>(
  limit: number,
  items: T[],
  fn: (item: T) => Promise<void>,
): Promise<void> {
  let nextIndex = 0;
  const workers = Array.from({ length: limit }, async () => {
    while (nextIndex < items.length) {
      const current = items[nextIndex];
      nextIndex += 1;
      await fn(current);
    }
  });
  await Promise.all(workers);
}

export const boundedDrainActor = actor({
  state: {
    processed: 0,
    lastWindowSize: 0,
    lastWindowTotal: 0,
  },
  run: workflow(async (ctx) => {
    await ctx.loop("bounded-drain-loop", async (loopCtx) => {
        const window: WorkMessage[] = [];

        for (let i = 0; i < MAX_PER_ITERATION; i += 1) {
          const [message] = await loopCtx.queue.nextBatch("wait-work", {
            timeout: i === 0 ? 30_000 : 10,
          });
          if (!message) break;
          window.push(message.body as WorkMessage);
        }

        if (window.length === 0) return;

        await loopCtx.step("process-window", async (step) =>
          processWindow(step, window),
        );

      });
  }),
  actions: {
    getState: (c) => c.state,
  },
});

async function processWindow(
  ctx: WorkflowStepContextOf<typeof boundedDrainActor>,
  window: WorkMessage[],
): Promise<void> {
  let windowTotal = 0;
  await runWithLimit(CONCURRENCY_LIMIT, window, async (work) => {
    const result = await processWork(work.value);
    windowTotal += result;
  });

  ctx.state.processed += window.length;
  ctx.state.lastWindowSize = window.length;
  ctx.state.lastWindowTotal = windowTotal;
}

export const registry = setup({ use: { boundedDrainActor } });
```

## Signal-driven control loop

Use this when workflow progress should be triggered by commands/events instead of fixed polling intervals.

examples/docs/actors-workflows/signal-control-loop.ts:

```ts
import { actor, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";

type ControlSignal = { kind: "pause" | "resume" | "stop" };

export const controlLoopActor = actor({
  state: {
    mode: "running" as "running" | "paused" | "stopped",
    handledSignals: 0,
  },
  run: workflow(async (ctx) => {
    await ctx.loop("control-loop", async (loopCtx) => {
        const [message] = await loopCtx.queue.nextBatch("wait-signal", {
          timeout: 30_000,
        });

        if (!message) return;
        const signal = message.body as ControlSignal;

        await loopCtx.step("apply-signal", async (step) =>
          applyControlSignal(step, signal.kind),
        );

      });
  }),
  actions: {
    getState: (c) => c.state,
  },
});

function applyControlSignal(
  ctx: WorkflowStepContextOf<typeof controlLoopActor>,
  kind: ControlSignal["kind"],
): void {
  ctx.state.handledSignals += 1;
  if (kind === "pause") ctx.state.mode = "paused";
  if (kind === "resume") ctx.state.mode = "running";
  if (kind === "stop") ctx.state.mode = "stopped";
}

export const registry = setup({ use: { controlLoopActor } });
```

## Human approval gate

Use this when an operation must pause for a user or system decision before continuing.

examples/docs/actors-workflows/approval-gate.ts:

```ts
import { actor, queue, setup } from "rivetkit";
import { type WorkflowStepContextOf, workflow } from "rivetkit/workflow";

export const approvalGateActor = actor({
  state: { status: "pending" as string },
  queues: {
    approval: queue<{ approved: boolean }>(),
  },
  run: workflow(async (ctx) => {
    await ctx.step("validate-order", async (step) => {
      await validateOrder("order-123");
      step.state.status = "awaiting_approval";
    });

    const decision = await ctx.queue.next("wait-approval");

    if (decision.body.approved) {
      await ctx.step("fulfill-order", async (step) => {
        await fulfillOrder("order-123");
        step.state.status = "fulfilled";
      });
    } else {
      await ctx.step("cancel-order", async (step) => {
        await cancelOrder("order-123");
        step.state.status = "cancelled";
      });
    }
  }),
  actions: {
    getState: (c) => c.state,
  },
});

async function validateOrder(orderId: string): Promise<void> {
  const res = await fetch(
    `https://api.example.com/orders/${orderId}/validate`,
    { method: "POST" },
  );
  if (!res.ok) throw new Error("Order validation failed");
}

async function fulfillOrder(orderId: string): Promise<void> {
  await fetch(`https://api.example.com/orders/${orderId}/fulfill`, {
    method: "POST",
  });
}

async function cancelOrder(orderId: string): Promise<void> {
  await fetch(`https://api.example.com/orders/${orderId}/cancel`, {
    method: "POST",
  });
}

export const registry = setup({ use: { approvalGateActor } });
```
