User ManualMessages

Messages

/messages lets you publish in-app tooltips, modals, and banners that fire on specific events. Once a campaign is set to Active and the product’s auto-render toggle is on, the SDK on your customers’ sites will render the campaign automatically — no per-customer integration required.

The list

Each message has a status badge: Draft, Active, or Paused.

  • New message — give the message a name, then jump to the editor.
  • Edit — opens the editor.
  • Preview — opens /messages/:id/preview to rehearse the flow without firing real events.
  • Delete — removes the message with an undo toast (you have a few seconds to Undo before it’s gone).

The editor

The editor (/messages/:id) has four sections plus an A/B Test badge if the message is part of an experiment (see A/B-test variants below).

Name

A human-readable label.

Trigger

When the message should fire:

  • Event name — the event that triggers the message (e.g. page_viewed, $pageview, signup_started). Auto-captured events ($pageview, $click, $form_submit) work as triggers.
  • URL regex (optional) — additionally limits firing to URLs whose location.pathname matches the regex. Examples: ^/pricing$ for the exact pricing page, ^/checkout for any URL under /checkout.
  • Audience (optional) — gates rendering by user properties. See Audience targeting below.

Steps

One or more steps that play in order. Each step has:

  • Type — Modal, Banner, or Tooltip. Steps within one campaign can mix types.
  • CSS selector (Tooltip only) — the element the tooltip anchors to (e.g. #upgrade-btn).
  • Content — the message body.
  • CTA label / CTA URL (optional) — adds a button.

When a campaign has multiple steps, the renderer adds a “Next →” button on non-final steps and a “Done” button on the final step. The user can also click × on any step to dismiss the campaign. See Step lifecycle.

Frequency cap

How often the same user can see this campaign:

  • Once per session — within one browser session only. New session ⇒ user sees it again. (The SDK handles this in memory; the server doesn’t filter.)
  • Once per day — once every 24 hours, measured from the user’s last terminal interaction.
  • Once ever (default) — the safe default — once the user dismisses, clicks CTA, or completes the flow, they never see it again.

The cap counts only terminal interactionsdismiss, cta_click, or complete. A user who sees step 1 of a 3-step campaign and walks away (no dismiss, no CTA) is not considered “consumed” and may see the campaign again later.

Saving

In the top-right of the editor, change the status (Draft / Active / Paused) and click Save.

Auto-rendering on your customers’ sites

⚠️

Auto-render is off by default at the product level. Active campaigns won’t appear on customer sites until you flip the toggle.

Flip the product toggle

Go to Settings → Products → [your product] → In-app messages and turn on Auto-render in-app messages.

Mark the campaign Active

In the message editor, set the status to Active and save.

Wait for the SDK to refetch

The customer’s SDK fetches the active list on init and on tracker.identify(), plus on window focus after >30 min idle. There’s a 5-minute sessionStorage cache, so changes can take up to 5 min to propagate to in-progress sessions.

The user does the trigger event

The next time the user fires the trigger event on a URL that matches the URL regex, the campaign renders.

Step lifecycle

User actionWhat firesWhat happens
Renderer mounts step 0message_impressionOne per campaign — not per step
Click “Next →” on a non-final step(no event)Current step dismisses, next step renders
Click “Done” on the final stepmessage_completeFlow ends successfully — counts toward cap
Click × on any stepmessage_dismissFlow ends — counts toward cap
Click CTA on any stepmessage_cta_clickFlow ends, optional URL opens — counts toward cap

See Event taxonomy for the full payload shape of each event.

Audience targeting

Currently supported: score-based audiences that target users by predicted churn risk.

In the trigger config, set the audience to:

{ "type": "score", "churnProbGt": 0.5 }

Either or both of:

  • churnProbGt — only users whose user_scores.churnProb is strictly greater than this value qualify.
  • topN — only users whose churn-risk rank within the tenant is below topN qualify.

When both are set, the user must clear both conditions.

Other audience types (userFilterTree, custom property filters) are on the roadmap but not yet shipped. Campaigns with no audience field render for everyone.

Preview

The Preview button on the editor opens /messages/:id/preview — a sandbox that renders the campaign exactly as it would on a customer’s site, but with a no-op track callback so no events fire to the backend. Use it to rehearse multi-step flows and check copy/positioning before going Active.

A/B-test variants

You can A/B test two (or more) versions of a campaign — different copy, different CTA, different timing — and let TrackCrumb pick a winner via the same Bayesian engine that powers Experiments.

Create a variant

  1. Open an existing message and click Create variant.
  2. The dashboard clones the message (steps, trigger, frequency cap copied), creates a new Experiment with control (the original) and treatment (the clone) arms, creates a flag at 50% rollout linking them, and routes you to the clone’s editor.
  3. Edit the clone — change the copy, CTA text, step structure, anything you want to test.
  4. Set both messages to Active and ensure the product’s auto-render toggle is on. Done.

How rendering picks a variant

When the SDK fetches active campaigns, the response groups variant messages under one logical campaign with a variants map:

{
  "id": "msg-original",
  "experimentId": "exp-abc",
  "variants": {
    "control":   { "messageId": "msg-original", "steps": [...] },
    "treatment": { "messageId": "msg-clone",    "steps": [...] }
  }
}

The SDK calls flagsManager.getVariant("<flag_key>") to deterministically pick an arm by distinct_id, then renders the matching variant’s steps[]. The same user always sees the same arm.

Frequency cap aggregates across variants

When a campaign has variants, the cap aggregates: a user who consumes (dismisses, clicks CTA, or completes) the control variant will not see the treatment variant in the same session — and vice-versa. Cap-checking sums terminal events across all variant message ids of the same experimentId.

Reading results

Open the linked experiment from the A/B Test badge on the message editor. The Experiments results panel shows winner + per-arm breakdown + SRM check + apply-winner — see Experiments for details.

When you click Apply winner, the linked flag flips to 100% and that variant becomes the only one rendered. The losing variant message remains in the dashboard for your records but stops appearing on customer sites.

Test one thing at a time. If your control and treatment differ in copy AND timing AND CTA, you won’t know which change moved the metric. Two variants × one change = clear answer.

Frequency cap mechanics

When the SDK fetches /api/v1/messages/active?distinct_id=<user>, the server consults the message_impressions table:

  • ever — excludes if any terminal event exists for (message_id, distinct_id).
  • day — excludes if a terminal event exists with created_at within the last 24 h.
  • session — server returns the campaign; the SDK’s in-memory Set<messageId> blocks within-session re-renders.

Terminal events are dual-written by the SDK: once via the standard /e ingest path (so they show up in funnels and trends like any other event) and once via POST /api/v1/messages/:id/track (which populates message_impressions for the cap to read).

Troubleshooting

  • Campaign isn’t rendering. Check (1) status is Active, (2) product toggle is on, (3) trigger event name matches what the SDK actually fires (open the Network tab and look for /e requests), (4) URL regex matches location.pathname, (5) the cap hasn’t been consumed (try a different distinct_id).
  • Campaign re-renders unexpectedly across sessions. Check the frequency cap. session is per-browser-session; for cross-session capping use day or ever.
  • Changes don’t propagate. The 5-minute SDK cache delays in-flight sessions. Reload the customer’s site to force a refresh.