Skip to content

ADR: Templated AI Sections and Action Bindings

Status

Draft

Date

2026-03-18

Context

Current state

AI Sections today are rendered by Rose. Clients place:

<div data-rose-section="homepage-hero"></div>

The widget discovers that placeholder, loads the AI Section configuration from the existing config.ai_sections source, and renders our React UI into the page.

This mode works well for quick embeds and should remain available.

Problem

The current model is too rigid for two common client needs.

Need 1: Client-owned markup, Rose-managed content

Some clients want Rose to keep managing the title, suggested questions, optional search bar, and behavior, but they do not want Rose to own the HTML or CSS. They want to place Rose content inside their own design system and page structure.

Need 2: Client-owned markup, client-authored content, Rose-managed behavior

Some clients do not want to pull any AI Section copy from the backoffice at all. They simply want to author buttons or a freeform input directly in their own HTML, while still using Rose to open the chat and send the question.

Examples:

  • "Ask about pricing"
  • "Book a demo"
  • a custom freeform input inside their pricing section

Constraints

  • the current data-rose-section mode must remain fully supported
  • we do not want to introduce a public content API in this ADR
  • if the widget does not load, is blocked, or a section is disabled, we must avoid showing broken interactive UI
  • the solution should work for both full sections and small fragments embedded inside existing customer-owned markup

Decision

We will support three parallel integration modes.

Mode 1: Rose-rendered section

<div data-rose-section="homepage-hero"></div>

Rose owns:

  • content
  • HTML
  • CSS behavior
  • interactions

This remains the default turnkey path.

Mode 2: Template hydration from Rose-managed content

<template data-rose-section-template="homepage-hero">
  <section class="acme-hero">
    <h2 data-rose-title></h2>

    <div class="acme-question-list">
      <button data-rose-repeat="question" data-rose-action="ask">
        <span data-rose-question-text></span>
      </button>
    </div>
  </section>
</template>

Rose owns:

  • content from the existing AI Section config
  • behavior
  • analytics wiring

The client owns:

  • HTML
  • CSS
  • layout
  • visual design

Mode 3: Template action binding with client-authored content

<template data-rose-actions-template="pricing-ctas">
  <div class="acme-pricing__ai-buttons">
    <button
      class="btn btn-secondary"
      data-rose-action="ask"
      data-rose-question="What does pricing look like?"
    >
      Ask about <span class="accent">pricing</span>
    </button>
  </div>
</template>

Rose owns:

  • behavior
  • analytics wiring

The client owns:

  • HTML
  • CSS
  • visible copy
  • layout

This mode does not fetch content from the AI Sections backoffice at all.

Why templates for both advanced modes

We are intentionally using native HTML <template> elements for both Mode 2 and Mode 3.

That gives us safe failure behavior:

  • before hydration, nothing is visible
  • if the widget never loads, the browser keeps the template inert
  • no broken button, empty title, or dead input is shown

This is the main reason not to expose a visible-DOM contract as the primary advanced integration path.

React and SSR note

Native <template> is a good fit for plain HTML, CMS embeds, and website builders, but React-style rendering needs one additional rule.

In React or Next.js, we should not treat <template> as a normal hydrated JSX subtree. Instead, the recommended integration is to emit the template as raw HTML so the browser parser creates template.content natively.

In practice:

  • plain HTML / Webflow / Framer embed: write the native <template> directly
  • React / Next.js: render the template markup via dangerouslySetInnerHTML or an equivalent raw HTML escape hatch

This avoids React hydration mismatches and preserves the expected browser behavior of template.content.

What this ADR does not include

  • no public REST API for AI Section content
  • no generic visible data-rose-question progressive-enhancement mode
  • no builder-specific preview system
  • no change to the current config.ai_sections data model

Contracts

Mode 1 Contract: Rose-rendered section

No change.

<div data-rose-section="homepage-hero"></div>

This continues to use the current AI Sections rendering path.

Mode 2 Contract: Rose-managed content template

Root marker

<template data-rose-section-template="homepage-hero">
  • required
  • the attribute value maps directly to the current AI Section section_id
  • the widget replaces the template in place after successful hydration

Supported markers

Title

<h2 data-rose-title></h2>
  • optional
  • Rose sets textContent from the AI Section title
  • if no title is configured, Rose removes the node
  • if the marker is absent, Rose does not attempt to render a title

