Skip to content

Scripting

Bonsai Backend supports executing custom JavaScript within conversation actions via script tools. Scripts run in a secure, isolated sandbox with strict resource limits.

Sandbox Environment

Scripts execute in isolated-vm, a V8 isolate-based sandbox that provides:

  • Memory limit — 16 MB per execution
  • Time limit — 5 seconds per execution
  • No Node.js APIs — No require, fs, http, process, etc.
  • No network access — Scripts cannot make HTTP calls
  • No filesystem access — Scripts cannot read or write files

Available Globals

Scripts have access to these global variables:

VariableAccessDescription
varsRead/WriteCurrent stage variables
userProfileRead/WriteEnd user's profile data
userInputRead/WriteCurrent user input text (string)
conversationIdRead-onlyCurrent conversation ID
projectIdRead-onlyCurrent project ID
stageIdRead-onlyCurrent stage ID
stageRead-onlyFull stage object: id, name, availableActions, metadata, enterBehavior, useKnowledge
historyRead-onlyConversation message history
actionsRead-onlyMatched action results and their parameters
originalUserInputRead-onlyOriginal unmodified user input
resultsRead-onlyResults from tools and webhooks
timeRead-onlyRich time context: iso, date, time, dayOfWeek, timezone, calendar, anchor, etc.
projectRead-onlyProject-level settings: timezone (IANA identifier or null), languageCode (ISO code or null), language (human-readable name or null)
userInputSourceRead-onlyInput channel: 'text' | 'voice' | null
constsRead-onlyProject-level constants (from project settings)
stageVarsRead-onlyVariables for all stages, keyed by stage ID
eventsRead-onlyAll conversation events in chronological order (messages, actions, stage transitions, etc.)
copyRead-onlySelected sample copy content joined by newlines and rendered using selected decorator; empty string if no copy matched this turn
copyContentRead-onlyRaw selected content before copy decorator is applied
sampleCopyRead-onlyAll sample copies active for the current stage: Array<{ name, trigger, content: string[] }>
consoleconsole.log(), console.error(), console.warn()

Modifying State

Scripts can modify three mutable globals. Changes persist after script execution.

Key deletion — Assigning new properties and deleting existing ones (delete vars.foo) both work correctly. The entire object is replaced after each script run.

Stage Variables

javascript
// Set a variable
vars.retryCount = (vars.retryCount || 0) + 1;

// Set nested objects
vars.order = {
  id: "ORD-123",
  status: "pending",
  items: ["Widget A", "Widget B"]
};

User Profile

javascript
userProfile.preferredLanguage = "es";
userProfile.lastInteraction = new Date().toISOString();

User Input

javascript
// Modify what the LLM sees as user input
userInput = userInput.toLowerCase().trim();

// Add context
userInput = "Context: order " + vars.orderId + ". User says: " + userInput;

Reading Context

javascript
// Access conversation history
const lastMessage = history[history.length - 1];

// Check classification results
const matchedActions = actions;

// Access smart_function / script tool results
const analysisResult = results.tools?.sentimentAnalyzer?.result;

// Access webhook tool results (also mirrored at results.webhooks for backward compat)
const orderData = results.webhooks?.orderLookup;
// equivalently:
const orderData2 = results.tools?.orderLookup?.result;

// Current stage metadata
const isBookingStage = stage.name === 'Booking';
const availableActionNames = stage.availableActions.map(a => a.name);

// Distinguish voice vs text input
if (userInputSource === 'voice') {
  userInput = userInput.toLowerCase();
}

// Time-based logic
const hour = parseInt(time.hour, 10);
const isBusinessHours = time.dayOfWeek !== 'Saturday' && time.dayOfWeek !== 'Sunday' && hour >= 9 && hour < 17;
vars.isBusinessHours = isBusinessHours;

// Cross-stage variable access
const prevStepData = stageVars?.['stage-id-here']?.someField;

// Project-level constants
const companyName = consts.companyName;

Utility Functions

The sandbox provides a set of pure utility functions:

uuid()

Generate a random UUID v4.

javascript
vars.correlationId = uuid(); // e.g. '3b1f8c2d-4e5a-6b7c-8d9e-0f1a2b3c4d5e'

formatDate(iso, locale?, options?)

Format an ISO date string using Intl.DateTimeFormat. locale defaults to the runtime locale; options accepts any Intl.DateTimeFormat options object.

javascript
// Short date in Polish
const label = formatDate(time.iso, 'pl-PL', { dateStyle: 'long' });
// e.g. '27 lutego 2026'

// Day and month only
vars.appointmentLabel = formatDate(vars.appointmentDate, 'en-GB', { day: 'numeric', month: 'long' });
// e.g. '14 March'

History Utilities

Five helper functions are available for working with conversation history. They operate purely on the in-memory history and events arrays — no host calls, no overhead.

lastMessage(role?)

Returns the content of the last message, optionally filtered to 'user' or 'assistant'.

javascript
const last = lastMessage();           // last message regardless of role
const lastUser = lastMessage('user'); // last thing the user said

messageCount(role?)

Returns the total number of messages, optionally filtered by role.

javascript
if (messageCount('user') >= 5) vars.needsEscalation = true;

historyText(opts?)

Formats messages as "User: ...\nAssistant: ...". All options are optional.

OptionTypeDescription
nnumberLimit to last N messages
role'user'|'assistant'Only include one role
labels{ user?, assistant? }Override the User: / Assistant: prefix strings
javascript
vars.recentContext = historyText({ n: 6 });         // last 3 turns
vars.summary = historyText();                       // full conversation

// Custom prefixes
vars.transcript = historyText({ n: 10, labels: { user: 'Customer', assistant: 'Agent' } });
// e.g. "Customer: I need help\nAgent: Sure, let me check..."

