Skip to main content

Code Nodes

Code Nodes

Code Nodes in BotDojo provide the flexibility and power of running JavaScript (with Python support coming soon) within your AI applications. While visual designers excel at handling functions that follow similar patterns, such as building prompts and making calls to Language Models (LLMs), there are cases where writing code is more efficient and effective.

Code Nodes come in handy when you need to perform specific actions like making calculations or calling out to an API. They also simplify the process of handling complex logic involving loops, branching (if/then/else), and other programming constructs.

Input and Execution

Each Code Node accepts an input defined by a JSON Schema, similar to LLM Nodes and AI Agent Nodes. The entry function for a Code Node is called execute, and it takes three parameters:

  1. input: An object that represents the input data, structured according to the defined input JSON Schema.
  2. node: The node object itself, which allows you to inspect properties of the Code Node to alter its behavior.
  3. context: An object that provides access to the session state and allows you to log events that will be available in the execution trace.

Here's an example of the execute function signature:

async function execute(
input: InputSchema, // Input data structured according to the defined JSON Schema
node: INode, // Node object containing properties that can alter behavior
context: IFlowContext // Provides access to session state and execution environment
): Promise<OutputSchema> // Returns output matching the defined output JSON Schema
{
// Code logic goes here
}

Interacting with Other Nodes

Code Nodes can take special nodes as inputs, such as LLMs, Prompts, and Tools. This enables the Code Node to call these interfaces and access other nodes within the flow. By leveraging this capability, you can seamlessly integrate third-party libraries with BotDojo, getting the best of both worlds—the ability to mix and match functionalities that best suits your senario.

async function execute(input,node, context)
{
let spanishResults = []
context.log('starting function')
for(let sentence of input.sentences)
{
let spanish = await input.llm.completion("Please translate "+sentence+" to spanish. Only respond with the translation.\n")
spanishResults.push(spanish.outputText)
}
context.log('return result')
return {
spanish_sentences:spanishResults
}
}

Below is an example of using Code Nodes. Click the image below to Clone it to your project.

Returning Output

Code Nodes return a JavaScript object that must satisfy the output JSON Schema you defined. In addition to the typed properties you expose to the rest of the flow, you can attach a few helper fields (all prefixed with an underscore) that tell BotDojo how to present the response inside chat. These helper keys are stripped before the schema is validated, so you can mix them with strongly typed data safely.

Special response fields

  • _markdown — render rich text in the main chat transcript. Provide a Markdown string; it can include tables, call-outs, and buttons that open side panels.
  • _canvas — show an interactive canvas attachment. Supply an object with canvasId, canvasType, and canvasData. You can create or update canvases with context.canvas.createCanvas / updateCanvas and then return the reference here so the chat UI loads it.
  • _panels — open or update side panels next to the chat. Each entry follows the IChatSidePanel shape (id, title, panelType, and the data required for that type such as markdown, url, or canvasId).
  • _citations — surface supporting evidence beneath the assistant reply. Provide an array of citation objects (for example { title, url, snippet }).

The canvas helper supports several built-in types:

  • 'iframe' embeds an external URL and can optionally set agent_enabled to wire up bi-directional events with the chat agent.
  • 'code-interpreter' connects the message to a persistent sandbox session (used by the Code Interpreter Canvas node).
  • 'markdown' stores rich text separately from the chat stream so it can be re-opened later from the Canvas tray.
  • 'file' renders binary content (PDF, images, etc.) by referencing an uploaded document.
  • 'dojo-canvas' hosts an app built with the botdojo-canvas-client, enabling fully custom interfaces that talk to your flow via WebSockets.

Example: combine schema output with UI helpers

export async function execute(input, node, context) {
const state = await context.sessionState.get('dashboard');
let canvasId = await state.getVariable('canvasId');

if (!canvasId) {
const newCanvasId = `dashboard-${context.sessionId}`;
const created = await context.canvas.createCanvas({
canvasId: newCanvasId,
canvasType: 'iframe',
canvasData: {
url: `https://dash.example.com/embed/${context.sessionId}`,
agent_enabled: false,
},
});
canvasId = created.canvasId;
await state.setVariable('canvasId', canvasId);
}

const summary = `Processed ${input.records.length} customer records.`;

return {
totalRecords: input.records.length,
_markdown: `### Data loaded\n${summary}`,
_canvas: {
canvasId,
canvasType: 'iframe',
canvasData: {
url: `https://dash.example.com/embed/${context.sessionId}`,
},
},
};
}

