Expire something

Run a side effect (email, cleanup, notification) when an invitation, checkout hold, or magic link expires. Handler reads current state and skips if the user already acted.

Use this to
  • Email a user that their Clerk invitation expired (Clerk has no webhook on time-based expiry)
  • Release a held checkout cart 30 minutes after the customer started
  • Invalidate a magic-link sign-in if the user doesn't click within 15 minutes
  • Mark an upload as failed if processing hasn't completed within 10 minutes
Code
Clerk invitation: notify the user when it expires
// Clerk silently invalidates expired invitations but does not
// fire a webhook on time-based expiry. DelayKit fills the gap:
// schedule a job that runs the same moment Clerk stops accepting.
const invitation = await clerkClient.invitations.createInvitation({
  emailAddress: "user@example.com",
  expiresInDays: 7,
});
await dk.schedule("expire-invitation", {
  key: invitation.id,
  delay: "7d",
});

dk.handle("expire-invitation", async ({ key }) => {
  const invitation = await clerkClient.invitations.getInvitation(key);
  if (invitation.status !== "pending") return; // already accepted

  await sendEmail(invitation.emailAddress, "Your invitation has expired");
  await db.pendingUsers.delete(invitation.id);
});
Checkout hold: release stock if checkout stalls
// when the customer starts checkout, hold the inventory
await dk.schedule("release-hold", {
  key: cart.id,
  delay: "30m",
});

dk.handle("release-hold", async ({ key }) => {
  const reservation = await reservations.find(key);
  if (reservation.status !== "held") return; // already paid or cancelled
  await reservations.release(key);
});
Cancel the expiry on early acceptance
// in your Clerk webhook or post-payment route
await dk.unschedule("expire-invitation", invitation.id);
npm install delaykit
bun add delaykit