Skip to main content

Entity Evaluators

Entity Evaluators are specialized handlers that process each workflow entity type. They share a common base class and implement type-specific logic for conditions, scripts, and branching.

Base Class

All evaluators extend BaseEntityEvaluator:

class BaseEntityEvaluator {
static RESULT_VAR = '___result___';

constructor(scriptTimeout = 5000) {
this.scriptTimeout = scriptTimeout;
}

// Abstract method - must be implemented
async evaluate(context, entity, node, message, executionContext) {
throw new Error('Subclass must implement evaluate()');
}
}

Shared Methods

MethodDescription
injectMessage(context, message)Inject message as message variable
injectArguments(context, entity, executionContext)Inject entity arguments
injectPromptAndModel(context, entity, executionContext)Inject prompt/model data
processPromptHandlebars(context, entity, executionContext)Substitute {{...}} placeholders in prompt
runScript(context, entity)Execute entity script
getResultVariable(context)Read ___result___ value
setResultVariable(context, value)Set ___result___ value
evaluateVariable(context, variableName)Read any variable value
captureContextVariables(context, executionContext)Capture all context vars

Argument Injection

Arguments support two modes:

Literal Values:

argumentValue: "Hello World"
// Becomes: var argName = "Hello World";

Handlebar References:

argumentValue: "{{message.user.id}}"
// Becomes: var argName = message.user.id;

EventEvaluator

Handles Event entities — the entry points for workflow processing.

File Location

src/workers/evaluators/EventEvaluator.js

Modes

ModeConfigurationBehavior
Single PathtfCondition: "Single Path"Continue if true, exit if false
True/FalsetfCondition: "True/False"Branch based on boolean result
MultitfCondition: "Multi", logicFieldBranch based on variable value

Execution Flow

async evaluate(context, entity, node, message, executionContext) {
// 1. Inject message
await this.injectMessage(context, message);

// 2. Evaluate condition → set ___result___
const conditionResult = await this.evaluateCondition(context, entity);
await this.setResultVariable(context, conditionResult);

// 3. Run script (if exists)
if (entity.script) {
await this.injectArguments(context, entity, executionContext);
const scriptSuccess = await this.runScript(context, entity);
if (!scriptSuccess) {
return { action: 'exit_workflow', children: [] };
}
}

// 4. Determine branching based on mode
const mode = entity.logicField ? 'Multi' : entity.tfCondition;

if (mode === 'Multi') {
return await this.evaluateLogicBranching(context, entity, node, entity.logicField);
}

if (mode === 'True/False') {
return this.findTrueFalseBranch(node, conditionResult);
}

// Single Path mode
if (conditionResult === false) {
return { action: 'exit_workflow', children: [] };
}

return { action: 'process_children', children: node.children || [] };
}

Condition Evaluation

Uses the evaluateCondition injected script:

async evaluateCondition(context, entity) {
if (!entity.condition) {
return true; // No condition = always pass
}

const conditionJson = JSON.stringify(entity.condition);
const result = await context.eval(
`evaluateCondition(${conditionJson}, message)`,
{ copy: true, timeout: this.scriptTimeout }
);

return result;
}

Branch Matching

True/False Mode:

findTrueFalseBranch(node, conditionResult) {
const branchValue = String(conditionResult); // "true" or "false"
const matched = node.children.find(child =>
String(child.value) === branchValue
);

if (matched) {
return { action: 'process_children', children: matched.children || [] };
}

return { action: 'exit_workflow', children: [] };
}

Multi Mode:

async evaluateLogicBranching(context, entity, node, logicField) {
const branchValue = await this.evaluateVariable(context, logicField);

// First, try exact match
const matched = node.children.find(child =>
String(child.value) === String(branchValue)
);

if (matched) {
return { action: 'process_children', children: matched.children || [] };
}

// Fall back to wildcard branch if no exact match
const wildcardBranch = node.children.find(child => child.value === '*');

if (wildcardBranch) {
return { action: 'process_children', children: wildcardBranch.children || [] };
}

return { action: 'exit_workflow', children: [] };
}

Wildcard Branch Support

In Multi mode, you can define a wildcard branch (*) that catches any value not matched by explicit branches:

Event (Multi Mode)
├── "touchdown" → Touchdown Handler
├── "field_goal" → Field Goal Handler
├── "*" → Default Handler (catches all other values)

The wildcard branch:

  • Only triggers when no explicit branch matches
  • Useful for catch-all/default handling
  • Labeled with * as the branch value

PromptEvaluator

Handles Prompt entities — AI/LLM integration.

File Location

src/workers/evaluators/PromptEvaluator.js

Execution Flow

