Script API
Every method, event and object available to a Voxta script.
The reference half of Scripting, which covers how scripts are written and when they run.
Everything below hangs off chat, imported once at the top of a script:
import { chat } from "@voxta";Messages
Every type is a method on chat. See Messages.
chat.instructions('Only you see this');
chat.note('Both sides see this, no reply');
chat.secret('Only the character sees this');
chat.event('A door slams'); // narrated, triggers a reply
chat.story('Describe the storm'); // story writer expands it
chat.userMessage('Where are we going?');
chat.characterMessage('To the garden');
chat.roleMessage('detective', 'Right behind you');note, secret and instructions take { expiresAfterTurns } to drop out of the prompt after N turns:
chat.secret('Standing by the window.', { expiresAfterTurns: 2 });event, story and customMessage return a promise that settles when the generated message finishes, so you can sequence around it:
export async function trigger(e) {
await chat.story('The lights go out.');
chat.appTrigger('PlaySound', chat.scenario.assets.get('breaker.wav'));
}
// Or pass a callback instead
chat.story('The lights go out.', () => chat.setFlag('blackout'));customMessage
chat.customMessage({
role: 'Secret', // Assistant, User, Event, Note, Secret, Instructions
text: 'Then something incredible happens',
useStoryWriter: true,
maxNewTokens: 200,
maxSentences: 2,
allowMultipleLines: false,
includeChatHistory: false,
triggerReply: false,
narrate: false,
expiresAfterTurns: 5,
});State
Variables
Persist for the life of the chat, including across resume. Any JSON value.
chat.variables.score = 0;
chat.set('score', 0); // same thing
const s = chat.get('score', 0); // with a default
chat.variables.score += 5;Flags
chat.setFlag('wears_hat');
chat.setFlag('!wears_hat'); // unset
chat.unsetFlag('wears_hat');
chat.setFlag('pose.sitting'); // enum — clears other pose.*
chat.setFlags('a', 'b.enum', '!c');
chat.hasFlag('wears_hat');
chat.setFlag('cooldown', { messages: 10 }); // expires after 10 messages
chat.setFlag('rush_hour', { seconds: 120 }); // expires after 2 min chat timeSee Flags.
Contexts
chat.setContext('crown', '{{ char }} is wearing a crown.', 'my_flag'); // third arg is an optional flag filter
chat.setContext('crown'); // clearCharacters
chat.character // main character
chat.roles.guard // by scenario role
chat.characters // every character in the scene
chat.user
chat.narratorEach has id, name, scenarioRole, assets, appConfiguration.
chat.setRoleEnabled('guard', false); // remove from the chat
chat.setCharacterCanSpeak(false); // present, but silent
chat.setCharacterCanSpeak(false, 'guard');Assets
chat.scenario.assets.get('sounds/door.wav'); // exact path, throws if missing
e.character.assets.oneOf('emote_'); // random match (regex), throws if none
e.character.assets.oneOrNoneOf('rare_'); // random match, undefined if none
chat.scenario.assets.matchFiles('^bg/.*png$'); // all matching pathsAlso import { oneOf } from "@voxta/utils" for picking from a plain array.
App triggers
chat.appTrigger('Emote', '🌳', '#00ff00');
chat.setBackground(chat.scenario.assets.get('room.jpg'), 1);
// Queued: runs after the current speech finishes
chat.queue.appTrigger('PlaySound', chat.scenario.assets.get('ping.mp3'));
chat.queue.roleMessage('guard', 'Halt.');
chat.queue.roleEnabled('guard', true);
chat.queue.setFlags('a', '!b');See App triggers and HUD & stage effects.
Timers
Seconds, on chat time — a timer pauses when the chat does. Max 32 active.
chat.setTimeout(() => chat.setFlag('late'), 4);
const h = chat.setInterval(() => chat.setFlag('tick'), 30);
chat.clearInterval(h);
chat.time; // seconds elapsed since the chat startedA timer is queued, not sample-accurate. To land a sound inside a spoken line, use a tool with the "insert" audio method.
Generation
chat.generateImage('A candlelit tavern at night');
chat.generateImage('A neon street', { analyze: true }); // character sees the result
chat.generateImage('A portrait', e.character); // target a character
chat.imagine(); // LLM writes the prompt from context
chat.imagine('the room she just walked into');
chat.imagine({ prompt: 'her childhood home', analyze: true });
chat.imagine({ params: { avatar: true } }); // use as the avatar
chat.imagine({ params: { background: true, layer: 1 } }); // use as a background
chat.generateAnimation({ prompt: 'A slow curtsy', duration: 4, seed: 12345 });
chat.perform('Throw both arms up and jump once'); // the character acts it out
chat.interrupt(); // stop speech / cancel the replyparams.layer follows the background layer zones.
infer
Ask the model for text. Async.
export async function trigger(e) {
const line = await chat.infer('Write a short, cryptic fortune.', { maxSentences: 1 });
chat.event(line);
}Pass a schema and you get a parsed object instead of a string. Fields are "string", "number", "boolean", or an array of allowed values.
const verdict = await chat.infer('Did the user agree to the deal?', {
schema: { agreed: 'boolean', tone: ['friendly', 'hostile', 'neutral'] },
});
if (verdict.agreed) chat.setFlag('deal_struck');| Option | Notes |
|---|---|
maxNewTokens | Token cap. |
maxSentences | Sentence cap (text mode only). |
allowMultipleLines | Allow line breaks (text mode only). |
includeChatHistory | Include the transcript in the prompt. |
schema | Switches to structured mode. |
The e object
Passed to trigger and to listeners. Which fields exist depends on what fired it.
| Field | Notes |
|---|---|
e.message | id, senderId, index, conversationIndex, chatTime, role, text |
e.character | The character behind the event |
e.user | Always available |
e.arguments | Actions, tools, and app:* events |
e.afterSpeech(fn) | Run fn once the resulting speech finishes |
e.chatFlow(who) | Force who replies next: chat.roles.main, chat.user |
e.evaluateNextEvent() | Let the next event also fire this pass |
Event listeners
Register on chat, usually from the scenario init script. chat.on is an alias for addEventListener.
| Event | Fires when | Extra fields |
|---|---|---|
init | The script loads | — |
start | A new chat begins | hasBootstrapMessages |
resume | An existing chat is reopened | message |
userMessageReceived | The user sent a message | rewriteUserMessage(text) |
generating | A reply starts generating | character |
generatingComplete | A reply finished | message |
speechStart | TTS playback starts | startIndex, isNarrator |
speechComplete | TTS playback ends | — |
transcriptionStarted | The user starts talking | — |
transcriptionFinished | The user stops talking | text (empty if nothing was said) |
imageGenerated | An image finished generating | message |
buttonPressed | The user pressed a button | button |
controlChanged | The user moved a control | contextKey, variable, value |
beforeSelectActionInference | Just before an action layer is chosen | layer, timing, actions, setActions(names) |
action:<name> | Action inference or a tool picked it | arguments, action, layer, contextKey |
app:<name> | The host app raised a custom event | arguments |
chat.addEventListener('userMessageReceived', (e) => {
e.rewriteUserMessage(e.message.text.replace(/John/g, 'Jane'));
});
chat.addEventListener('start', () => {
chat.variables.score = 0;
});
// Narrow the candidate actions before the inference pass runs
chat.addEventListener('beforeSelectActionInference', (e) => {
if (e.layer === 'movement' && chat.hasFlag('tied_up')) e.setActions([]);
});Answering a tool
An action:* listener for a tool can return a string, which is handed back to the character as the tool's result.
chat.addEventListener('action:roll_dice', (e) => {
return `You rolled a ${1 + Math.floor(Math.random() * 20)}.`;
});If several listeners answer, the first one wins.
Defining things at runtime
Each of these is keyed by a context key: calling it again with the same key replaces the set, and an empty array clears it.
Actions
chat.setActions('desk', [
{
name: 'adjust_desk_height',
description: 'Adjust the desk between sitting (0) and standing (5)',
layer: 'desk_control',
timing: 'AfterAssistantMessage',
invocation: 'ActionInference', // or 'ToolCalling'
arguments: [
{ name: 'height', type: 'integer', description: 'Desk height 0-5', required: true },
],
},
]);
chat.addEventListener('action:adjust_desk_height', (e) => {
chat.set('desk_height', e.arguments.height);
});| Field | Notes |
|---|---|
name, description | Required. |
shortDescription | Shown to the character itself. |
layer | Groups mutually exclusive actions. |
timing | AfterUserMessage, BeforeAssistantMessage, AfterAssistantMessage, AfterAnyMessage, Manual. |
invocation | ActionInference (default) or ToolCalling. |
arguments | { name, type, description?, required?, choices? }. Types: string, integer, double, boolean, array, void. |
flagsFilter, matchFilter, roleFilter | Conditions. See Actions. |
once, disabled, cancelReply, finalLayer | Behaviour switches. |
activates | Actions this one unlocks. |
effect | setFlags, note, secret, instructions, event, story, trigger, maxTokens, maxSentences. Runtime actions cannot define a script — use an action:* listener. |
Buttons and controls
chat.setButtons('doors', [
{ name: 'Left door', description: 'Open the left door', effect: { setFlags: ['chose_left'] } },
{ name: 'Right door', description: 'Open the right door', effect: { setFlags: ['chose_right'] } },
]);
chat.setControls('settings', {
title: 'Settings',
controls: [
{ type: 'toggle', variable: 'lights_on', label: 'Lights' },
{ type: 'slider', variable: 'volume', label: 'Volume', min: 0, max: 100, step: 5, live: true },
],
});See Buttons & controls.