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
| Moment | What runs |
|---|---|
| Chat opens | Init scripts, then the init and start listeners |
| Existing chat reopened | Init scripts, then resume |
| An interaction fires | That interaction's handler |
| Something happens in the chat | Any listener registered for it |
| Later, on purpose | Timer 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/assertwrite 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.