Engineering

Beyond the Score: Visitor Emotion Detection and Decision Stage

Nine numbers tell you how much. Two labels — emotionalState and decisionStage — tell you what kind of session you're looking at.

July 27, 2026 • 9 min read

The Two Fields That Aren't Numbers

This post completes our behavioral models series. Across ten deep-dives we walked through the 26 scoring models that ClickStream runs against every event — a pipeline held to a p95 < 3ms per-event budget as a CI-enforced benchmark. Almost everything those models produce is a number: intent 0–100, frustration 0–100, churn risk, abandonment probability, session momentum.

But the visitor snapshot that your page code actually reads — the 11-field ScoreSnapshot in @clickstreamhq/signals — contains exactly two fields that are not numbers. Straight from the published type definitions:

/** Behavioral emotional state from the current session. */
emotionalState:
  | 'curious'
  | 'engaged'
  | 'frustrated'
  | 'confused'
  | 'excited'
  | 'decisive'
  | 'hesitant'
  | 'neutral';

/** Decision stage the visitor appears to be in. */
decisionStage: 'browsing' | 'evaluating' | 'comparing' | 'deciding' | 'purchasing';

The snapshot is a curated subset of the collector's full 26-model output — only the scores that are useful inside a customer's page logic are surfaced, and internal outputs like anomaly type and next-action alternatives stay collector-side. Nine of the surfaced fields are 0–100 scores (session momentum runs -100..100). The two label fields exist because some questions don't have numeric answers. "How frustrated is this visitor?" is a number. "Is frustration the defining feature of this session, or just background noise under genuine purchase momentum?" is a classification.

Visitor Emotion Detection from Behavior, Not Biometrics

First, what visitor emotion detection analytics mean here — and what they don't. There is no camera, no microphone, no biometric input of any kind. The emotional state classifier is a pure derivation from behavioral scores and session features that the pixel already observes: dwell time, pages per session, scroll depth, navigation entropy, and the intent, frustration, engagement, confusion, and momentum scores computed by the other models. It answers one question: which emotional label best explains the behavior in this session?

The eight emotional states

State What the behavior looks like Sensible response
curious Engaged multi-page reading with deep scrolls and no frustration signals Let them explore; surface related content
engaged Sustained, active attention on the task at hand Stay out of the way
frustrated Frustration or confusion signals dominating the session — rage clicks, error loops, dead ends Open a help path; do not upsell
confused Wandering navigation, searching without finding Clarify and simplify the current page
excited High engagement, real intent, and accelerating session momentum all at once Clear the path to convert; remove friction
decisive Direct, purposeful movement toward a specific outcome Shorten the route; skip the tour
hesitant Stalling at commitment points — forms, pricing, checkout Reassure: guarantees, transparent pricing
neutral Nothing clears a confidence bar Treat as "no signal," not a mild emotion

How the classifier decides

The classification runs as a priority-ordered rule cascade over the behavioral scores — first match wins. Three properties of that design are worth understanding:

Every classification carries a confidence value internally. The snapshot exposes only the label; the confidence stays collector-side, which is exactly why the cascade is tuned to prefer neutral over a shaky guess. The thresholds behind each rule are defaults, and like the rest of the scoring pipeline they are tunable per site — a content site's "deep engagement" is not a checkout flow's.

For the full treatment of the underlying models — including the confusion detector's behavioral signals and how confusion differs from frustration — see Part 3 of the series.

The Five Decision Stages

decisionStage answers a different question: not how the session feels, but where in the decision process the visitor appears to be. The five values — browsing, evaluating, comparing, deciding, purchasing — read like a funnel, but the field is not a funnel report. It's a live label recomputed as the session evolves, and visitors move in both directions: a surprise shipping cost can knock someone from deciding back to comparing in a single pageview.

How the stage is computed

The stage comes from the decision confidence model, which blends behavioral evidence into a single confidence estimate and then reads the stage off an escalating ladder of thresholds:

