Voxta docs

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 time

See Flags.

Contexts

chat.setContext('crown', '{{ char }} is wearing a crown.', 'my_flag');  // third arg is an optional flag filter
chat.setContext('crown');                                              // clear

Characters

chat.character          // main character
chat.roles.guard        // by scenario role
chat.characters         // every character in the scene
chat.user
chat.narrator

Each 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 paths

Also 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 started

A 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 reply

params.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');
OptionNotes
maxNewTokensToken cap.
maxSentencesSentence cap (text mode only).
allowMultipleLinesAllow line breaks (text mode only).
includeChatHistoryInclude the transcript in the prompt.
schemaSwitches to structured mode.

The e object

Passed to trigger and to listeners. Which fields exist depends on what fired it.

FieldNotes
e.messageid, senderId, index, conversationIndex, chatTime, role, text
e.characterThe character behind the event
e.userAlways available
e.argumentsActions, 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.

EventFires whenExtra fields
initThe script loads
startA new chat beginshasBootstrapMessages
resumeAn existing chat is reopenedmessage
userMessageReceivedThe user sent a messagerewriteUserMessage(text)
generatingA reply starts generatingcharacter
generatingCompleteA reply finishedmessage
speechStartTTS playback startsstartIndex, isNarrator
speechCompleteTTS playback ends
transcriptionStartedThe user starts talking
transcriptionFinishedThe user stops talkingtext (empty if nothing was said)
imageGeneratedAn image finished generatingmessage
buttonPressedThe user pressed a buttonbutton
controlChangedThe user moved a controlcontextKey, variable, value
beforeSelectActionInferenceJust before an action layer is chosenlayer, timing, actions, setActions(names)
action:<name>Action inference or a tool picked itarguments, action, layer, contextKey
app:<name>The host app raised a custom eventarguments
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);
});
FieldNotes
name, descriptionRequired.
shortDescriptionShown to the character itself.
layerGroups mutually exclusive actions.
timingAfterUserMessage, BeforeAssistantMessage, AfterAssistantMessage, AfterAnyMessage, Manual.
invocationActionInference (default) or ToolCalling.
arguments{ name, type, description?, required?, choices? }. Types: string, integer, double, boolean, array, void.
flagsFilter, matchFilter, roleFilterConditions. See Actions.
once, disabled, cancelReply, finalLayerBehaviour switches.
activatesActions this one unlocks.
effectsetFlags, 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.

What's next

On this page