Appearance
Feature: Conditional Flow, Variables & Block Events
Implementation Status
| Building Block | Status | Details |
|---|---|---|
Flow variables + {varName} substitution | ✅ Done | CE store, template engine, Set Variable operator, {answer} + {answer.position} system vars |
| "When" conditions on operators | ✅ Done | All functional operators, evaluateCondition with =, !=, >, <, >=, <= |
| Builder polish | ✅ Done | { autocomplete, custom dropdowns, pills, keyboard nav, highlights, previous choices |
| CSS classes from variables | ✅ Done | cavai-answer-*, cavai-var-*, cavai-answer-correct/wrong on .creative-outer-container |
| Correct/wrong answer marking | ✅ Done | ✓ toggle on answers, cavai-answer-correct/cavai-answer-wrong classes, Composer passthrough |
answer.result variable | ✅ Done | "correct" / "wrong" string for templates + conditions |
| Refresh bug | ✅ Fixed | Clear reactive proxy in-place + strip DOM classes |
| Live variables in blocks | ✅ Done | {score} in static Text blocks updates reactively |
| Auto quiz scoring | ✅ Done | {score} + {score.total} auto-calculated from ✓ markers |
| OnEvent trigger operator | ❌ Not started | See onEvent-plan.md |
| Date/time system variables | ❌ Not started | See dateTime-variables-plan.md |
See poc-implementation.md for detailed implementation log.
Problem Statement
Many common use cases require custom JavaScript (Tag operator) today:
- "If user chose answer 1, show message A; if answer 2, show message B"
- "After form submit, hide form and show thank-you message"
- "When slider reaches slide 3, change the CTA text"
- "Show different images depending on the path taken"
- "Run a quiz with score tracking"
- "After countdown, show a different offer"
These are things non-technical users (designers, campaign managers) should be able to do without writing code. The system should be block-agnostic — not just form-specific, but events and variables for every block type that has meaningful interactions.
Architecture Context
Builder (Vue 2.7) produces a static JSON (operators + links). CE (Vue 3) executes it at runtime.
What exists today
- Answer routing: An Answer operator can have multiple outgoing links. CE uses
positionto route based on which choice was clicked. This is the ONLY conditional routing in the system. - ChangeText/ChangeImage/ShowHide: Target specific elements but execute unconditionally — always apply when reached in the flow.
- Tag (JS): Can do anything (including
window.scoreetc.), but requires coding knowledge. - Single
nextStepId: Every non-Answer operator has exactly one next step. No branching. - No state/variables: Operators are stateless. No way to say "remember what the user chose earlier."
Core limitation
Answer branching works, but each branch is a completely separate flow path. If you want 3 answers to lead to slightly different versions of the same next steps, you must duplicate the entire downstream flow 3 times. That's the pain point.
The Plan: 3 building blocks that combine into full power
We skip a dedicated If/Else branching operator — conditions directly on operators (Solution 1) give the same power with less flow clutter. Three things to build:
1. "When" conditions on existing operators (LOW effort, HIGH value)
Add an optional "Only apply when" field to ChangeText, ChangeImage, ShowHide, CSS, and other modify operators.
User experience:
[ChangeText operator]
Target: Message 1
New text: "Correct! Well done!"
▸ Only apply when: Answer 1 of Question 1 ← NEWMultiple operators can target the same element with different conditions — only the matching one fires.
What changes:
- Builder: Add
conditionfield to operator body. UI: collapsible "Only apply when" section with dropdown to pick an Answer or variable condition. - CE: Before applying a change operator, evaluate condition. Skip if not met.
- Data contract:
body.condition: { type: 'answer', operator: 'answer_0', position: 1 }or{ type: 'variable', name: 'score', op: '>=', value: 3 }
What this unlocks:
- Conditional text/image/visibility changes
- Quiz right/wrong feedback without JS
- Personalized content based on user choices
- Combined with variables: conditional on accumulated state
Effort: ~3-5 days (builder + CE).
2. "OnEvent" trigger operator (LOW-MEDIUM effort)
A new operator type that waits for an event, then advances to the next linked step.
This is block-agnostic by design. Every block type that has meaningful user interactions can emit events. The OnEvent operator is one generic component — just a dropdown of event types.
Event types by block:
| Block | Event | Trigger | Example use case |
|---|---|---|---|
| Form | formSubmit | User submits form | Hide form, show thank-you |
| Form | formFieldChange | User changes a field | Live validation feedback |
| Slider | slideChange | User navigates to a slide | Change CTA text per slide |
| Slider | slideEnd | User reaches last slide | Show "See more" button |
| Button | buttonClick | User clicks a button block | Trigger flow from visual block |
| Video | videoEnd | Video finishes playing | Show CTA after video |
| Video | videoProgress | Video reaches % threshold | "Keep watching!" at 50% |
| Generic | countdown | Timer reaches threshold | "Time's up!" at 0s, "Hurry!" at 10s |
| Generic | click | User clicks target element | Skip button, any CTA |
| Generic | scroll | User scrolls to threshold | Reveal content on scroll |
User experience:
[On Form Submit] → ShowHide [hide: Form, show: Thank You]
[On Slide Change: slide 3] → ChangeText [target: CTA, text: "Buy now — last slide!"]
[On Video End] → ShowHide [show: CTA overlay]
[On Countdown: 0s remaining] → ShowHide [hide: Quiz, show: "Time's up!"]
[On Button Click: "Buy"] → Set [conversions += 1]What changes:
- Builder: New operator type
onEventwith event-type dropdown. Minimal UI — pick event type, configure params (e.g. slide number, seconds, percentage), output link. - CE: Register event listener at runtime per block type. On event, resolve
nextStepIdand continue flow. - Conditional visibility in builder: Only show relevant event types when matching blocks exist (e.g.
formSubmitonly whenhasFormBlock,slideChangeonly when slider block exists — followinghasConversationBlockpattern). - Data contract:
body: { eventType: 'slideChange', params: { slideIndex: 3 } }
MVP priority: Start with formSubmit and countdown (most requested). Then slideChange/slideEnd, buttonClick, videoEnd. Each new event type is ~1 day of work since the infrastructure is shared.
Effort: ~2-3 days for core OnEvent + first two event types. ~1 day per additional event type.
3. Flow variables with inline template syntax (MEDIUM effort, TRANSFORMATIVE value)
A key-value store that operators can write to, and that any text field can read via {variable} syntax.
This is the key insight: instead of using ChangeText operators to set text conditionally, put variables directly in message text. The CE template-renders them at display time.
Set Variable operator
Simple new operator node:
[Set Variable]
Name: score ← dropdown of existing + free text for new
Operation: += 1 ← dropdown: =, +=, -=, toggleData contract: body: { variable: 'score', operation: '+=', value: 1 }
Inline variable syntax in all text fields
Any operator text field supports {variableName}. In the builder, variables render as pills/tags (colored bubbles) for discoverability:
Message operator text field:
┌─────────────────────────────────────────────────┐
│ Du svarte [answer] og scoren din er [score]/3 │
│ │
│ (where [answer] and [score] are colored pills) │
└─────────────────────────────────────────────────┘Inserting variables:
- Type
{to trigger autocomplete dropdown listing available variables - Or click a "insert variable" button that shows a pill picker
- Similar UX to Notion mentions (
@) or Slack's autocomplete
System variables (built-in, auto-populated from block state):
| Variable | Source | Example value |
|---|---|---|
| Flow | ||
{answer} | Text of last selected answer | "Oslo" |
{answer.position} | Index of last selected answer (1-based) | 2 |
{answer.question} | Text of the question that was answered | "Capital of Norway?" |
| Form block | ||
{form.fieldName} | Value from named form field | "john@example.com" |
{form.submitted} | Whether form has been submitted | true |
| Slider block | ||
{slider.current} | Current slide index (1-based) | 3 |
{slider.total} | Total number of slides | 5 |
{slider.isLast} | Whether on last slide | true |
| Video block | ||
{video.progress} | Playback progress (0-100) | 75 |
{video.ended} | Whether video has finished | false |
| Countdown | ||
{countdown.remaining} | Seconds left on active countdown | 15 |
{countdown.expired} | Whether countdown has reached 0 | false |
These populate automatically as blocks update their state. No setup needed — they're just available for use in {variable} syntax and in "Only apply when" conditions.
User-defined variables (set by Set Variable operator):
| Variable | Set by | Example |
|---|---|---|
{score} | Set [score += 1] after correct answer | 3 |
{attempts} | Set [attempts -= 1] after each question | 1 |
{feedback} | Set [feedback = "Great job!"] conditionally | "Great job!" |
How it works in CE
- CE maintains a
flowVariables: Record<string, any>store at runtime - System variables auto-populate from CE state (last answer, form data, countdown)
- Set Variable operator writes to the store
- Before rendering any operator text, CE runs template substitution:
text.replace(/\{(\w+(?:\.\w+)*)\}/g, (_, key) => flowVariables[key] ?? '') - Re-renders when variables change (reactive)
What this replaces
| Before (Tag JS) | After (no code) |
|---|---|
window.score = 0 | Set Variable [score = 0] |
window.score++ | Set Variable [score += 1] |
el.innerText = 'Score: ' + window.score | Message text: "Score: {score}" |
el.innerText = userChoice | Message text: "You picked {answer}" |
form.addEventListener('submit', ...) | OnEvent [formSubmit] → next step |
Effort: ~1-2 weeks for Set Variable operator + CE variable store + template renderer + builder pill UI.
Combined: The Full Picture
All three building blocks work independently but combine into something powerful. Here's a product showcase creative using all of them:
Example A: Timed quiz
Set [score = 0]
│
├── OnEvent [countdown: 30s] → ShowHide [hide: Quiz, show: Time's Up]
│ ChangeText [target: Result, text: "You got {score}/3"]
│
├── Question 1: "Capital of Norway?"
│ → "Oslo" (correct) → Set [score += 1] ─┐
│ → "Bergen" ─────────────────────┤
│ → "Tromsø" ─────────────────────┘
│ → Message: "You answered {answer}."
│ ChangeText [target: Feedback, text: "Correct!", when: answer.position = 1]
│ ChangeText [target: Feedback, text: "Nope, it was Oslo", when: answer.position ≠ 1]
│
├── Question 2 + 3: ...same pattern...
│
└── ShowHide [hide: Countdown]
Message: "Final score: {score}/3"
ChangeText [target: Result, text: "Perfect! 🏆", when: score = 3]Example B: Product slider with dynamic CTA + lead capture
OnEvent [slideChange: slide 1] → ChangeText [target: CTA, text: "Explore the {slider.current} of {slider.total}"]
OnEvent [slideChange: slide 3] → ChangeText [target: CTA, text: "This is our bestseller!"]
ChangeImage [target: Badge, image: bestseller-badge.png]
OnEvent [slideEnd] → ShowHide [show: Form block]
ChangeText [target: CTA, text: "Want to know more? Leave your details!"]
OnEvent [formSubmit] → ShowHide [hide: Form, show: Confirmation]
Message: "Thanks {form.name}! We'll reach out at {form.email}."Example C: Video ad with timed reveal
OnEvent [videoProgress: 50%] → ShowHide [show: "Teaser text"]
OnEvent [videoEnd] → ShowHide [hide: Video, show: CTA overlay]
ChangeText [target: CTA, text: "Get 20% off — limited time!"]
OnEvent [countdown: 60s] → ChangeText [target: CTA, text: "Offer expires in {countdown.remaining}s!"]
OnEvent [countdown: 0s] → ShowHide [hide: CTA, show: "Offer expired"]
OnEvent [buttonClick: "Buy"] → ShowHide [show: Form]
OnEvent [formSubmit] → Message: "Order confirmed, {form.name}!"What these show:
- Set Variable tracks state across interactions
- {variables} render inline — no ChangeText needed for basic dynamic text
- When conditions handle branching logic without duplicating paths
- OnEvent reacts to any block interaction — form, slider, video, button, countdown
- Zero JavaScript in all three examples
MVP Implementation Order
Week 1: Variables + inline syntax ✅ COMPLETE
- ✅ CE:
flowVariablesstore + template substitution in operator text rendering - ✅ CE: System variables for
{answer},{answer.position} - ✅ Builder: Set Variable operator (name, operation, value)
- ✅ Builder:
{autocomplete in text fields (show available variables as pills)
Week 1.5: Conditions + CSS classes + Correct/Wrong ✅ COMPLETE
- ✅ Builder + CE: "Only apply when" condition field on all functional operators
- ✅ CSS classes from flow variables on widget root (
cavai-answer-*,cavai-var-*) - ✅ Correct/wrong answer marking — ✓ toggle on answers +
cavai-answer-correct/cavai-answer-wrong - ✅ Builder polish: custom dropdowns, pills, keyboard nav, highlights, previous choices BFS
Live variables + auto quiz (completed)
- ✅ Live variables: snapshot vs live split —
{answer}frozen,{score}reactive - ✅ Variables in static building blocks (CreativeTextBlock.vue)
- ✅ Auto quiz:
{score}+{score.total}system vars, auto-increment on correct answer - ✅
answer.resultvariable + refresh bug fix
Week 2: OnEvent (form + countdown) — NOT STARTED
- Builder + CE: OnEvent operator infrastructure + formSubmit event type — see onEvent-plan.md
- Builder + CE: OnEvent countdown event type
- CE: System variables for
{form.fieldName},{countdown.remaining}
Week 3: Block events (slider, video, button) — NOT STARTED
- CE: OnEvent slideChange / slideEnd + system variables
{slider.current},{slider.total},{slider.isLast} - CE: OnEvent videoEnd / videoProgress + system variables
{video.progress},{video.ended} - CE: OnEvent buttonClick
- Builder: conditional event type visibility (only show slider events when slider block exists, etc.)
Future polish
- Date/time system variables — see dateTime-variables-plan.md
- OnEvent: scroll event type
- Condition UI: compound conditions (AND/OR)
- Variable operations: toggle, append, min/max
UX Principles
Variables are the primary interface, not operators. The Set Variable operator is just plumbing. The real UX is typing {score} in a message and seeing it render as a pill. That's the "aha" moment.
Conditions should be hidden by default. Most operators don't need them. Show "Only apply when..." as a collapsible section at the bottom. Click to expand, pick condition. Keeps the simple case simple.
Autocomplete is essential. Typing { must show a dropdown of available variables. Without it, users won't discover the feature. With it, it's self-documenting.
Don't over-engineer conditions. Start with simple equality: "when Answer X", "when score = 3". Compound conditions (AND/OR) can come later. Simple covers 90% of cases.
Key Files
Builder
src/utils/_temp_buildercomponents.ts— operator definitions,componentGroupOrdersrc/pages/Chatbots/components/CavaiFlow/DataHelper.ts:118-238— step/link/nextStepId assignmentsrc/pages/Chatbots/components/CavaiFlow/OperatorHelper.ts:205-211— operator inputs/outputssrc/pages/Chatbots/components/CavaiFlow/CavaiFlow.vue:2262-2320— link creation, Answer routingsrc/pages/Chatbots/components/CavaiFlow/operators/ShowHideOp.vue— multi-target patternsrc/pages/Chatbots/components/CavaiFlow/operators/ChangeTextOp.vue— targeting patternsrc/pages/Chatbots/components/CavaiFlow/operators/TagOp.vue— JS operator (reference for what variables replace)src/store/modules/builder.ts:256-258—hasConversationBlock(pattern forhasFormBlock)src/interfaces/chatbot.ts:14-161— OperatorProperties interface
Creative Engine
- Form submission handler (needs event hook for OnEvent)
- Step execution logic (needs condition evaluation)
- Text rendering (needs template substitution for
{variables}) - Runtime context (needs
flowVariablesstore)
Composer
src/remapper/remapData.ts— operator data remapping to CE format