Basic Usage
The everyday patterns for working with a relay once it's installed
This page assumes you've already scaffolded relay.ts and relay.handlers.ts — see Installation if you haven't. It walks through the patterns you'll reach for day to day. Each links out to a Concepts page for the full explanation.
Registering a handler for a real event
Once you've added a provider plugin (see Plugins), relay.on() autocompletes every event that plugin exposes, and event.data is typed per event — no casting:
// relay.handlers.ts
import type { AppRelay } from './relay';
export function registerHandlers(relay: AppRelay): void {
relay.on('stripe.charge.succeeded', async (event, ctx) => {
const charge = event.data.object; // a typed Stripe.Charge
const payment = await ctx.step.run('record-payment', async () => {
return { chargeId: charge.id, amount: charge.amount };
});
ctx.log.info('payment recorded', payment);
});
}See Typed Events for how the catalog is inferred from the plugins array.
Using ctx.step.run
Wrap each unit of side-effecting work in its own named step. If the handler throws partway through and retries, steps that already completed return their cached output instead of re-running:
relay.on('order.placed', async (event, ctx) => {
const payment = await ctx.step.run('charge-payment', async () => {
return chargeCard(event.data.orderId, event.data.amount);
});
const shipment = await ctx.step.run('create-shipment', async () => {
return createShipment(event.data.orderId);
});
await ctx.step.run('send-confirmation', async () => {
await sendEmail(event.data.customerEmail, { payment, shipment });
});
});Give steps names that describe the side effect, not the event — you'll read them again in relay inspect output. See Durable Steps for how caching and retries interact.
Logging with ctx.log
ctx.log.info/warn/error writes to the same execution log the runtime uses for its own lifecycle events (like retrying (attempt 2, scheduled)), tagged by source so you can tell them apart in relay inspect:
relay.on('stripe.charge.succeeded', async (event, ctx) => {
ctx.log.info('processing charge', { chargeId: event.data.object.id });
// ...
});Ingesting events: webhook vs. direct
In production, events usually arrive via the mounted webhook route (see Next.js) — the provider plugin verifies the signature and calls restaq.ingest() for you. In tests or scripts, call restaq.ingest() directly with the same shape:
const execution = await restaq.ingest({
id: crypto.randomUUID(),
type: 'order.placed',
data: { orderId: 'ord_1', amount: 4200 },
receivedAt: new Date().toISOString(),
});Both paths run the same handler and produce the same execution record — there's no special-casing for "test" events.
Inspecting and replaying via the CLI
Once an execution exists, the relay CLI gives you three ways to look at or re-run it without touching the database by hand:
relay events list # list recent executions
relay inspect <eventId> # status, steps, logs for one execution
relay replay <eventId> # reprocess a historical event as a new executionSee the three retry operations for when to use retryExecution, restartExecution, or replayExecution, and the CLI reference for every command.