Skip to content

Post-Conversion Triggers

Overview

Post-conversion qualification is the flow that opens the Rose widget after a visitor submits a client form (e.g., demo request, free trial signup), asking additional profiling questions to enrich the lead before it hits the CRM.

There are two activation modes, configurable per form via qualification.profiling.forms.<formId>.trigger:

  1. Thank-you page (trigger.pages) — the widget activates when the visitor lands on a configured thank-you URL after submitting the client form.
  2. Inline form-submit (trigger.on_form_submit) — the widget activates on the same page the form lives on, when the client form submits but does not redirect (inline AJAX confirmations like Unbounce, HubSpot embeds, etc.).

Both modes can be configured simultaneously; whichever condition fires first activates the form.

IX-2646

The inline form-submit path was introduced for landing pages that show an in-place confirmation instead of redirecting to a thank-you URL. See the Linear ticket for the original use case (Matera).

When to Use Each Mode

Client behaviour after submit Mode
Redirects to a dedicated thank-you page trigger.pages
Shows an inline success message, stays on the same URL trigger.on_form_submit
Mixed across pages Both — each page falls through to whichever matches

Mode 1 — Thank-You Page (trigger.pages)

The default post-conversion path. When the visitor lands on a URL matching one of the configured patterns, SiteConfigManager.getActivePostConversionForm returns the form and the widget auto-opens into qualification.

{
  "trigger": {
    "type": "post_conversion",
    "pages": ["/thank-you*", "/merci*"]
  }
}
  • pages uses the same wildcard syntax as the widget's display_only_on_urls (* matches any substring).
  • No event wiring required — the widget's existing URL-change listener re-evaluates the display decision on navigation.

Mode 2 — Inline Form-Submit (trigger.on_form_submit)

When the client form doesn't redirect, Rose listens for the form submission event, persists an activation flag, and dispatches a custom window event so the widget's display controller switches into post-conversion mode on the same page.

{
  "trigger": {
    "type": "post_conversion",
    "pages": [],
    "on_form_submit": {
      "enabled": true,
      "pages": ["/landing/demo*"],
      "form_action_matches": ["https://get.example.com/fsg"]
    }
  }
}

Three safety gates

All three must pass before the qualification form activates:

  1. enabled: true — explicit opt-in per form.
  2. pages match — current URL matches a configured landing-page pattern.
  3. form_action_matches match — the submitted form's action attribute matches a configured URL pattern.

The third gate is non-optional: an empty form_action_matches blocks activation even when enabled: true. This is a safety gate — without a per-client form-action contract, any stray submission on the page (newsletter signup, search box, cookie banner) would trigger qualification.

How to fill on_form_submit

  1. Open the landing page in DevTools → Network tab → submit the form with test data.
  2. Look at the POST request's URL. That's the form action URL — paste it (or a pattern with *) into form_action_matches.
  3. Add the landing page path to pages (the URL where the form lives, not the thank-you URL — there isn't one).
  4. Set enabled: true.

How it works end-to-end

sequenceDiagram participant V as Visitor participant FD as FormDetectionManager participant AI as analytics/index.ts participant PC as PostConversionFormSubmitTracker participant SS as sessionStorage participant W as window (event bus) participant DC as DisplayController participant SCM as SiteConfigManager participant RW as RoseWidget V->>FD: Submits client form (XHR/fetch) FD->>AI: onFormSubmit(data) [status=success] AI->>PC: notifyFormSubmit(domain, data) PC->>SCM: getResolverForDomain(domain) SCM-->>PC: profiling config PC->>PC: check enabled + pages + form_action_matches alt all three gates pass PC->>SS: setItem(rose_pc_form_submit_activated_{domain}, formId) Note right of PC: Dispatch only on successful write PC->>W: dispatchEvent(FORM_SUBMIT_ACTIVATION_EVENT) W->>DC: handler() DC->>DC: guard: prevDecision.allowed must be true DC->>SCM: shouldUsePostConversion(domain) SCM->>SS: getItem(rose_pc_form_submit_activated_{domain}) SS-->>SCM: formId SCM-->>DC: { formId, config } DC->>RW: setDisplayDecision({...prev, isPostConversion: true}) RW->>V: Auto-opens qualification form else any gate fails PC->>PC: debug log, skip end

Key invariants

  • prevDecision.allowed must already be true. The form-submit event only flips an already-displayed widget into post-conversion mode. It does not force the widget to appear on a page where display rules would otherwise hide it. If you configure on_form_submit on a page that isn't in display_only_on_urls / page_display_rules, you'll see a ⚠️ DisplayController: form-submit activation ignored warning in the console — add the page to the widget's display rules.
  • Dispatch is gated on a successful sessionStorage.setItem. If storage is blocked (privacy extensions, ITP in iframes, quota exceeded), the warning is logged and the event is not dispatched, avoiding a silent no-op that would falsely report "activation triggered".
  • Activation is session-scoped (until tab close). isFormSubmitActivated reads from sessionStorage, so re-opens on revisits within the same tab are intentional — the visitor is nudged back to finish qualification. Two higher-level gates in RoseWidget prevent unwanted re-prompts:
    • completedPostConversion (localStorage) — skip auto-open if qualification already finished.
    • rose_pcq_dismissed_{domain} (sessionStorage) — skip auto-open if the visitor explicitly dismissed the qualification.

How to verify in production

  1. Submit the form on the landing page with test data.
  2. Rose widget should open into the qualification form within ~1 second, on the same page.
  3. In PostHog, confirm rw_client_form_submitted fires, followed by rw_widget_impression with isPostConversion=true in the same session.

Configuration Reference

The JSON schema lives at schemas/configs/website/qualification.schema.json. Generated types:

  • TypeScript — frontend/shared/src/types/configs.generated.ts (QualificationProfilingFormsValueTriggerOnFormSubmit)
  • Python — backend/packages/ixconfig/ixconfig/models/generated.py
Layer File
Form submission listener frontend/shared/src/config/PostConversionFormSubmitTracker.ts
Display re-evaluation frontend/widget/src/components/DisplayController.tsx
Active form resolution frontend/shared/src/config/SiteConfigManager.ts (getActivePostConversionForm)
Analytics hookup frontend/shared/src/analytics/index.ts (onFormSubmitnotifyFormSubmit)
Widget auto-open gates frontend/shared/src/components/RoseWidget.tsx