Question repeater

<button data-rose-repeat="question" data-rose-action="ask">
  <span data-rose-question-text></span>
</button>
  • optional
  • defines the single repeatable question item for the template
  • Rose clones this node once per configured question
  • v1 supports only one repeater type: question
  • v1 supports only one repeated question region per template

Question text

<span data-rose-question-text></span>
  • optional but normally present inside the repeated item
  • Rose sets textContent to the current question

Ask action

<button data-rose-action="ask">
  • optional
  • when clicked, Rose sends the current repeated question to the widget

Search form

<form data-rose-search-form>
  <input data-rose-search-input />
  <button type="submit" data-rose-search-submit>Ask</button>
</form>
  • optional
  • only rendered when showSearchBar is enabled in the AI Section config
  • Rose sets the placeholder using the same fallback rules as the current AI Sections component
  • form submit sends the typed value to the widget

In other words, Mode 2 search behavior is backoffice-governed:

  • if show_search_bar is false, Rose does not render the search form
  • if show_search_bar is true, Rose renders and binds the authored search form markup

Data reused from the current AI Section config

Mode 2 reuses the current fields:

  • title
  • questions
  • context
  • show_search_bar
  • search_bar_placeholder

Mode 2 does not apply the Rose-rendered styling contract:

  • styling
  • custom_css
  • rendered-mode layout and animation behavior

Those fields belong to the Rose-rendered UI path, not the client-owned-markup path.

Mode 3 Contract: Client-authored action template

Root marker

<template data-rose-actions-template="pricing-ctas">
  • required
  • the value is an optional identifier for analytics and debugging
  • no backoffice content is loaded for this template
  • the widget replaces the template in place after successful hydration

Supported markers

Authored ask action

<button
  data-rose-action="ask"
  data-rose-question="What does pricing look like?"
>
  Ask about <span class="accent">pricing</span>
</button>
  • required for click-to-ask behavior
  • the visible label is fully authored by the client
  • data-rose-question is the hidden message sent to the widget

Authored search form

<form data-rose-search-form>
  <input data-rose-search-input placeholder="Ask anything about pricing" />
  <button type="submit" data-rose-search-submit>Ask</button>
</form>
  • optional
  • the visible placeholder is fully authored by the client
  • submit sends the typed value to the widget

Context

<button
  data-rose-action="ask"
  data-rose-question="What does pricing look like?"
  data-rose-context="User is on pricing page"
>
  • optional
  • prepends hidden context to the message sent to the widget

Mode 3 rules

  • Rose does not fetch title or questions from backoffice
  • Rose does not rewrite the visible copy
  • Rose only binds interactions and analytics
  • Mode 3 is fully decoupled from backoffice AI Section display settings
  • in particular, show_search_bar does not apply to Mode 3
  • if the client includes data-rose-search-form, Rose binds it
  • if the client omits data-rose-search-form, no search UI is shown
  • no AI Section backoffice entry is required for Mode 3

Mode 3 localization

Mode 3 is also fully decoupled from backoffice localization.

That means:

  • the visible label language is owned by the client site
  • the hidden data-rose-question language is also owned by the client site
  • the authored search input placeholder is owned by the client site
  • Rose does not translate or localize authored Mode 3 content

As a consequence, a Mode 3 template can send a question in a different language from the current widget UI language if the authored HTML is inconsistent.

Example:

<button
  data-rose-action="ask"
  data-rose-question="What does pricing look like?"
>
  Quels sont vos tarifs ?
</button>

This is acceptable for Mode 3 because the client is the source of truth for authored content. We must document this responsibility clearly so customers understand that Mode 3 localization consistency is site-owned, not Rose-managed.


Runtime Behavior

Discovery

After the widget is ready, Rose discovers all three modes:

  • [data-rose-section]
  • template[data-rose-section-template]
  • template[data-rose-actions-template]

Each is rendered by a separate path.

Mode 1 flow

No change. Rose uses the current AI Sections discovery and rendered-component flow.

Mode 2 flow

For each template[data-rose-section-template]:

  1. read section_id
  2. load the AI Section config using the existing internal service
  3. if no config exists or the section is disabled, leave the template untouched
  4. clone template.content
  5. bind title
  6. expand the repeated question item
  7. bind optional search form
  8. wire interactions and analytics
  9. replace the template in place

