English

xr/studio Documentation

Custom Code API

Script your story with the Custom Code action: the cyango namespace, its methods and types, reading GUI inputs, variables, navigation, and examples.

Custom Code

The Custom Code action runs TypeScript when its event fires, such as On Click, On Scene Ready, or On Language Changed. Use it when the built-in action types are not enough, for example to read form inputs, branch on device or language, keep values between scenes, or drive the story directly.

To add it, open the Actions tab on an entity (or select the scene), pick an event type, and add a Custom Code action. The editor has TypeScript autocomplete — type cyango. to see what is available.

When you save the story, Studio compiles every custom code block to JavaScript. The player runs the compiled output only, so you get type-aware editing without shipping the TypeScript compiler to viewers. Fix any compile errors shown in the editor before publishing.

The sandbox

Your code runs inside a secure sandbox, wrapped in an async function. In practice:

  • You can use await directly, and return to stop early.
  • console.log, setTimeout, setInterval (capped at 60 seconds) and fetch are available.
  • DOM globals are blocked: window, document, location, navigator, history, eval. Use cyango.utils.getWindowLocation() and cyango.utils.getNavigator() instead.
  • Errors are caught and logged to the browser console, so a broken snippet will not crash the story.

Everything Cyango exposes lives on one global object, cyango.

A story also has two blocks that run once, when the story loads. Open Settings, then Head and Footer Code. Head runs first, and Footer runs last. Both use the same editor and the same sandbox as a Custom Code action, with their own snippets in the panel beside the code.

Use them to prepare the story before a visitor sees it, or to report on it afterwards:

  • Set the language or the quality level from the URL, before the first scene is shown.
  • Load a configuration from your backend into variables that every action can read.
  • Install Google Analytics, Google Tag Manager or any other tag.
  • Write the SEO and sharing tags: canonical, robots, Open Graph, Twitter cards and JSON-LD.

The head object

These two blocks get one thing no other block has: head, which writes into the page. document and window stay blocked everywhere, so this is how a tag gets on the page.

MethodWhat it does
head.addScript(src, options)Loads an external script, once. await it when the tag has to be configured afterwards
head.addInlineScript(code, options)Runs JavaScript in the page, which is the only way to reach window globals such as dataLayer and gtag
head.setMeta(name, content)Sets a name meta tag, such as description or robots
head.setMetaProperty(property, content)Sets a property meta tag, which is what Open Graph uses
head.setLink(rel, href, options)Sets a link tag, such as canonical or an alternate per language
head.setJsonLd(data, options)Writes a JSON-LD block
head.setTitle(title)Sets the page title. The story name is used by default

Every call is idempotent. A block that runs again updates its tags instead of adding a second copy, so a tag is never loaded twice and a visit is never counted twice.

A Custom Code action has no head. To send an event from an action, use the Google Analytics Measurement Protocol snippet, which is only a fetch and works in any block.

The cyango object

There are five namespaces:

NamespaceWhat it holds
cyango.utilsHelpers: find entities, store variables, read GUI inputs, detect device
cyango.storyStateRead and drive the story: scenes, language, assets, XR
cyango.uiStateApp and interface state: loading, camera controls, modals
cyango.timelineStateScene timeline and media playback
cyango.typesEnums used as arguments and for comparisons

Values you read are live, such as cyango.storyState.activeSceneId. Functions you call perform an action, such as cyango.storyState.setActiveScene(id).

Variables

Global variables last for the whole session and survive scene changes. Use them to pass data from one action to another.

MethodDescription
cyango.utils.setGlobalVar(key, value)Store any value under a key
cyango.utils.getGlobalVar(key)Read a stored value
cyango.utils.deleteGlobalVar(key)Remove one key (returns boolean)
cyango.utils.clearGlobalVars()Remove all variables
cyango.utils.globalVarsThe whole record, as a snapshot
// On a button click: count presses, then act on the third
const clicks = (cyango.utils.getGlobalVar('clicks') ?? 0) + 1;
cyango.utils.setGlobalVar('clicks', clicks);

if (clicks < 3) return;

const scene = cyango.storyState.activeStoryJson.scenes.find((s) => s.name === 'Reward');
if (scene) cyango.storyState.setActiveScene(scene.id);

Finding entities and scenes

Method or valueReturns
cyango.utils.getActiveScene()The current scene (IScene), or null
cyango.utils.getEntityByName(entities, name)First entity with that name, or null
cyango.utils.getEntityById(entities, id)Entity by its id, or null
cyango.utils.thisEntityThe entity this action is attached to
cyango.utils.getAssetUrl(assetId?)Story asset URL by id; no argument uses this entity's assigned asset
cyango.storyState.activeSceneIdId of the current scene
cyango.storyState.activeStoryJsonThe whole story document, including .scenes
const scene = cyango.utils.getActiveScene();
const logo = cyango.utils.getEntityByName(scene.entities, 'Logo');

