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:
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-sectionmode 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¶
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
dangerouslySetInnerHTMLor 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-questionprogressive-enhancement mode - no builder-specific preview system
- no change to the current
config.ai_sectionsdata model
Contracts¶
Mode 1 Contract: Rose-rendered section¶
No change.
This continues to use the current AI Sections rendering path.
Mode 2 Contract: Rose-managed content template¶
Root marker¶
- 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¶
- optional
- Rose sets
textContentfrom 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¶
- optional but normally present inside the repeated item
- Rose sets
textContentto the current question
Ask action¶
- 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
showSearchBaris 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_baris false, Rose does not render the search form - if
show_search_baris true, Rose renders and binds the authored search form markup
Data reused from the current AI Section config¶
Mode 2 reuses the current fields:
titlequestionscontextshow_search_barsearch_bar_placeholder
Mode 2 does not apply the Rose-rendered styling contract:
stylingcustom_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¶
- 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-questionis 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_bardoes 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-questionlanguage 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]:
- read
section_id - load the AI Section config using the existing internal service
- if no config exists or the section is disabled, leave the template untouched
- clone
template.content - bind title
- expand the repeated question item
- bind optional search form
- wire interactions and analytics
- replace the template in place
Mode 3 flow¶
For each template[data-rose-actions-template]:
- clone
template.content - find all authored ask actions
- bind click handlers using each node's
data-rose-question - bind any authored search forms
- 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 = renderedortemplate_managedcontent_source = backofficesection_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_authoredcontent_source = authored_htmlsection_id = nulltemplate_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_sectionsremains 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-questionoutside templates - builder-specific preview tooling
- deprecating or replacing the current
data-rose-sectionpath
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.
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