Skip to content

Custom Scripts (Tag/JS Operator)

The Tag operator (displayed as "JS" in the Builder) lets users write custom JavaScript that executes during the conversation flow. This document covers how custom scripts work and common pitfalls.

How It Works

  1. User writes JavaScript in the Tag operator's textarea (or the large code editor)
  2. The code is stored in operator.properties.body.inputText.value, wrapped in <script> tags
  3. At runtime, Script.ts in the Creative Engine extracts the code, creates a <script> element, appends it to document.head, and removes it after execution
User writes code in Builder (TagOp.vue / OperatorLargeCode.vue)
  |
Stored as: <script>user code here</script>
  |
Composer bakes it into the published creative
  |
Engine flow reaches the operator
  |
Script.ts extracts code, appends <script> to document.head
  |
Code executes synchronously, <script> element removed

Execution Context

  • Scripts run inside the creative's iframe (preview iframe or published creative iframe)
  • document refers to the creative's document, not the Builder
  • window is the creative's window object
  • Scripts have access to special globals when detected in the code: DATASTORE, ANALYTICS, changeText(), changeImage(), changeUrl(), changeVideo(), showElement(), hideElement(), resetCreative(), restartFlow()

Gotchas

innerText returns empty on flow elements

Problem: document.querySelector('.response').innerText returns an empty string even though the element exists and contains text.

Cause: innerText respects CSS rendering. If responseProperties.hidden is true in the conversation block settings, the response bubble gets visibility: hidden (from MessageHolder.vue styles). The element still exists in the DOM -- querySelector finds it and innerHTML returns its content -- but innerText returns empty because the element is visually hidden.

How to hit this: Toggle the "Hidden" switch on Response properties in the Conversation block configuration. This is easy to do accidentally while styling messages and choices.

Fix: Use textContent instead of innerText:

js
// Bad -- returns empty when response has visibility: hidden
const answer = document.querySelector('.response').innerText

// Good -- always returns the text content regardless of CSS
const answer = document.querySelector('.response').textContent.trim()
PropertyRespects CSS?Returns empty when visibility: hidden?
innerHTMLNoNo (returns raw HTML)
textContentNoNo (returns raw text)
innerTextYesYes

This applies to any element styled with visibility: hidden or display: none. When querying flow elements (messages, choices, responses) from custom scripts, prefer textContent over innerText to avoid silent failures from styling changes.

Script execution timing

Scripts execute synchronously when the flow reaches them. The engine appends a <script> element to document.head -- it runs immediately, then gets removed. This means:

  • Vue DOM updates from the previous flow step (e.g. rendering a response bubble after a choice click) may not have happened yet -- they are queued for the next microtask
  • If querying elements created by the immediately preceding step, wrap in setTimeout:
js
setTimeout(() => {
  const el = document.querySelector('.response')
  // ...
}, 50)
  • All globals set on window (e.g. window.quizScore) persist across script operators within the same creative session
  • After a preview restart, window properties survive but stored DOM references become stale because preview.innerHTML is cleared and the creative remounts

CSS class names in the creative DOM

Flow elements use these CSS classes for querying:

ElementClassGenerated by
Message bubble.messagetranslateFlowComponentType('Text')
Choice buttons.choiceComponent type
Response (answered choice).responsetranslateFlowComponentType('Choice', true)
Inner text content.msg-innerText.vue template
Message content wrapper.message-contentMessageHolder.vue template
Text block.t1, .t2, ...Block abbreviation
Button block.b1, .b2, ...Block abbreviation
Graphic block.g1, .g2, ...Block abbreviation

Note: Class name obfuscation exists (obfuscator.ts) but is currently disabled (DataStore.obfuscateStyles defaults to false). If enabled in the future, all these class names will be randomized and custom scripts relying on them will break.

Key Source Files

FileRepoPurpose
src/components/blocks/functional/Script.tsCreative-EngineScript execution, special access injection
src/logic-system/processors/conversationFlow.tsCreative-EngineFlow step processing, calls funcBlocks.Tag()
src/utils/funcBlocks.tsCreative-EngineMaps operator types to handler functions
src/components/conversationflow/MessageHolder.vueCreative-EngineRenders messages, choices, responses
src/components/blocks/basic/Text.vueCreative-EngineRenders text content inside bubbles
src/pages/.../operators/TagOp.vueApplication-FrontendBuilder textarea for script input
src/pages/.../flowactors/OperatorLargeCode.vueApplication-FrontendLarge code editor overlay

Internal documentation