The example stores the canvas identifier in session state so future runs can update it with context.canvas.updateCanvas. Only the declared schema field (totalRecords) is exposed to downstream nodes; the _markdown and _canvas helpers control the chat experience.

Writing Code

The context parameter provides access to several important features and capabilities:

Core Context Object (IFlowContext)

The IFlowContext interface represents the execution environment for a flow. Think of it as the central hub that provides access to various services and state management. Here are its key components:

interface IFlowContext {
sessionState: ISessionState;
onIntermediateStepUpdate: (step: FlowRequestIntermediateStep) => Promise<void>;
flowId: string;
flowRequestId: string;
sessionId: string;
callSubFlow: (subFlowId: string, input: any) => Promise<any>;
env: (name: string) => Promise<any>;
}

Session State Management

The session state system provides a way to store and retrieve data during flow execution. It's organized in a hierarchical structure:

  1. ISessionState - The top-level session state manager
  2. ISessionStateStore - Individual state stores that contain messages and variables

Here's how you would work with session state:

// Getting a state store
const stateStore = await context.sessionState.get("myStateKey");

// Adding a message
await stateStore.addMessage({
role: "user",
content: "Hello, how can I help you today?"
});

// Getting all messages
const messages = await stateStore.getMessages();

// Setting a variable
await stateStore.setVariable("userName", "John");

// Getting a variable
const userName = await stateStore.getVariable("userName");

Message Management

The system uses a structured approach to handling messages through the ChatMessage class:

// Example of creating and storing a message
const message = new ChatMessage();
message.role = "assistant";
message.content = "I can help you with that!";
message.date = new Date();

await stateStore.addMessage(message);

Progress Tracking

The context provides a way to report progress through intermediate steps:

// Reporting progress
await context.onIntermediateStepUpdate({
stepId: "step1",
stepLabel: "Processing User Input",
stepStatus: "processing",
stepPercentComplete: 50,
content: "Analyzing user request...",
startTime: new Date()
});

Sub-Flow Execution

The context allows for calling sub-flows, which is useful for modular flow design:

// Calling a sub-flow
const result = await context.callSub_MySubFlow(
{ userId: "123" }
);

Environment Variables

You can access environment variables through the context:

// Getting an environment variable
const apiKey = await context.env("API_KEY");

For more information on Environment Variables see Environment Variables.

Connecting interface Nodes

You can connect and interact with other primative interfaces in a Code Node. To do so add an Interface Node to your flow and connect it to the Code Node. You can access and call methods on these interfaces by calling await input.inputName.method().

alt text

The system includes several other important interfaces that work with the context:

  1. ILanguageModel - For interacting with language models
  2. IRetriever - For searching and retrieving information
  3. IFlowStorage - For managing file storage
  4. IPrompt - For handling prompt generation
  5. IMemory - For managing conversational memory

These interfaces work together to provide a complete environment for flow execution. For example, you might use them together like this:

// Example of a complex interaction
async function handleUserQuery(context: IFlowContext) {
// Get the state store
const store = await context.sessionState.get("mainConversation");

// Get previous messages
const messages = await store.getMessages();

// Add new user message
await store.addMessage({
role: "user",
content: "What's the weather like?"
});

// Report progress
await context.onIntermediateStepUpdate({
stepId: "weatherQuery",
stepLabel: "Checking Weather",
stepStatus: "processing",
content: "Retrieving weather information...",
startTime: new Date()
});

// Use environment variables
const weatherApiKey = await context.env("WEATHER_API_KEY");

// Store the result
await store.setVariable("lastWeatherCheck", new Date());
}

This system is designed to provide a robust environment for building complex flows while maintaining state, handling messages, and managing resources effectively. The context object serves as the central point of access to all these capabilities.