Mode 3 flow

For each template[data-rose-actions-template]:

  1. clone template.content
  2. find all authored ask actions
  3. bind click handlers using each node's data-rose-question
  4. bind any authored search forms
  5. replace the template in place

Failure behavior

Failure behavior is the same for Mode 2 and Mode 3:

  • if the widget is not ready, do nothing
  • if content loading fails in Mode 2, do nothing
  • if a template is malformed, do nothing for that template and continue
  • because the source remains a native <template>, no broken visible UI is shown

React and Next.js integration

For React-based apps, the integration contract is slightly different from plain HTML authoring.

Recommended pattern:

  • React may render the surrounding page structure normally
  • the Rose template itself should be emitted as raw HTML
  • Rose discovers and replaces that template after the page is interactive

Illustrative example for Mode 2:

const roseTemplateHtml = `
  <template data-rose-section-template="homepage-hero">
    <section class="hero">
      <h2 data-rose-title></h2>
      <div class="chips">
        <button data-rose-repeat="question" data-rose-action="ask">
          <span data-rose-question-text></span>
        </button>
      </div>
    </section>
  </template>
`;

export function RoseHeroTemplate() {
  return (
    <div
      style={{ display: 'contents' }}
      dangerouslySetInnerHTML={{ __html: roseTemplateHtml }}
    />
  );
}

Illustrative example for Mode 3:

const roseActionsHtml = `
  <template data-rose-actions-template="pricing-ctas">
    <button
      data-rose-action="ask"
      data-rose-question="What does pricing look like?"
    >
      Ask about <span class="accent">pricing</span>
    </button>
  </template>
`;

export function PricingActionsTemplate() {
  return (
    <div
      style={{ display: 'contents' }}
      dangerouslySetInnerHTML={{ __html: roseActionsHtml }}
    />
  );
}

This is compatible with SSR in Next.js because the server can output the template markup normally, but the browser still parses it as a native <template> before Rose hydrates it.

Why replace the template in place

Replacing the <template> where it already sits makes placement explicit and keeps the authoring model easy to explain.

Example:

<div class="page-block">
  <p>Above</p>

  <template data-rose-actions-template="pricing-ctas">
    <button
      data-rose-action="ask"
      data-rose-question="What does pricing look like?"
    >
      Ask about pricing
    </button>
  </template>

  <p>Below</p>
</div>

If the widget hydrates successfully, the button appears exactly where the template was.


Analytics and Attribution

All three modes should use the existing widget trigger path.

Mode 1 and Mode 2 should preserve current AI Section analytics semantics:

  • button click source: ai_section_button
  • search submit source: ai_section_search_bar

Mode 2 attribution should continue to rely on the existing backoffice AI Section identity:

  • integration_mode = rendered or template_managed
  • content_source = backoffice
  • section_id = <existing ai_sections.section_id>

Mode 3 also needs first-class attribution even though no AI Section config is loaded.

For Mode 3, Rose should explicitly track where the action came from using the template identifier and authored question:

  • integration_mode = template_authored
  • content_source = authored_html
  • section_id = null
  • template_id = <data-rose-actions-template value>
  • question_text = <data-rose-question value or submitted freeform text>

This keeps visibility on where prompts originate even when the visible copy is fully owned by the client.

Illustrative examples:

{
  "integration_mode": "template_managed",
  "content_source": "backoffice",
  "section_id": "homepage-hero",
  "template_id": null,
  "trigger_type": "button",
  "question_text": "How does pricing work?"
}
{
  "integration_mode": "template_authored",
  "content_source": "authored_html",
  "section_id": null,
  "template_id": "pricing-ctas",
  "trigger_type": "button",
  "question_text": "What does pricing look like?"
}

This ADR does not fix the exact PostHog event names for Mode 3, but it does require:

  • distinct attribution from typed user messages
  • support for optional template identifiers
  • support for optional hidden context

Consequences

Positive

  • we keep the current turnkey embed path unchanged
  • we support client-owned markup with Rose-managed content
  • we support client-owned markup with client-authored content
  • native templates give safe failure behavior by default
  • no public API is required
  • the implementation reuses the current AI Sections content source and widget trigger path