async evaluate(context, entity, node, message, executionContext) {
// 1. Inject message
await this.injectMessage(context, message);

// 2. Inject prompt, model, and arguments
await this.injectPromptAndModel(context, entity, executionContext);
await this.injectArguments(context, entity, executionContext);

// 3. Process handlebar substitution in prompt
await this.processPromptHandlebars(context, entity, executionContext);

// 4. Run pre-processing script
if (entity.script) {
await this.runScript(context, entity);
}

// 5. Auto-execute prompt with model
if (entity.model) {
await context.eval('(async () => { await executePromptWithModel(); })()', {
promise: true,
timeout: this.scriptTimeout
});
}

// 6. Capture context variables
await this.captureContextVariables(context, executionContext);

// 7. Always continue to children
return { action: 'process_children', children: node.children || [] };
}

Handlebar Substitution

Before script execution, all {{...}} patterns in the prompt are replaced with actual values:

async processPromptHandlebars(context, entity, executionContext) {
if (!entity.prompt) return;

// Check if prompt contains handlebars
const hasHandlebars = /\{\{[^}]+\}\}/.test(entity.prompt);
if (!hasHandlebars) return;

// Process in V8 context where all variables are available
// Evaluates paths like {{message.user.name}}, {{argumentName}}, etc.
// Unresolved handlebars are kept as-is and logged as warnings
}

Example Substitution:

// Original prompt
"Generate content for user {{message.user.name}} about {{topic}}"

// After injection and substitution
"Generate content for user John Smith about NFL touchdowns"

This allows dynamic prompt construction without needing script code.

Model Injection

async injectPromptAndModel(context, entity, executionContext) {
if (entity.modelId) {
await context.eval(`var modelId = ${JSON.stringify(entity.modelId)};`);
}

if (entity.model) {
const modelData = {
id: entity.model.id,
name: entity.model.name,
modelUrl: entity.model.modelUrl,
token: entity.model.token,
clientKey: entity.model.clientKey,
secretKey: entity.model.secretKey,
};
await context.eval(`var entityModel = ${JSON.stringify(modelData)};`);
}

if (entity.prompt) {
await context.eval(`var prompt = ${JSON.stringify(entity.prompt)};`);
}
}

Key Behavior

  • Always continues to children (script failures don't halt)
  • Auto-executes executePromptWithModel() if model is configured
  • Captures all context variables for downstream use

ActionEvaluator

Handles Action entities — custom script execution.

File Location

src/workers/evaluators/ActionEvaluator.js

Execution Flow

async evaluate(context, entity, node, message, executionContext) {
// 1. Inject message
await this.injectMessage(context, message);

// 2. Inject arguments (no prompt/model for Actions)
await this.injectArguments(context, entity, executionContext);

// 3. Run script
if (entity.script) {
const scriptSuccess = await this.runScript(context, entity);

// Capture context after execution
await this.captureContextVariables(context, executionContext);

if (!scriptSuccess) {
// Log warning but continue
}
}

// 4. Always continue to children
return { action: 'process_children', children: node.children || [] };
}

Key Behavior

  • No conditions — Action entities don't evaluate conditions
  • No model/prompt — Pure script execution
  • Always continues — Even on script failure
  • Captures context — Variables available downstream

Return Actions

All evaluators return an action object:

ActionMeaningUsed By
exit_workflowStop this workflow, try nextEventEvaluator
process_childrenContinue to child nodesAll evaluators
workflow_completeWorkflow finished, stop processingEvaluator (automatic)

Action Object Structure

interface EvaluatorResult {
action: 'exit_workflow' | 'process_children' | 'workflow_complete';
children?: WorkflowNode[]; // For process_children
}

Script Execution Details

Wrapping for Async

All scripts are wrapped to support await:

async runScript(context, entity) {
const wrappedScript = `(async () => { ${entity.script} })()`;

await context.eval(wrappedScript, {
timeout: this.scriptTimeout,
promise: true
});
}

Timeout Handling

await context.eval(wrappedScript, { 
timeout: this.scriptTimeout // Default 5000ms
});

// On timeout:
// Error: Script execution timed out

Error Recovery

try {
await this.runScript(context, entity);
} catch (error) {
const isTimeout = error.message?.includes('Script execution timed out');

logger.error({
error: error.message,
entityId: entity.id,
isTimeout,
}, isTimeout ? 'Script timeout' : 'Script error');

return false; // Script failed
}

Context Variable Capture

After script execution, all context variables are captured:

async captureContextVariables(context, executionContext) {
const varsJson = await context.eval(`JSON.stringify(
Object.keys(global).reduce((acc, key) => {
if (!key.startsWith('__') && typeof global[key] !== 'function') {
try {
acc[key] = global[key];
} catch (e) {}
}
return acc;
}, {})
)`);

const vars = JSON.parse(varsJson);
Object.assign(executionContext, vars);
}

This makes variables set by one entity available to downstream entities.