Cadence
Cadence answers one question: having shown this form to this person, when may we show it again? It is set per form in the console and enforced by the SDK on the device, so it works the same across web, React, and Vue.
The model
Every wait is measured from the last time the form was shown. There is only ever one clock. What the user did selects which wait applies — it does not start a second, competing timer.
| Outcome | Meaning |
|---|---|
answered | They submitted a response. |
dismissed | They actively closed it without answering. |
ignored | It appeared and they never interacted with it. |
This is why the three are separated: someone who just gave you an NPS score should not be asked again for a month, but someone who flicked the popup away while busy might reasonably be asked again next week, and someone who never even noticed it, tomorrow.
Modes
| Mode | Behaviour | Use for |
|---|---|---|
once | Shown at most once per user, ever, whatever they did. | Onboarding surveys, one-time announcements. |
every_session | Shown at most once per browser session. The default. | General-purpose feedback prompts. |
cooldown | A separate wait per outcome — the flexible option. | Recurring NPS/CSAT programmes. |
always | No cap at all. Shown every single time it is triggered. | Buttons the user clicks deliberately; local development. |
Internally there is only cooldown. every_session expands to a 'session' wait on all three outcomes, and once to a 'never' wait on all three. One rule to reason about, not several that interact.
Cooldown waits
A wait is a number of days, or one of two special values:
30— thirty days.'session'— not again for the rest of this session.'never'— not again, ever.0or absent — no wait; it may show again immediately.
// Console config for a recurring NPS survey
cadence: {
mode: 'cooldown',
cooldown: {
answered: 90, // gave a score — leave them alone for a quarter
dismissed: 14, // closed it — try again in two weeks
ignored: 1, // never saw it — try again tomorrow
},
giveUpAfterShows: 3, // stop entirely after 3 shows with no answer
}Read as a sentence: it behaves exactly as it reads.
giveUpAfterShows
A hard stop. After this many shows without ever receiving an answer, the form is retired for that user permanently. It counts shows, not days, and it is cleared the moment they answer.
It is ignored under mode: 'always' — uncapped means uncapped.
Scoping cooldowns per context
By default one cooldown covers the whole form: ask about any course, and the user is quiet about all courses. Set frequencyScope: 'form_context' and the cooldown is keyed per context value instead, so the same form can be asked once per course, per screen, or per project.
// Console: frequencyScope: 'form_context', with a context targeting rule on courseId
client.showForm('course_csat', { context: { courseId: 'c-101' } }) // shows
client.showForm('course_csat', { context: { courseId: 'c-101' } }) // blocked — same course
client.showForm('course_csat', { context: { courseId: 'c-102' } }) // shows — different courseSee Context for how to attach it.
Cadence applies to manual shows too
This surprises people. showForm() skips the trigger system, but the eligibility check still runs at the moment the form would appear. A form set to once opens on the first click of your feedback button and silently does nothing on every click after that.
Set that form to always. A button the user deliberately clicked should never silently do nothing — cadence exists to stop you interrupting them, not to stop them talking to you.
What cadence is keyed against
Records are stored per identity. While anonymous that is the anonymous id; after identify() it is the user id, and everything recorded anonymously is moved across so nothing is lost or double-counted. A form dismissed while logged out still counts after logging in.
Records live in localStorage, so they are per-device. The same person on their phone and their laptop has two independent cooldowns.
Testing locally
Set the form to always while developing, or clear the SDK's storage between attempts:
// Paste in the browser console to forget all cadence history
Object.keys(localStorage)
.filter((k) => k.startsWith('intunix:'))
.forEach((k) => localStorage.removeItem(k))
sessionStorage.clear()
location.reload()Next: Identity & sessions, which explains what cadence is keyed against and why identify() matters more than reset().