Reading GUI input fields

Input field entities capture whatever the visitor types. Read or set that value by the input's entity id. The value is kept even if the field is hidden and shown again.

MethodDescription
cyango.utils.getGuiInputValue(entityId)Current text (string), or null if never set
cyango.utils.setGuiInputValue(entityId, value)Set the field's text
cyango.utils.clearGuiInputValue(entityId)Empty the field
// On a Submit button: read an email field and store it
const scene = cyango.utils.getActiveScene();
const emailField = cyango.utils.getEntityByName(scene.entities, 'EmailInput');

const email = cyango.utils.getGuiInputValue(emailField.id);
cyango.utils.setGlobalVar('email', email);

cyango.utils.setGuiInputValue(emailField.id, ''); // clear after submit

Scrolling GUI containers

For GUI containers whose content scrolls, such as lists and panels:

MethodDescription
cyango.utils.getGuiScrollPosition(entityId)Current { x, y } offset (call trackGuiScroll first)
cyango.utils.getGuiMaxScrollPosition(entityId)Maximum scroll offset
cyango.utils.setGuiScrollPosition(entityId, x, y)Jump to an offset
cyango.utils.scrollGuiBy(entityId, dx, dy)Offset from the current position
cyango.utils.trackGuiScroll(entityId) and untrackGuiScroll(entityId)Start and stop recording the user's scroll
cyango.utils.lockGuiScroll(entityId) and unlockGuiScroll(entityId)Block and allow user scrolling
Method or valueDescription
cyango.storyState.setActiveScene(sceneId)Go to a scene, optionally with a transition
cyango.storyState.setActiveLanguage(lang)Switch language (fires On Language Changed)
cyango.storyState.triggerAction(action, entityId?)Run an action object directly
cyango.uiState.openUrl(url)Open an external URL
cyango.storyState.activeLanguage and availableLanguagesCurrent and available languages
// Send Portuguese visitors to a localized scene
if (cyango.storyState.activeLanguage === cyango.types.LanguageTypes.pt_PT) {
  const scene = cyango.storyState.activeStoryJson.scenes.find((s) => s.name === 'Intro PT');
  if (scene) cyango.storyState.setActiveScene(scene.id);
}

AR and VR

Method or valueDescription
cyango.storyState.enterAR(), enterVR(), exitXR()Enter or exit immersive modes
cyango.storyState.isAR, isVR, isMobileARWhether a mode is currently active

Timeline and media

Method or valueDescription
cyango.timelineState.setPlayingMode(mode)Play or pause the scene timeline. See cyango.types.PlayingModes
cyango.timelineState.controlMedia(mode)Control scene media: 'play', 'pause', 'stop', 'mute', 'unmute', 'seek'
cyango.timelineState.setElapsedTime(ms)Seek the timeline
cyango.timelineState.elapsedTimeCurrent time, in milliseconds
cyango.timelineState.setPlayingMode(cyango.types.PlayingModes.PLAY);

Device and environment

Method or valueReturns
cyango.utils.detectDeviceType()"iPad", "iPhone", "Android", "Windows", "Mac", or "Unknown"
cyango.utils.detectBrowser()Browser name, such as "Apple Safari" or "Google Chrome"
cyango.uiState.selectedBreakpointCurrent breakpoint. See cyango.types.BreakpointStateTypes
cyango.utils.getWindowLocation()The Location object, including URL and query string
cyango.utils.getNavigator()The Navigator object
if (cyango.utils.detectDeviceType() === 'iPhone') {
  // show a mobile-specific hint
}

Types

cyango.types provides the enums used across the API. Use them for arguments and comparisons:

ActionType, EventType, PlayingModes, TransitionType, EntityTypes, MaterialTypes, BreakpointStateTypes, SceneTypes, AssetCategories, AssetFileTypes, AssetMimeTypes, LanguageTypes.

Example: a sign-up gate

// On Click of a Continue button
const scene = cyango.utils.getActiveScene();
const nameField = cyango.utils.getEntityByName(scene.entities, 'NameInput');
const name = (cyango.utils.getGuiInputValue(nameField.id) ?? '').trim();

// Nothing typed, so clear the field and stay on this scene
if (!name) {
  cyango.utils.clearGuiInputValue(nameField.id);
  return;
}

// Remember the name, send it to your backend, then continue
cyango.utils.setGlobalVar('visitorName', name);

await fetch('https://example.com/signups', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ name }),
});

const next = cyango.storyState.activeStoryJson.scenes.find((s) => s.name === 'Welcome');
if (next) cyango.storyState.setActiveScene(next.id);