UserSay/ Docs

Moments — when Sarah shows up

A Moment is a user state, not an event hook — the eleven states she recognises, what each costs you to enable, and how she avoids asking the same person twice.

At any second, every user of your product is in some state. Just landed. Two diagrams in and stuck. Reading your pricing page. Finger on the confirm-cancel button.

A Moment is one of those states. It is not a trigger and not an event hook — those are the plumbing underneath it. A Moment is the wrapper around the plumbing: the user is in state X right now, and here is the one question only askable in state X.

That distinction is the entire mechanic. Sarah arrives while the context is still loaded in the user's working memory, so instead of "why did you leave?" she can ask "walk me through the week before you decided" — and they actually answer, because they're still in it.

Twenty-four hours later that context is gone and you get the tidy version: "too expensive." "just exploring." "7 out of 10." A Diffus user ticked "too expensive" on the exit survey. Asked inside the product a minute earlier, the same person said:

"The price is fine — I was happy to pay as long as it delivered. It didn't."

Then named the competitor they were switching to. Same person, same decision, two different answers. The difference isn't the question. It's the second you asked it in.


The eleven built-ins

Eleven Moments ship with the product — one per state Sarah can recognise today.

A Moment is a point in time, not a category. It says when she engages, and nothing about which of your goals that conversation ends up serving. The grouping below is only a hint about where each one usually lands. A cancel chat routinely surfaces the pricing objection that belongs to Grow; a pricing_visit often turns into the clearest bug report you'll get all week. Read the left column as "where this tends to matter", never as ownership.

The last column is the setup tier from How much you have to build, and it means exactly what it means there. All four tiers assume the script tag is in and identify() is being called.

Usually lands inMomentIDWhat state it catchesWhat it costs you
ActivateWelcomewelcomeFirst fifteen seconds — they're here and haven't done anything yetFree
First valuefirst_useTheir first real success just completed — not their first attemptOne line of code
Generation failedgeneration_failedThey tried and the output missedOne line of code
StuckstuckEverything they need is on screen and they still don't know what to doOne line of code
KeepCancelcancelRight before the final confirm step of a cancellationOne line of code
Return visitreturn_visitA non-paying user is back — 2nd session, or after ≥ 7 days quietFree
GrowOut of creditscredits_exhaustedInterrupted mid-task by their own usage limitFree
Visiting pricingpricing_visitEvaluating — on /pricing for ≥ 10sFree
Paywall exitpaywall_shownThey saw the price and backed out, and didn't go to /pricing in the 5 minutes afterOne line of code
Return paid userreturn_paid_userA paying user is back — 2nd session, or after ≥ 7 days quietA field in identify()
AnywhereOpen feedbackopen_feedbackWhatever they chose to bring you — the one Moment where the user picks the topicA button you add

Why two stages aren't in that table

Understand isn't in the table for the opposite reason. It doesn't need a Moment because it runs on all of them. Every completed conversation is mined for signals, personas, and demand afterwards, whichever Moment opened it. A cancel chat, a pricing chat, and a stuck-user rescue all feed the same Signals board. That's why turning on more Moments makes Understand better without you configuring anything.

Two more notes on the table.

credits_exhausted is free because she watches for HTTP 402 responses coming back from your own API. If your product already returns 402 when a user runs out, it fires with no instrumentation at all. Two things have to be true for that: your API returns 402, and 402 detection is on — which it is on every new project, unless you switch it off in the dashboard. Miss either one and the Moment simply drops a tier to UserSay.trigger('credits_exhausted'), behaving identically from there.

Understand has one Moment but feeds on all eleven. Signals and personas are built from every transcript, whatever fired it. open_feedback is listed there because it's the only one where the user, not your product, decides what the conversation is about.

Wiring the ones that need a line

The five one-line Moments are the ones only you can define — she won't guess at what a first success looks like in your product, because a Moment that fires at the wrong second costs more than one that doesn't fire. Each is a single call, placed where the thing actually happens:

UserSay.trigger('first_use');         // in the success callback of their first creation
UserSay.trigger('generation_failed'); // repeated regenerations, a low rating, a big manual edit
UserSay.trigger('stuck');             // your own idle / retry / dead-end heuristic
UserSay.trigger('paywall_shown');     // when the user closes your upgrade modal
UserSay.trigger('cancel');            // in your cancel flow, alongside the confirm

Two behaviours are worth knowing before you place them. paywall_shown doesn't fire on the call — the SDK waits five minutes and fires only if the user didn't head to /pricing in the meantime, because someone who went to read the pricing page hasn't bounced. And cancel is the one worth wiring twice: trigger() puts Sarah alongside a cancellation your own code still performs, which is all you can do until she has tools. Once she has them, hand her the button instead — a different call, and the difference between a cancel understood and a cancel saved. Either way the cancellation goes through; what must never happen is your flow waiting on her, because the SDK reports a completed interview and not an abandoned one.

