Voxta docs

Scripting

Where scripts live, when they run, and what they can reach.

Scripts are how a scenario does something no field expresses — a delay, a computation, a decision that depends on state.

They are standard ES modules: import/export, async/await, promises. Every method they can call is in the Script API; this page is about how they fit together.

Two shapes

A handler exports trigger, and runs when the thing it is attached to fires — an event, a button, an action, a tool.

import { chat } from "@voxta";

export function trigger(e) {
  chat.note(`${e.character.name} did something.`);
}

An init script runs once when the chat starts and registers listeners instead. The scenario's index script is the usual home for this.

import { chat } from "@voxta";

chat.addEventListener('start', () => {
  chat.variables.score = 0;
});

chat.addEventListener('userMessageReceived', (e) => {
  if (e.message.text.includes('password')) chat.setFlag('said_password');
});

A scenario usually has both: an init script setting up state and listeners, and a handler on each interaction that needs one.

When they run

MomentWhat runs
Chat opensInit scripts, then the init and start listeners
Existing chat reopenedInit scripts, then resume
An interaction firesThat interaction's handler
Something happens in the chatAny listener registered for it
Later, on purposeTimer callbacks, and chat.queue.* after the current speech

The full listener list is in the Script API.

What survives

Module state resets when you leave the chat. A let at the top of a file is gone on reload.

Use chat.variables for anything that must persist — they are stored with the chat and survive resume. Use flags for on/off state other things are gated on.

The distinction that matters: a variable holds a value, a flag is something conditions can filter on. A number belongs in a variable; whether the door is open belongs in a flag. Conditions can read variables too, with $name.

Sharing code

Scenario scripts import siblings by name, and parent-scenario scripts through ../base/:

// scenario/lib.js
import { chat } from "@voxta";

export function winRound(e, points) {
  const score = chat.set("score", chat.get("score", 0) + points);
  chat.note(`${e.character.name} won! Score: ${score}`);
}

// scenario/handler.js
import { winRound } from './lib';
import { somethingShared } from '../base/shared';

export function trigger(e) {
  winRound(e, 10);
}

Module exports are bindings, not references — you cannot reassign an exported scalar from another module. Wrap mutable state in an object (export let counter = { value: 0 }), or use chat.variables when it should persist.

The sandbox

  • No host interop, no eval, no network, no filesystem. Only @voxta, @voxta/utils, and sibling scenario files.
  • Strict mode, recursion capped at 128, 10M statements per entry, 2s regex timeout, 30s promise timeout.
  • console.log / debug / info / warn / error / assert write to the script log.

A runaway script ends as a script error rather than taking the server down with it.

Debugging

The Script section of the chat inspector carries the log, and a browser for code snippets. Flags and Contexts next to it answer most "why didn't that fire?" questions faster than a console.log does.

What's next

On this page