Skip to content

Feature: Conditional Flow, Variables & Block Events

Implementation Status

Building BlockStatusDetails
Flow variables + {varName} substitution✅ DoneCE store, template engine, Set Variable operator, {answer} + {answer.position} system vars
"When" conditions on operators✅ DoneAll functional operators, evaluateCondition with =, !=, >, <, >=, <=
Builder polish✅ Done{ autocomplete, custom dropdowns, pills, keyboard nav, highlights, previous choices
CSS classes from variables✅ Donecavai-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✅ FixedClear 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 startedSee onEvent-plan.md
Date/time system variables❌ Not startedSee 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 position to 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.score etc.), 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    ← NEW

Multiple operators can target the same element with different conditions — only the matching one fires.

What changes:

  • Builder: Add condition field 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:

BlockEventTriggerExample use case
FormformSubmitUser submits formHide form, show thank-you
FormformFieldChangeUser changes a fieldLive validation feedback
SliderslideChangeUser navigates to a slideChange CTA text per slide
SliderslideEndUser reaches last slideShow "See more" button
ButtonbuttonClickUser clicks a button blockTrigger flow from visual block
VideovideoEndVideo finishes playingShow CTA after video
VideovideoProgressVideo reaches % threshold"Keep watching!" at 50%
GenericcountdownTimer reaches threshold"Time's up!" at 0s, "Hurry!" at 10s
GenericclickUser clicks target elementSkip button, any CTA
GenericscrollUser scrolls to thresholdReveal 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 onEvent with 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 nextStepId and continue flow.
  • Conditional visibility in builder: Only show relevant event types when matching blocks exist (e.g. formSubmit only when hasFormBlock, slideChange only when slider block exists — following hasConversationBlock pattern).
  • 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: =, +=, -=, toggle

Data 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):

VariableSourceExample 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 submittedtrue
Slider block
{slider.current}Current slide index (1-based)3
{slider.total}Total number of slides5
{slider.isLast}Whether on last slidetrue
Video block
{video.progress}Playback progress (0-100)75
{video.ended}Whether video has finishedfalse
Countdown
{countdown.remaining}Seconds left on active countdown15
{countdown.expired}Whether countdown has reached 0false

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):

VariableSet byExample
{score}Set [score += 1] after correct answer3
{attempts}Set [attempts -= 1] after each question1
{feedback}Set [feedback = "Great job!"] conditionally"Great job!"

How it works in CE

  1. CE maintains a flowVariables: Record<string, any> store at runtime
  2. System variables auto-populate from CE state (last answer, form data, countdown)
  3. Set Variable operator writes to the store
  4. Before rendering any operator text, CE runs template substitution: text.replace(/\{(\w+(?:\.\w+)*)\}/g, (_, key) => flowVariables[key] ?? '')
  5. Re-renders when variables change (reactive)

What this replaces

Before (Tag JS)After (no code)
window.score = 0Set Variable [score = 0]
window.score++Set Variable [score += 1]
el.innerText = 'Score: ' + window.scoreMessage text: "Score: {score}"
el.innerText = userChoiceMessage 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

  1. ✅ CE: flowVariables store + template substitution in operator text rendering
  2. ✅ CE: System variables for {answer}, {answer.position}
  3. ✅ Builder: Set Variable operator (name, operation, value)
  4. ✅ Builder: { autocomplete in text fields (show available variables as pills)

Week 1.5: Conditions + CSS classes + Correct/Wrong ✅ COMPLETE

  1. ✅ Builder + CE: "Only apply when" condition field on all functional operators
  2. ✅ CSS classes from flow variables on widget root (cavai-answer-*, cavai-var-*)
  3. ✅ Correct/wrong answer marking — ✓ toggle on answers + cavai-answer-correct/cavai-answer-wrong
  4. ✅ Builder polish: custom dropdowns, pills, keyboard nav, highlights, previous choices BFS

Live variables + auto quiz (completed)

  1. ✅ Live variables: snapshot vs live split — {answer} frozen, {score} reactive
  2. ✅ Variables in static building blocks (CreativeTextBlock.vue)
  3. ✅ Auto quiz: {score} + {score.total} system vars, auto-increment on correct answer
  4. answer.result variable + refresh bug fix

Week 2: OnEvent (form + countdown) — NOT STARTED

  1. Builder + CE: OnEvent operator infrastructure + formSubmit event type — see onEvent-plan.md
  2. Builder + CE: OnEvent countdown event type
  3. CE: System variables for {form.fieldName}, {countdown.remaining}

Week 3: Block events (slider, video, button) — NOT STARTED

  1. CE: OnEvent slideChange / slideEnd + system variables {slider.current}, {slider.total}, {slider.isLast}
  2. CE: OnEvent videoEnd / videoProgress + system variables {video.progress}, {video.ended}
  3. CE: OnEvent buttonClick
  4. Builder: conditional event type visibility (only show slider events when slider block exists, etc.)

Future polish

  1. Date/time system variables — see dateTime-variables-plan.md
  2. OnEvent: scroll event type
  3. Condition UI: compound conditions (AND/OR)
  4. 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, componentGroupOrder
  • src/pages/Chatbots/components/CavaiFlow/DataHelper.ts:118-238 — step/link/nextStepId assignment
  • src/pages/Chatbots/components/CavaiFlow/OperatorHelper.ts:205-211 — operator inputs/outputs
  • src/pages/Chatbots/components/CavaiFlow/CavaiFlow.vue:2262-2320 — link creation, Answer routing
  • src/pages/Chatbots/components/CavaiFlow/operators/ShowHideOp.vue — multi-target pattern
  • src/pages/Chatbots/components/CavaiFlow/operators/ChangeTextOp.vue — targeting pattern
  • src/pages/Chatbots/components/CavaiFlow/operators/TagOp.vue — JS operator (reference for what variables replace)
  • src/store/modules/builder.ts:256-258hasConversationBlock (pattern for hasFormBlock)
  • 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 flowVariables store)

Composer

  • src/remapper/remapData.ts — operator data remapping to CE format

Internal documentation