Lead alerts
A confirmed hand-raise pages a human — outside the journey's exit rules and the lead's preferences.
The flag fires even when the lead has unsubscribed — an explicit hand-raise reaches the operator, with the subscription state recorded in the properties instead of gating the trigger.
Full write-upexport const setupOffer = defineJourney({
meta: {
id: "setup-offer",
name: "Human-in-the-loop — setup offer",
enabled: true,
trigger: { event: Events.TRIAL_STARTED },
entryLimit: "once",
suppress: hours(12),
// Converting withdraws the pitch, even mid-wait. The awaited answer
// event (offer.answered) must NEVER appear here.
exitOn: [{ event: Events.SUBSCRIPTION_CREATED }],
},
run: async (user, ctx) => {
await ctx.sleep({ duration: days(1), label: "pre-offer" });
if (!(await ctx.guard.isSubscribed())) return;
await sendEmail({
to: user.email,
userId: user.id,
journeyStateId: user.stateId,
template: Templates.OFFER_SETUP_CALL,
subject: "Want a hand getting set up?",
journeyName: user.journeyName,
});
// Answers are provisional clicks confirmed ~30s later; lookback covers
// the gap between the send and this wait being established.
const answer = await ctx.waitForEvent({
event: Events.OFFER_ANSWERED,
timeout: days(4),
lookback: minutes(30),
});
// Gates sends to the USER — the internal flag records the value
// instead of being gated by it.
const subscribed = await ctx.guard.isSubscribed();
if (answer.timedOut || answer.properties?.answer !== "interested") {
return; // silence or "not now" — the no is respected
}
// Scalars only: the lead's email and name are resolved server-side by
// the notify-lead task. Never put PII in event properties.
await ctx.trigger({
event: Events.LEAD_FLAGGED,
userId: user.id,
properties: {
reason: "setup-offer",
answer: "interested",
sourceEvent: Events.OFFER_ANSWERED,
answeredAt: new Date().toISOString(),
subscribed,
},
});
},
});