historyContains(substr, role?)

Case-insensitive substring search across message content.

javascript
if (historyContains('cancel', 'user')) vars.showCancellationFlow = true;
if (historyContains('error')) vars.errorMentioned = true;

stageMessages(role?)

Returns only the messages exchanged since the most recent stage transition (i.e. in the current stage), optionally filtered by role. Returns all history if no stage transition has occurred.

javascript
const stageUserMsgs = stageMessages('user');
vars.stageRetries = stageUserMsgs.length;

Flow Control

Flow control functions are available in script tools and script tool calls only. They are silently ignored in action conditions and inline = expressions.

All signals are queued and applied after the script finishes — the script always runs to completion first.

goToStage(stageId)

Transition to a different stage after the script.

Note: goToStage() is silently ignored when called inside a script tool that belongs to a lifecycle action (__on_enter or __on_leave). Use it only in regular user-triggered or command-triggered actions.

javascript
if (vars.retryCount >= 3) {
  goToStage('escalation-stage-id');
}

endConversation(reason?)

End the conversation gracefully. Triggers the on_leave lifecycle on the current stage.

javascript
if (vars.taskComplete) {
  endConversation('Task completed successfully');
}

abortConversation(reason?)

Abort the conversation immediately.

javascript
if (vars.fraudDetected) {
  abortConversation('Fraud detection triggered');
}

prescriptResponse(text)

Deliver a fixed response to the user, bypassing LLM generation entirely.

javascript
if (vars.language === 'pl') {
  prescriptResponse('Dziękujemy za kontakt. Do widzenia!');
} else {
  prescriptResponse('Thank you for contacting us. Goodbye!');
}

suppressResponse()

Suppress any response generation for this turn. Useful when the script handles the outcome fully through goToStage or when a silent state update is needed.

javascript
// Silently process and transition without generating a response
vars.stepCompleted = true;
goToStage('next-step-id');
suppressResponse();

Events

The events array contains all conversation events in chronological order, including messages, actions, stage transitions, tool calls, and more. It is the complete audit trail of the conversation turn.

ScriptEvent shape

FieldTypeDescription
idstringUnique event ID
eventTypestringEvent type (see below)
timestampstringISO 8601 timestamp
eventDataobjectEvent-specific payload
metadataobject?Optional metadata

Event types

eventTypeKey eventData fields
messagerole, text, originalText
actionactionName, stageId, effects
tool_calltoolId, toolName, parameters, success, result?, error?
classificationclassifierId, input, actions
transformationtransformerId, input, appliedFields
commandcommand, parameters?
jump_to_stagefromStageId, toStageId
conversation_startstageId, initialVariables?
conversation_resumepreviousStatus, stageId
conversation_endreason?, stageId
conversation_abortedreason, stageId
conversation_failedreason, stageId?

Examples

javascript
// How many times has the current stage been entered?
const jumpsHere = events.filter(
  e => e.eventType === 'jump_to_stage' && e.eventData.toStageId === stageId
);
vars.stageEntryCount = jumpsHere.length;

// Did a specific tool succeed?
const lookup = events.find(
  e => e.eventType === 'tool_call' && e.eventData.toolName === 'customerLookup'
);
vars.lookupSucceeded = lookup?.eventData.success ?? false;

// What stage did we come from?
const lastJump = events.filter(e => e.eventType === 'jump_to_stage').at(-1);
vars.previousStageId = lastJump?.eventData.fromStageId ?? null;

Console Output

Scripts can log messages for debugging. Console output is captured in the conversation events:

javascript
console.log("Processing order:", vars.orderId);
console.warn("Retry count high:", vars.retryCount);
console.error("Missing required field");

Use Cases

Conditional Logic

javascript
if (vars.retryCount >= 3) {
  vars.needsEscalation = true;
  vars.escalationReason = "Max retries exceeded";
}

Data Transformation

javascript
// Parse and restructure webhook tool response
// Webhook results land in both results.webhooks and results.tools
const response = results.webhooks?.customerLookup;
if (response) {
  vars.customerName = response.data.firstName + " " + response.data.lastName;
  vars.accountTier = response.data.subscription?.tier || "free";
  vars.isActive = response.data.status === "active";
}

// Parse a smart_function or script tool result
const analysis = results.tools?.sentimentAnalyzer?.result;
if (analysis) {
  vars.sentiment = analysis.label;
  vars.sentimentScore = analysis.score;
}

Input Processing

javascript
// Extract and normalize data from user input
const numbers = userInput.match(/\d+/g);
if (numbers && numbers.length > 0) {
  vars.extractedNumber = parseInt(numbers[0]);
}

// Clean up user input
userInput = userInput.replace(/[^\w\s]/g, "").trim();

Flow Control Flags

javascript
// Set flags that conditions in other actions can check
vars.hasCompletedVerification = true;
vars.currentStep = "payment";
vars.allowNavigation = vars.allFieldsFilled && vars.termsAccepted;

Limitations

  • No async/await — All code must be synchronous
  • No external modules — Cannot import or require packages
  • No network calls — Use a webhook tool instead
  • No timerssetTimeout, setInterval are not available
  • 16 MB memory — Complex data structures or large strings may hit the limit
  • 5 second timeout — Long-running computations will be terminated

Best Practices

  • Keep scripts short — Use scripts for data manipulation, not complex logic
  • Guard against undefined — Always check if variables exist before accessing them
  • Use tools for external calls — Scripts handle local state; webhook and smart_function tools handle external interactions
  • Log for debugging — Use console.log() to track script execution in conversation events
  • Avoid side effects — Only modify vars, userProfile, and userInput

Released under the Apache-2.0 License.