Timezone-aware scheduling
Turn 'next Tuesday at 9am, their time' into a Date and sleep durably until it.
Two sends, both landing at a deliberate local moment — tomorrow 08:30 wall-clock, then next Tuesday inside business hours.
Full write-upexport const firstWeekSchedule = defineJourney({
meta: {
id: "first-week-schedule",
name: "Scheduling — first-week touchpoints",
enabled: true,
trigger: { event: Events.TRIAL_STARTED },
entryLimit: "once",
suppress: hours(12),
exitOn: [{ event: Events.SUBSCRIPTION_CREATED }],
},
run: async (user, ctx) => {
// Tomorrow at 08:30 wall-clock in the user's own timezone. The chain
// returns a plain Date; sleepUntil does the durable waiting.
await ctx.sleepUntil(ctx.when.tomorrow().at("08:30"), {
label: "day-1-morning",
});
if (!(await ctx.guard.isSubscribed())) return;
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: Templates.ONBOARDING_DAY_ONE,
subject: "Day one: the three things worth doing first",
journeyName: user.journeyName,
});
// Next Tuesday at 09:00, clamped into business hours for this chain:
// an instant outside 09:00–17:00 snaps forward to the next open slot.
const tuesday = ctx.when.window("09:00", "17:00").next("tue").at("09:00");
await ctx.sleepUntil(tuesday, { label: "tuesday-tips" });
if (!(await ctx.guard.isSubscribed())) return;
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: Templates.ONBOARDING_WEEKLY_TIPS,
subject: "Three workflows other teams ship in week one",
journeyName: user.journeyName,
});
},
});