Negative

  • native templates are not WYSIWYG in many visual builders
  • advanced integrations use a narrower, explicit contract rather than a flexible mini-template engine
  • backoffice styling and custom CSS do not apply in Mode 2
  • Mode 3 content lives outside backoffice, so content changes require customer HTML changes

Neutral

  • config.ai_sections remains the source of truth for Mode 1 and Mode 2
  • the current rendered AI Sections UI remains the best option for fast setup
  • Mode 2 and Mode 3 are advanced integration paths for clients who want design control

Implementation Scope

What already exists and can be reused

Component Status Notes
window.InboundXWidget.triggerQuestion() Exists Core interaction mechanism
External trigger event handling Exists Already expands the widget and sends messages
config.ai_sections table Exists Current content source for Mode 1 and Mode 2
aiSectionsService.ts Exists Current config fetch and transform logic
data-rose-section discovery Exists Current rendered-mode auto-discovery
Backoffice AI Sections editor Exists Continues to manage Mode 1 and Mode 2 content

What needs to be built

Component Effort Description
Template discovery Small Discover template[data-rose-section-template] and template[data-rose-actions-template]
Mode 2 hydrator Medium Clone content, bind title, expand questions, wire search
Mode 3 binder Small Clone content, bind authored actions and authored search forms
Shared action binding helpers Small Reuse trigger/context wiring across Mode 2 and Mode 3
Discovery API update Small Auto-init all three modes
Analytics extension Small Add explicit Mode 2 vs Mode 3 attribution fields
Documentation Medium Document the three contracts, examples, and ownership rules for search/localization in Mode 3
Backoffice examples Small Show template-mode examples alongside the current embed code

Out of scope for this ADR

  • public section content API
  • visible progressive-enhancement mode for data-rose-question outside templates
  • builder-specific preview tooling
  • deprecating or replacing the current data-rose-section path

Real-World Examples

Case 1: Fast turnkey embed

Use this when the client wants the quickest setup and is happy for Rose to own the rendered UI.

<div data-rose-section="homepage-hero"></div>

Typical use:

  • small marketing team
  • no custom frontend work
  • backoffice manages both content and presentation

Case 2: Custom design, Rose-managed content

Use this when the client wants Rose to keep managing titles, prompts, and optional search, but wants to render them inside their own design system.

<template data-rose-section-template="homepage-hero">
  <section class="acme-hero">
    <p class="acme-eyebrow">AI assistant</p>
    <h2 class="acme-title" data-rose-title></h2>

    <div class="acme-question-list">
      <button class="acme-chip" data-rose-repeat="question" data-rose-action="ask">
        <span data-rose-question-text></span>
      </button>
    </div>

    <form class="acme-search" data-rose-search-form>
      <input class="acme-search__input" data-rose-search-input />
      <button class="acme-search__submit" type="submit" data-rose-search-submit>
        Ask
      </button>
    </form>
  </section>
</template>

Typical use:

  • design-led site
  • content still managed in Rose backoffice
  • client wants their own markup and CSS

Case 3: Custom design, client-authored content, Rose behavior only

Use this when the client wants to hand-author the visible CTA text or search UI and only use Rose to open the widget and send the question.

<section class="acme-pricing">
  <h2>Simple pricing</h2>
  <p>Start fast, scale later.</p>

  <div class="acme-pricing__actions">
    <a href="/book-demo" class="btn btn-primary">Book demo</a>

    <template data-rose-actions-template="pricing-ctas">
      <div class="acme-pricing__ai-buttons">
        <button
          class="btn btn-secondary"
          data-rose-action="ask"
          data-rose-question="What does pricing look like?"
        >
          Ask about <span class="accent">pricing</span>
        </button>

        <button
          class="btn btn-secondary"
          data-rose-action="ask"
          data-rose-question="Can I book a demo?"
        >
          Book a <span class="accent">demo</span>
        </button>
      </div>
    </template>
  </div>
</section>

Typical use:

  • client wants complete control over the visible copy
  • no need to sync CTA text with Rose backoffice
  • client wants styled words, custom labels, or page-specific phrasing
  • search UI should be controlled directly in frontend markup, not by backoffice flags
  • Rose is only responsible for opening chat and sending the hidden question