The survey lifecycle
Surveys in Orbit are a full lifecycle, not a form: an instrument you define once, a dispatch governed by frequency caps, a token-gated response, a scored composite, and a benchmarked result that feeds back into the CDP as contact traits and events. The operational guides are Surveys & VoC, Post-call surveys, and Voice survey scorecards; this page is the concept underneath them. Read it once and “what exactly happens between sending an NPS survey and segmenting on detractors?” stops being a mystery.Section 1 — The instrument model, channels, and tokens
A survey is one tenant-owned row carrying the question copy, an optional follow-up prompt, up to six dispatch channels (sms, whatsapp, email,
viber, rcs, push), optional translations, and the survey type
(nps, csat, ces — or a multi-question instrument with rating, scale,
choice, matrix, open-text, ranking, conjoint, or MaxDiff questions). Every
send validates the chosen channel against that list before anything else.
Two different token mechanisms gate the two reply paths:
The signed token carries everything the unauthenticated landing page needs
to atomically write a response — no session, no database lookup on the
public endpoint — and the HMAC is compared in constant time so a recipient
cannot forge a
{survey, contact} pair by editing the URL. A token that
fails verification returns a generic “link expired” page: the endpoint never
leaks whether a survey id exists.
The [#XXXX] reply tag exists because channel replies are bare numbers:
when a customer texts “8” back, the inbound pipeline needs to know which
of their pending surveys it answers. One shared parser owns the marker
regex for both the inbound webhook and the surveys controller, so the
format can’t drift between the two match paths. Email and push mint no
reply tag — their reply path is the landing-page tap-through.
Section 2 — The response lifecycle: dispatch → open → submit
Every dispatch — the bulk send endpoint, a flow/automation node, or the single-contact automation primitive — inserts asurvey_responses row with
responded_at = NULL before the message goes out. That write-first
ordering means an inbound numeric reply can match even if the provider send
fails mid-flight, and a failed send keeps the row for retry.
The lifecycle then proceeds:
- Dispatch. The row is written; the message exits via the canonical messaging stack (billing, opt-out, and provider routing all apply), or via the push device-token fan-out for the push channel.
- Open. A tap on the signed link renders a self-contained page — a score row plus optional comment box — or a thank-you page when the recipient already answered. Email opens, of course, may arrive days later; the 30-day token window covers that without leaving a leaked link valid forever.
- Submit. The public submit endpoint upserts the pending row on the
first tap, and a repeat visit to the same link updates
scoreandcommentbut never resetsresponded_at—COALESCE(responded_at, NOW())collapses a double-tap or client retry onto one response. The edit window, then, is “as long as the token is valid”: corrections within the 30-day window update the same row instead of creating duplicates.
responded_at IS NULL.
The same first-response-wins discipline holds on post-call surveys: a
unique partial index on (survey, call) collapses a re-triggered post-call
dispatch for the same call onto the first response row, so provider retries
never double-count.
Because the first response is detected and closed before the idempotent
write, downstream side effects — trait writeback, the CDP event, detractor
escalation — fire exactly once per response even when a recipient
re-taps the link.
Section 3 — Scoring, scorecards, and the benchmark
A raw average answers “what is my score?” but not “is it good?”. Orbit composites four read surfaces over the same response rows:
The benchmark refuses to paint a verdict on a handful of responses: below
the minimum sample (30 scored responses by default) it returns
insufficient_data with a null percentile while still echoing your measured
value and the bands — the same sample-gating discipline the messaging
health score uses.
The scoring engines themselves are pure functions: the controller does all
database work, the library does the arithmetic, and every choice/benchmark
computation is deterministic — a “trust the model” hand-wave never enters
the pipeline.
Section 4 — Trait mapping into the CDP
A submitted score is not left sitting in an analytics table. On the first scored response, two side effects close the loop into the customer data platform:- Contact-trait writeback. The response merges
nps_score/csat_score/ces_score,is_detractor,survey_last_score, andsurvey_last_responded_atinto the contact’s attributes. Segments and computed traits can then filter onnps_score ≤ 6oris_detractor = truedirectly. - The
survey_respondedCDP track event. Persisted through the canonical event-ingest path — the same chain the SDK/trackuses — and published onto the per-tenant event channel, so realtime computed traits recompute and event-triggered journeys fire within about a second. The event properties carrysurvey_id,survey_type,channel,score, andis_detractor— a journey’s property filters can target detractors specifically (“enrol detractors” needs no extra wiring). A deterministic message id keyed on the response row collapses a double-submit onto one event row.
Section 5 — VoC analytics rollups
Response comments feed the Voice-of-Customer rollup (GET /surveys/:id/voc), a deterministic, dependency-free aggregation over
each response’s score, comment, and response time:
- Sentiment — a compact, high-precision opinion lexicon classifies each verbatim positive / neutral / negative, with negation handling (“not good” flips polarity). The thresholds are shared with the conversation sentiment surface, so the labels mean the same thing across products.
- Themes — recurring tokens that clear a minimum mention floor surface as bounded theme lists with per-theme driver analysis (which themes pull the score up or down).
- Verbatims — the raw comments, capped so the payload stays bounded.
- Trend — the score tracked over time, so “is the theme getting better?” has a concrete answer.
Section 6 — Frequency caps: one survey per respondent window
Two governance layers protect a contact from over-surveying, and every dispatch path — bulk send, flow node, automation primitive — honours both:- Same-survey dedupe. A contact sent this survey within the fixed 24-hour window is skipped. This always applies.
- Cross-survey fatigue cap. A contact sent any survey within the
configurable fatigue window (72 hours by default;
0disables this layer) is skipped. This is what keeps concurrent NPS, CSAT, and CES programs from collectively over-surveying the same person.
Section 7 — Localization and push delivery
Survey copy localizes at dispatch time, resolved per recipient against the survey’s translation map:- exact locale tag match (case-insensitive),
- primary-subtag match (
pt-PTrequest can hit aptvariant), - the survey’s default locale,
- base copy — always the fallback, so a survey with no translations renders exactly as it did before translations existed.
Cross-references
- Surveys & VoC — the operational guide: create, dispatch, read results.
- Post-call surveys — CSAT/NPS after voice interactions, IVR and message-delivery legs.
- Voice survey scorecards — queue- and agent-level post-conversation scorecards.
- The CDP scoring pipeline — the daily contact-score rolls survey traits join.
- The CDP event model — the event contracts
survey_respondedplugs into. - Tenant isolation — why every survey and
response row lives in
tenant_<id>and the signed token carries its schema with it.