The button Moment uses a different call:

feedbackBtn.addEventListener('click', () => {
  UserSay.openPrompt('open_feedback');
});

openPrompt() rather than trigger(), because it skips the dedup checks entirely. The user asked for the chat; there is nothing to suppress.


Choosing which to turn on

Seven Moments are enabled the day you create a project: welcome · return_visit · pricing_visit · credits_exhausted · paywall_shown · cancel · open_feedback. Four of those need no code, so a fresh install starts talking to users on its own; the other three sit enabled but silent until you wire them.

Turning on all eleven at once is the mistake worth avoiding. Too many conversations, too thin a signal in each, and your first week on the Signals board reads as noise. Start with three:

  1. welcome — free, fires on a first visit, and tells you who is actually showing up. It often doesn't match who you think the product is for.
  2. open_feedback — point your existing Feedback button at it. Lower volume than the rest, higher intent than any of them: they came to you.
  3. One high-intent Moment, whichever surface your product actually has — cancel if there's a cancel flow, credits_exhausted if there are usage limits, paywall_shown if there's an upgrade modal.

Three Moments produce roughly 20–50 conversations in a first week. Enough to see one pattern; not so many that everything blurs.

Add the fourth reactively. When the Signals board shows a gap you wish you could hear into — "a lot of people bounce off the paywall and I have no idea why" — that's the prompt to enable paywall_shown. Adding a Moment in answer to a question Sarah already raised beats adding it speculatively.


Suppression and dedup

She won't pester the same person twice. Every Moment carries a dedup policy, tracked separately for three different endings:

  • Fired, no engagement — the bubble appeared and the user ignored it or closed the tab.
  • Completed — the chat ran and ended normally.
  • Dismissed — the user actively closed the bubble or said no thanks.

Each ending gets its own gate: never again, a cooldown of N days, once per browser session, or no gate at all. The shipped defaults:

MomentAfter an unengaged fireAfter a completed chatAfter a dismissal
welcome · first_use · cancelnever againnever againnever again
return_visit · return_paid_user · paywall_shown7 days30 days7 days
credits_exhausted · generation_failed1 day30 days7 days
stuckonce per browser session30 days7 days
pricing_visitnever againnever again30 days
open_feedbackno gateno gateno gate

Three things are worth reading off it. The never-again Moments are the ones that genuinely happen once — there's one first visit and one cancellation per user, so a second ask would be a fiction. stuck is gated per browser session because that state recurs within a session and the bubbles would otherwise pile up across reloads. And open_feedback has no dedup at all: the user clicked your button, so there's nothing to suppress — a rate limit is the only thing behind it.

The counters live server-side against the uid you pass to identify(), and the SDK waits for them before its first suppression check. A user who ignored welcome on their laptop doesn't meet it again on their phone. Session-only gates are the exception, being per browser session by definition.

Any of the three endings can be overridden per Moment in the dashboard.


Customising one

Every Moment ships with defaults, and each is editable per project.

The opener. Her first line, supporting {product} and {reward}, with a held translation per locale if you want one. Most defaults are plain curiosity — welcome opens with "hey! what do you do? curious how you found {product} 😊". Only credits_exhausted names the reward up front, because that user is blocked mid-task and the incentive is what starts the conversation at all.

What she tries to learn. Each Moment carries a list of learnings — human-language lines, not internal keys, that go straight into her prompt. The recommended ones are pre-checked; suggested ones sit unchecked until you want them (cancel suggests "Whether they might come back later"). A separate open-exploration toggle decides whether she may follow a thread past the list. It's on by default, and it's where the surprising material tends to come from.

Depth. Two settings, and neither is a turn budget — conversation length is signal-based, and an engaged user keeps going regardless. Depth only moves the closing threshold. quick closes once she has the headline: who they are and what they came to do. deep holds out for at least one differentiator — a prior tool, a blocker, a price anchor, a recommendation. Eight of the eleven ship deep; return_visit, return_paid_user and open_feedback ship quick.

Two smaller knobs. Reward — credits, a coupon, or a link. open_feedback can never carry credits (the server strips it; they came to you unpaid and paying for that is the wrong shape), and pricing_visit ships with no default reward on purpose, because it's enabled out of the box and a hardcoded coupon code would be a promise Sarah makes that your checkout can't keep. Open mode — a preview bubble first, or the chat straight away. open_feedback defaults to chat, since the user already clicked something and a second preview is redundant.


Next: Plays — what she already knows to do once one of these puts her in the room.

On this page