Crux
GuidesDurable ExecutionSignals

Filters and idempotency

Build inert match or predicate views and make publication retries safe.

Match normalized data

when() creates a frozen, inert view. A match view stores canonical JSON data:

const checksChanged = signal({
  id: "ci.checks.changed",
  schema: z.object({
    sha: z.string(),
    status: z.enum(["pending", "passed", "failed"]),
    labels: z.array(z.string()),
    repository: z.object({ owner: z.string(), name: z.string() }),
  }),
});

const passed = checksChanged.when({ status: "passed" });
const acmeChecks = checksChanged.when({
  repository: { owner: "acme" },
});

Match semantics are deliberately small:

  • omitted object fields are unrestricted;
  • included object fields recurse;
  • scalar values use exact equality;
  • an included array must equal the complete array in the same order;
  • matching uses normalized schema output, not authored input.

Keep predicates with deployed code

Use a predicate for logic that partial equality cannot express:

const releaseCandidate = checksChanged.when(
  (payload) => payload.status === "passed" && payload.labels.includes("release"),
);

The predicate is code, not persisted data. It is evaluated by the deployed Flow target and must be available anywhere that target resumes. Use predicates only as statically declared Flow sources. Prefer a match view when equality is enough because its identity is portable JSON.

Because the transaction cannot run deployed predicate code, each occurrence for that Signal is durably accepted as a candidate before the Flow evaluates it. A false result leaves the waiter armed; it does not downgrade that candidate's receipt to process-local.

Filtered views have no publish(), subscribe(), or chained when() methods. Publish through the base Signal. Creating a view activates no consumer and does not strengthen a publication guarantee.

Make retries idempotent

Pass a stable key owned by the external occurrence:

const receipt = await checksChanged.publish(payload, {
  idempotencyKey: providerEventId,
});

Within one Signal identity, replaying the same key with the same canonical normalized payload returns the original receipt and creates no second occurrence or callback delivery. Reusing the key with different normalized data throws SignalError with code === "idempotency_conflict".

Scope the key to the event source, not to a delivery attempt. A provider event ID, inbox record ID, or stable application command ID is appropriate. A random value generated inside each retry is not.

Crux stores a versioned hash for durable idempotency and never includes the raw key in receipts or public errors. Process-local idempotency state is lost with the process; only a participating durable binding can make replay state survive restart.

Next, wait from a Flow.

On this page