All of the weights and thresholds above are defaults with per-site overrides, covered in depth in Part 4 of the series. Note that decisionStage is also distinct from the intent model's own four behavioral stages (browsing, researching, evaluating, converting): intent's stages classify how much the visitor wants an outcome, while decision stage estimates how far along the choice itself is. The two usually agree; when they diverge, the divergence is the insight.

Reading Labels and Numbers Together

The labels earn their place in the snapshot when you read them against the numbers. The same intent score means different things under different states. Consider two hypothetical visitors, both with intent at 72 — above the 70 threshold that isHighIntent() uses by default:

Snapshot (hypothetical) What it suggests Playbook
intent 72, excited, deciding Momentum is real and building Do nothing clever — remove steps between them and checkout
intent 72, frustrated, comparing Wants the outcome, blocked by the experience Fix the blocker: help widget, clearer comparison page
frustration 65, hesitant, deciding Close to committing but wavering at the commitment point Reassurance: guarantee, transparent pricing, no popups
engagement 80, curious, browsing Research session, not a purchase session Capture the relationship, not the sale — newsletter, docs

A numeric threshold alone — isFrustrated() fires at 60+, isHighIntent() at 70+ — tells you that you should react. The two labels tell you which reaction fits the session.

Using the Fields in Code

Both fields ship on visitor.scores in @clickstreamhq/signals, the developer-preview Signals client (0.1.0-alpha; the core @clickstreamhq/sdk at 1.4.0 is the stable package). If you're new to the API, start with Getting Started with the Signals API. Configure the client, then read the snapshot:

import { configure, getVisitor } from '@clickstreamhq/signals';

configure({
  apiKey: 'cs_live_xxx',
  endpoint: 'https://t.example.com', // your first-party tracking domain
});

const visitor = await getVisitor();

visitor.scores.emotionalState; // 'curious' | 'engaged' | 'frustrated' | ...
visitor.scores.decisionStage;  // 'browsing' | 'evaluating' | 'comparing' | ...

if (visitor.scores.emotionalState === 'frustrated' && visitor.scores.frustration >= 60) {
  // surface help — this is not the moment to upsell
}

Both labels are first-class criteria across the API. waitFor accepts them, so you can await a stage transition instead of polling for it yourself:

import { waitFor } from '@clickstreamhq/signals';

// Resolves when the visitor reaches the deciding stage
// (rejects at the default 30s timeout if they never do)
const visitor = await waitFor({ decisionStage: 'deciding' });

And the effects engine takes them in declarative rules:

import { applySignals } from '@clickstreamhq/signals';

applySignals([
  {
    when: { emotionalState: 'frustrated', frustrationMin: 60 },
    run: () => showHelpWidget(),
    once: true,
  },
]);

One operational detail worth knowing: before the first scored event for a fresh visitor propagates, the snapshot reports emotionalState: 'neutral' and decisionStage: 'browsing' with the snapshot flagged as pending. Those are also the values your code should treat as the safe no-op case — the same fail-open posture covered in Fail-Open Personalization. A rule keyed on frustrated or deciding simply never fires when Signals is slow, stale, or out.

Honest Limits: Inference, Not Mind Reading

Emotional state and decision stage are behavioral inferences. They deserve the same epistemically honest framing we've used throughout this series:

A behavioral model can tell you a visitor is stalling at your pricing page for the third time. It cannot tell you why. The label is a hypothesis about the session — treat it like one.

Series Complete: The Full Picture

With the two non-numeric fields covered, the behavioral models series is complete. The introduction maps all ten parts; the short version of where these two fields sit: 26 models run at the collector on every event, nine of their outputs surface as numeric scores in the snapshot, and these two labels compress everything the models observed into the terms a product decision actually gets made in. If you want to see your own traffic classified this way, the Signals overview is the place to start, and installation takes a single script tag.

See Your Visitors' State, Not Just Their Score

Eight emotional states and five decision stages, computed from behavior your site already generates — readable in one API call.

Start free