Crux
GuidesDurable ExecutionSignals

Signal recipes

Copy current-API patterns for ingestion, local callbacks, normalization, filtering, and acceptance handling.

Ingest a retrying external event

Define the Signal once at module scope and use the provider's stable event ID as the idempotency key:

import { signal } from "@use-crux/core";
import { z } from "zod";

const invoicePaid = signal({
  id: "billing.invoice.paid",
  schema: z.object({
    invoiceId: z.string(),
    accountId: z.string(),
    amountCents: z.number().int().nonnegative(),
  }),
});

interface ProviderEvent {
  id: string;
  invoiceId: string;
  accountId: string;
  amountCents: number;
}

export async function acceptProviderEvent(event: ProviderEvent) {
  return invoicePaid.publish(
    {
      invoiceId: event.invoiceId,
      accountId: event.accountId,
      amountCents: event.amountCents,
    },
    { idempotencyKey: event.id },
  );
}

Authenticate and verify the provider envelope before calling this function. Signal validation is not webhook authentication.

Attach independent local callbacks

const stopCacheUpdate = invoicePaid.subscribe(({ payload }) => {
  localInvoiceCache.markPaid(payload.invoiceId);
});

const stopUiNotification = invoicePaid.subscribe(async ({ payload }) => {
  await notifyConnectedClient(payload.accountId);
});

// During application shutdown or module disposal:
stopCacheUpdate();
stopUiNotification();

The callbacks are isolated and future-only. If either must survive a restart, do not treat this subscription as a queue.

Normalize at the boundary

const stockCounted = signal({
  id: "inventory.stock.counted",
  schema: z
    .object({ sku: z.string(), counted: z.string() })
    .transform(({ sku, counted }) => ({
      sku,
      counted: Number(counted),
    })),
});

stockCounted.subscribe(({ payload }) => {
  payload.counted; // number
});

await stockCounted.publish({ sku: "sku_123", counted: "12" });

The input type remains { sku: string; counted: string }; listeners and Flow waits receive { sku: string; counted: number }.

Wait for one exact business state

const approved = orderStatusChanged.when({ status: "approved" });

export const fulfillApprovedOrder = flow(
  "fulfill-approved-order",
  { signals: { approved } },
  async (scope) => {
    const occurrence = await scope.waitFor(approved, { timeout: "2d" });
    return scope.step("fulfill", () =>
      fulfillOrder(occurrence.payload.orderId),
    );
  },
);

This requires the qualifying Runtime/store described in Durable Flow waits. The match is portable JSON; the Flow receives the base Signal's complete typed occurrence.

Report acceptance honestly

const receipt = await invoicePaid.publish(payload, { idempotencyKey });

return {
  accepted: true,
  occurrenceId: receipt.occurrenceId,
  guarantee: receipt.guarantee,
};

Return “accepted,” not “processed.” Even a durable receipt says nothing about whether a waiting Flow completed successfully.

Keep a process-local-only event explicit

const refreshRequested = signal({
  id: "ui.refresh.requested",
  schema: z.object({ panel: z.string() }),
});

refreshRequested.subscribe(({ payload }) => refreshPanel(payload.panel));

const receipt = await refreshRequested.publish({ panel: "orders" });
if (receipt.guarantee !== "process-local") {
  throw new Error("A durable consumer was activated unexpectedly.");
}

This assertion is useful when durability would indicate an unintended binding or deployment change. For ordinary code, inspect the receipt without assuming which guarantee will be selected.

See the Signals API reference for every public type and overload.

On this page