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
| Method | Description |
|---|---|
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
| Mode | Configuration | Behavior |
|---|---|---|
| Single Path | tfCondition: "Single Path" | Continue if true, exit if false |
| True/False | tfCondition: "True/False" | Branch based on boolean result |
| Multi | tfCondition: "Multi", logicField | Branch 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:
| Action | Meaning | Used By |
|---|---|---|
exit_workflow | Stop this workflow, try next | EventEvaluator |
process_children | Continue to child nodes | All evaluators |
workflow_complete | Workflow finished, stop processing | Evaluator (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.
Related Topics
- Evaluator Overview — Main orchestrator
- Workflow Entities — Entity configuration
- Scripts Reference — Available script functions
- Processed Messages — View workflow results