jsfe
v0.9.95
Published
TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.
Maintainers
Readme
JSFE — JavaScript Flow Engine
ESM TypeScript library for workflow + tool orchestration.
Install
```bash npm i jsfe ```
📖 Documentation
- JavaScript Flow Engine User Guide
- Comprehensive tutorials, examples, and best practices
- README.md - Technical API reference (this document)
- System Flows and Shopify Flows - the bundled flow and tool libraries in
flows/, step by step
For detailed tutorials, step-by-step examples, and comprehensive workflow patterns, see the User Guide.
Usage
import { WorkflowEngine } from "jsfe";
// 1. Create the engine
const engine = new WorkflowEngine(
hostLogger, // Your logging instance
aiCallback, // Your AI communication function
flowsMenu, // Array of flow definitions
toolsRegistry, // Array of tool definitions
APPROVED_FUNCTIONS, // Pre-approved local functions
globalVariables, // Optional: Session-wide variables
validateOnInit, // Optional: Enable pre-flight validation (default: true)
language, // Optional: User's preferred language
aiTimeOut, // Optional: AI timeout in milliseconds (default: 2000ms)
messageRegistry, // Optional: Custom message templates
guidanceConfig // Optional: User assistance configuration
);
// Optional: AI-Powered Intent Detection & Parameter Extraction
// You can manually invoke the engine's AI logic to detect flows and extract parameters
// without executing the flow immediately. This is useful for pre-flight checks or UI hints.
const detectionResult = await engine.detectFlowWithParameters(input);
if (detectionResult?.flow) {
console.log(`Input triggers flow: ${detectionResult.flow.name}`);
// If the flow definition includes parameters, the AI extracts them from the prompt
// Example: "Send $50 to Bob" -> { amount: "50", payee: "Bob" }
if (detectionResult.parameters) {
console.log(`Extracted parameters:`, detectionResult.parameters);
}
}
// 2. Initialize a session for each user
let sessionContext = engine.initSession(yourLogger, 'user-123', 'session-456');
// 3. Process user message and update session context
const userEntry = {
role: 'user',
content: 'I need help with my account',
};
const updatedSessionContext = await engine.updateActivity(userEntry, sessionContext);
sessionContext = updatedSessionContext; // CRITICAL: Always update your session reference
if (updatedSessionContext.response) {
// Intent detected and handled - return the workflow response
// If flow activated you should not proceed to your normal handling
return updatedSessionContext.response;
}
// 4. Call your normal Generate reply process as usual
const reply = await yourConversationalReply(input);
// 5. Update the engine's context with the generated reply
const assistantEntry = {
role: 'assistant',
content: reply, // The reply generated by your process
};
sessionContext = await engine.updateActivity(assistantEntry, sessionContext);
// Return the generated reply to the user
return reply;Engine Initialization Parameters
The WorkflowEngine constructor accepts the following parameters in order, each serving a specific purpose in the engine's operation:
1. hostLogger (Logger | null)
- Purpose: Primary logging interface for the host application
- Requirements: Must support
.debug(),.info(),.warn(),.error()methods - Usage: Engine uses this for all operational logging and debugging output
- Example: Winston, or custom logger implementation
- Nullable: Can be
nullto disable host application logging
2. aiCallback (Function)
- Purpose: AI communication function for intent detection and response generation
- Signature:
async (systemInstruction: string, userMessage: string, jsonSchema?: string) => string— the optionaljsonSchema, when supplied by the engine, asks the host to request structured/JSON output conforming to that schema - Integration: Engine calls this function when AI analysis is needed
- Requirements: Must return AI response as string, handle errors gracefully
- Details: See dedicated AI Callback Function section below
3. flowsMenu (FlowDefinition[])
- Purpose: Array of available workflow definitions
- Content: All workflows that the engine can detect and execute
- Validation: Engine validates flow structure during initialization
- Requirements: Each flow must have valid id, name, description, and steps
- Primary Property: Use
primary: trueto mark user-facing entry point flows vs. helper sub-flows
4. toolsRegistry (ToolDefinition[])
- Purpose: Array of external tool definitions for CALL-TOOL steps
- Content: HTTP APIs, local functions, and mock tools available to workflows
- Validation: Parameter schemas validated against OpenAI Function Calling Standard
- Security: Tools define their own security levels and authentication requirements
5. APPROVED_FUNCTIONS (Object)
- Purpose: Secure registry of pre-approved local JavaScript functions
- Security: Only functions in this object can be executed by local-type tools
- Format: Plain object where keys are function names and values are the actual functions
- Validation: Functions must match tool definitions in toolsRegistry
6. globalVariables (Record<string, unknown>, optional)
- Purpose: Session-wide variables accessible to all workflows
- Scope: Available to all flows in the session via variable interpolation
- Security: Safe sharing of host application data with workflows
- Examples: User ID, session ID, application configuration, environmental data
- Nature: STATIC - Set during engine initialization, same for all sessions
Alternative Setting logger: engine.logger = logger
hostLogger (Logger)
- Purpose: Primary logging interface for the host application
- Requirements: Must support
.debug(),.info(),.warn(),.error()methods - Usage: Engine uses this for all operational logging and debugging output
- Example: Winston, or custom logger implementation
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'warn', // Enable debug logging to trace validation
format: winston.format.printf(({ level, message }) => {
return `${level}: ${message}`;
}),
transports: [
new winston.transports.Console()
]
});
engine.logger = logger;Dynamic Session Data: The cargo Property
Unlike globalVariables which are static and shared across all sessions, each EngineSessionContext has a cargo property for dynamic, session-specific data sharing:
**hostLogger** (Logger | null)
- **Purpose**: Primary logging interface for the host application
- **Requirements**: Must support `.debug()`, `.info()`, `.warn()`, `.error()` methods
- **Usage**: Engine uses this for all operational logging and debugging output
- **Example**: Winston, or custom logger implementation
- **Nullable**: Can be `null` to disable host application logging
// After initSession, you can set dynamic session data
let sessionContext = engine.initSession('user-123', 'session-456');
// Set dynamic data that workflows can access
sessionContext.cargo.userPreferences = { theme: 'dark', language: 'en' };
sessionContext.cargo.currentOrder = { id: 'ORD-123', total: 99.99 };
sessionContext.cargo.temporaryData = 'Any dynamic content';
// Workflows can reference cargo data with variable interpolation
// Example in flow step: "Your order {{cargo.currentOrder.id}} total is ${{cargo.currentOrder.total}}"Key Differences: globalVariables vs cargo
- globalVariables: Static, engine-wide, set at initialization, same for all sessions
- cargo: Dynamic, session-specific, can be modified anytime during conversation
- Use Cases:
globalVariables: API keys, system config, app metadatacargo: User data, conversation state, temporary values, personalization
7. validateOnInit (boolean, optional)
- Purpose: Enable comprehensive flow and tool validation during initialization
- Default:
true- recommended for development and production - Performance: Set to
falseonly in high-performance scenarios with pre-validated flows - Output: Detailed validation reports with errors, warnings, and success metrics
- Primary Flow Validation: Only flows marked with
primary: trueare validated as top-level workflows- Sub-flows are validated in context when called from primary flows
- Prevents validation errors for helper flows that depend on parent flow variables
- Ensures complete validation coverage through deep traversal of flow call graphs
8. language (string, optional)
- Purpose: User's preferred language for localized messages and prompts
- Format: ISO language code ('en', 'es', 'fr', 'de', etc.)
- Default: 'en' if not specified
- Usage: Engine selects appropriate prompt_xx properties from flow definitions
9. aiTimeOut (number, optional)
- Purpose: Timeout in milliseconds for AI callback function calls
- Default: 2000ms (2 second) if not specified
- Usage: Prevents AI calls from hanging indefinitely, providing better reliability
- Range: Recommended range 1000-30000ms depending on AI service response times
- Special Value: Set to
0to disable timeout (no time limit on AI calls) - Error Handling: Throws timeout error if AI call exceeds specified duration
// Examples of different timeout configurations:
// Fast local AI (short timeout)
const fastEngine = new WorkflowEngine(logger, aiCallback, flows, tools, functions,
{}, true, 'en', 500); // 500ms timeout
// Fast cloud AI
const standardEngine = new WorkflowEngine(logger, aiCallback, flows, tools, functions,
{}, true, 'en', 1000); // 1 second timeout
// No timeout limit (not recommended for production)
const noTimeoutEngine = new WorkflowEngine(logger, aiCallback, flows, tools, functions,
{}, true, 'en', 0); // 0 = no timeout10. messageRegistry (MessageRegistry, optional)
- Purpose: Custom message templates for engine-generated user messages
- Format: Multi-language message registry with customizable system messages
- Override: Allows customization of built-in engine messages
- Localization: Supports multiple languages with fallback to default messages
11. guidanceConfig (GuidanceConfig, optional)
- Purpose: Configuration for user guidance and help messages
- Features: Controls how and when the engine provides user assistance
- Modes: Append, prepend, template, or none for guidance integration
- Context: Different guidance for general vs. payment/financial workflows
AI Callback Function
The aiCallback parameter provides the engine access to your AI system for intent detection and workflow triggering. Here's a minimal implementation example:
// Minimal AI callback implementation.
// The engine passes an optional third arg, jsonSchema; when present, request
// structured/JSON output from the model conforming to it.
async function aiCallback(systemInstruction, userMessage, jsonSchema) {
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.OPENAI_API_KEY}`
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [
{ role: "system", content: systemInstruction },
{ role: "user", content: userMessage }
],
temperature: 0.1,
max_tokens: 200
})
});
if (!response.ok) {
throw new Error(`AI API request failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
return data.choices[0].message.content.trim();
} catch (error) {
throw new Error(`AI communication failed: ${error.message}`);
}
}AI Callback Interface:
- Input:
systemInstruction(string),userMessage(string), optionaljsonSchema(string), and optionalrequest(object — intent detection only, see below) - Output: AI response as a string
- Purpose: Analyzes user input to detect workflow intents and generate responses
- Integration: The engine calls this function when it needs AI analysis for intent detection
How the Engine Generates Input Arguments:
systemInstruction: Dynamically generated by the engine based on:- Available flow definitions and their descriptions
- Current session context and active flows
- Current conversation state and collected variables
userMessage: Intelligently composed by the engine, including:- The actual user input/prompt
- Relevant contextual information from the conversation
- Session state and variables needed for intent analysis
Both parameters are carefully engineered by the engine to work together for optimal intent detection. The engine automatically constructs comprehensive, context-aware prompts that provide the AI with all necessary information for accurate workflow selection and response generation. Your aiCallback implementation only needs to send these pre-constructed arguments to your AI service and return the response.
The structured request argument (intent detection only):
For intent detection — and only there — the engine passes a 4th argument: the same call in
structured form, so a host can answer it with a classifier instead of a text model.
async function aiCallback(systemInstruction, userMessage, jsonSchema, request) {
if (request?.task === 'detect_flow') {
// request.input — the user input, exactly as in <user-input>
// request.conversation — the last user/assistant turn: [{ role, content }], oldest first; [] when none
// request.flows — [{ id, name, description, parameters: [{ name, description, type?, enum? }] }]
// Reply exactly as the text path does: a JSON string { flowName, parameters }, flowName being a
// flow's name (or "None"). The engine validates it the same way, enums included.
}
// ... every other call, and the text path: unchanged
}systemInstruction and userMessage are still the complete text prompt on that call, so a host can
run its text model alongside the classifier (for example, for free-text parameters). A callback that
ignores the 4th argument behaves exactly as before; every other call still receives three arguments.
Host helpers — jsfe/host:
Pure functions for the host side of aiCallback, so a production host and a local test harness
share one implementation. Loading jsfe/host never loads the engine.
import { buildAiRequest, isDetectFlowRequest, toFlowRequest, fromFlowAnswers } from 'jsfe/host';
// An OpenAI-compatible chat-completions body from a model description. It picks the right
// response_format for each kind of jsonSchema argument, and never sends `response_format: null`.
const body = buildAiRequest(
{ name: 'gpt-4o-mini', reservedReplySize: 1024, supportsSchema: true },
systemInstruction, userMessage, jsonSchema);buildAiRequest(model, systemInstruction, userMessage, jsonSchema),parseSchemaArg,schemaName.modelfields:name,reservedReplySize, and the optionaltokenParam,supportsSchema,fixedSamplingandextraParams.isDetectFlowRequest,toFlowRequest/fromFlowAnswers(which flow),hasJevParameters/toParameterRequest/fromParameterAnswers(its enum and boolean parameters),freeTextParameters,mergeTextParameters,INTENT_YES_THRESHOLD: these map the structured intent-detection request to TypeSafe System One and back. The host sends the requests.- Stage 1 asks one yes/no probability per flow and picks the highest if it reaches
INTENT_YES_THRESHOLD(0.5), else"None". Stage 2, for the chosen flow only, asks one per enum value and one per boolean parameter. Below the threshold a parameter is omitted (the flow asks for it), never set tofalse. - Any question that comes back unanswered makes the decoder throw, so the host can fall back to a text model rather than read a missing answer as "no".
mergeTextParametersadds a text model's free-text parameters only when the text model chose the same flow. The classifier's enum and boolean values always win.
- Stage 1 asks one yes/no probability per flow and picks the highest if it reaches
Alternative AI Services: You can integrate any AI service (Claude, Gemini, local LLMs, etc.) by implementing this same interface. The engine only requires a function that takes system instructions and user input, then returns an AI response.
Core Registries
The engine operates through four primary registries that define its capabilities:
1. Flows Registry - Workflow Definitions
const flowsMenu = [
{
id: "payment-workflow",
name: "ProcessPayment",
prompt: "Process a payment",
description: "Handle payment processing with validation",
primary: true, // Optional: Marks this as a primary (user-facing) flow
steps: [
{ type: "SAY", value: "Let's process your payment." },
{ type: "SAY-GET", variable: "amount", value: "Enter amount:" },
{ type: "CALL-TOOL", tool: "PaymentProcessor", args: {...}, variable: "payment_result", }
]
}
];Flow Definition Properties
id(required): Unique identifier for the flowname(required): Human-readable flow name used in execution contextprompt(required): User-facing description for AI intent detectiondescription(required): Detailed description of the flow's purposeprimary(optional): Boolean flag marking user-facing entry point flowstrue: Primary flows are standalone workflows that users can directly triggerfalseor omitted: Helper/sub-flows called by other flows, not directly accessible- Validation Impact: Only primary flows are validated as top-level workflows during initialization
- AI Detection: Only primary flows are considered for intent detection and user interaction
parameters(optional): Values intent detection may extract from the triggering input and inject as flow variables. Each is{ name, description, type?, enum? }:typedefaults to"string".enum(string parameters only) lists the allowed values. The prompt shows them (ticket_type (string, one of: "Fraud" | "Cancellation")), and the engine enforces them on the reply: a value matching ignoring case and surrounding whitespace is replaced by the declared spelling, and any other value is dropped with a warning — so the flow asks for it instead of running on a value it never declared. Validation rejects an empty enum, a non-string or padded value, a value repeated ignoring case, and an enum on a non-string type.- Declare an enum wherever the allowed values are a closed set. It also lets a host answer intent
detection with a classifier instead of a text model (see the
requestargument ofaiCallback). - Validation also requires
parametersto be an array, every parameter to have a non-emptyname, and names to be unique within the flow.
steps(required): Array of workflow steps to executevariables(optional): Flow-specific variable definitions with types and descriptionsversion(optional): Flow version for compatibility tracking (defaults to "1.0")
Primary vs. Sub-Flow Architecture
The primary property enables a clean separation between:
Primary Flows (primary: true):
- User-facing workflows that can be directly triggered by user input
- Entry points for specific business processes (e.g., "StartPayment", "CreateTicket")
- Included in AI intent detection and flow selection
- Validated as standalone workflows during engine initialization
Sub-Flows (no primary property or primary: false):
- Helper workflows called by other flows (e.g., "RetryPayment", "ValidateInput")
- Internal logic flows for error handling, validation, or shared functionality
- Not directly accessible to users through intent detection
- Validated only when called from primary flows, ensuring proper variable context
This architecture prevents validation errors for sub-flows that depend on variables from their calling flows, while ensuring complete validation coverage during the engine's deep validation process.
Referencing Tool Results in Later Steps
When a CALL-TOOL step finishes, it can store the returned data into a variable you name via the variable property. That variable will hold the entire return object from the tool.
Using variable
{
"type": "CALL-TOOL",
"tool": "CreateSupportTicket",
"variable": "ticket_result",
"args": {
"subject": "{{subject}}",
"description": "{{description}}",
"customer_email": "{{customer_email}}"
}
},
{
"type": "SAY",
"value": "Ticket created: {{ticket_result.ticket.id}} — we'll email updates to {{ticket_result.ticket.customer_email}}."
}Important:
Whatever your tool returns becomes the value of the variable you specify. Because you get the raw return object, you do not need to use .result in your template paths—just reference the keys the tool returns (ticket_result.ticket.id, ticket_result.ok, etc.). If you omit variable, you won’t be able to access the tool’s output later.
2. Tools Registry - External Integrations
const toolsRegistry = [
{
id: "PaymentProcessor",
name: "Process Payment",
description: "Processes financial transactions securely",
parameters: { /* OpenAI Function Calling Standard Schema */ },
implementation: {
type: "local", // or "http" for REST APIs
function: "processPayment",
timeout: 10000
},
security: {
requiresAuth: true,
auditLevel: "critical",
dataClassification: "financial"
},
// Optional output contract: the shape of what the CALL-TOOL `variable` receives (after
// responseMapping, if any). Lets flow authors and AI co-pilots read result paths
// (payment_result.transaction.id) from the definition instead of guessing them.
returns: {
type: "object",
required: ["success"],
properties: {
success: { type: "boolean" },
transaction: { type: "object", properties: { id: { type: "string" } } }
}
}
}
];returns is optional and never changes behaviour. A tool with or without it runs identically.
A host can opt in to a diagnostic, engine.validateToolReturns = true (default false), which checks
each result against returns and logs a warning on a mismatch or on a schema that does not compile.
The result is still delivered unchanged either way.
3. Approved Functions Registry - Secure Local Functions
const APPROVED_FUNCTIONS = {};
// Define secure local functions
async function processPayment(args) {
// Secure payment processing logic
return { transactionId: "...", status: "success" };
}
// Register approved functions
APPROVED_FUNCTIONS['processPayment'] = processPayment;4. Global Variables - Secure Sharing of Local Data
const globalVariables = {
caller_id: "(555) 123-4567",
caller_name: "John Doe",
thread_id: "conversation-123"
};Session Management
- Each user requires a unique session context via
initSession(userId, sessionId) - CRITICAL:
updateActivity()returns an updatedEngineSessionContext - You must always update your session reference on every call
- The
EngineSessionContextobject should be persisted by your application - Pass the same session context to
updateActivityfor conversation continuity - Session Isolation: Each user/session must have its own context to prevent state contamination
Session Cargo for Dynamic Data
Each session context includes a cargo property for dynamic, session-specific data that workflows can access:
// Initialize session
let sessionContext = engine.initSession('user-123', 'session-456');
// Set dynamic session data that workflows can reference
sessionContext.cargo.userProfile = {
name: 'John Doe',
tier: 'premium',
preferences: { notifications: true }
};
// Workflows can access cargo data: "Welcome {{cargo.userProfile.name}}!"
sessionContext = await engine.updateActivity(userEntry, sessionContext);Voice Sessions: cargo.voice and cargo.digits
Two cargo fields connect the engine to a voice channel:
cargo.voice(set by the host): when truthy, the answer aSAY-GETcollects into itsvariableis cleaned byaiCallbackbefore it is stored. The cleaner receives the question just asked and the caller's words. It removes filler words, maps yes/no answers to"yes"/"no", and writes spoken numbers, phone numbers and emails in their written form ("john dot doe at gmail dot com" →[email protected]). It never returns an email address or phone number that appears in the question: when a prompt reads a failed value back ("I could not verify the email [email protected]…"), the caller's answer is treated as a correction, and a near-miss such asrpinkas@…is kept as said. If the cleanup fails or returns nothing, the raw answer is used.cargo.digits(set by the engine): when aSAY-GETstep declaresdigits(for example{ "min": 6, "max": 6 }), the engine copies it tocargo.digitswhile the answer is awaited, so the host can configure keypad (DTMF) collection. It is removed once the input is collected.
Correct Session Update Pattern
// Initialize session
let sessionContext = engine.initSession('user-123', 'session-456');
// For every updateActivity call, capture the returned context
sessionContext = await engine.updateActivity(userEntry, sessionContext);
// Check for workflow response
if (sessionContext.response) {
return sessionContext.response;
}
// Continue with regular conversation...
sessionContext = await engine.updateActivity(assistantEntry, sessionContext);❌ Common Mistake - Not Updating Session Reference
// WRONG - This will cause session corruption
const sessionContext = engine.initSession('user-123', 'session-456');
await engine.updateActivity(userEntry, sessionContext); // Session state lost!✅ Correct Pattern - Always Update Session Reference
// CORRECT - Session state maintained
let sessionContext = engine.initSession('user-123', 'session-456');
sessionContext = await engine.updateActivity(userEntry, sessionContext); // Session updatedContext Entry Types
- User input:
contextEntry.role = 'user'- analyzed and may trigger flow execution - Assistant response:
contextEntry.role = 'assistant'- added to context for awareness
ContextEntry Structure
interface ContextEntry {
role: 'user' | 'assistant' | 'system' | 'tool'; // Message role type
content: string | Record<string, unknown>; // Message content (text, object, etc.)
timestamp: number; // Unix timestamp in milliseconds
stepId?: string; // Optional: Used by the system when a Step calls a Tool
toolName?: string; // Optional: Used by the system to record Tool result into the chat context
metadata?: Record<string, unknown>; // Optional: Additional context data
}UpdateActivity Method
The updateActivity method is the primary interface for processing user inputs and assistant responses. It returns an updated EngineSessionContext that must be captured and used for subsequent calls.
Method Signature
async updateActivity(
contextEntry: ContextEntry,
engineSessionContext: EngineSessionContext
): Promise<EngineSessionContext>Return Value
The method returns an updated EngineSessionContext containing:
- Updated flow state: Modified flow stacks, variables, and execution context
- Response data: If a workflow was triggered,
sessionContext.responsecontains the result - Session metadata: Updated timestamps, accumulated messages, and transaction data
lastTurnToolCalls: every tool call thisupdateActivityattempted (see Host Responsibilities)lastFlowOutcome: the declared outcome of a flow that terminated during this call, if any (see Declared flow outcomes)lastTurnDispatch:{ fromFlow, matchedFlow }when aDISPATCHstep re-routed this call's input (matchedFlowisnullwhen no flow matched and the response isnull); one-shot
Integration Pattern
// Process user input
let sessionContext = await engine.updateActivity({
role: 'user',
content: userMessage,
timestamp: Date.now()
}, sessionContext);
// Check if a workflow was activated
if (sessionContext.response) {
// Workflow handled the input - return the workflow response
return sessionContext.response;
}
// No workflow triggered - proceed with normal conversation
const aiResponse = await yourAIFunction(userMessage);
// Update context with AI response
sessionContext = await engine.updateActivity({
role: 'assistant',
content: aiResponse,
timestamp: Date.now()
}, sessionContext);
return aiResponse;Critical Notes
- Always capture the return value: Failing to update your session reference will cause state corruption
- Session isolation: Each user/conversation needs its own session context
- Response checking: Check
sessionContext.responseto determine if a workflow handled the input - Persistence: Your application should persist the updated session context between requests
JSFEActiveFlowEmptyResponse: if a flow is still active but its last step produced no output and was not aRETURN,updateActivityresets the session's flow stacks (cargo and global variables are kept), clears pending messages, and throws an error with thisname. The session is already repaired: the next turn starts with fresh intent detection. Log or record the error; no rollback is needed. A deliberateRETURN ''(which ends every flow) is not flagged.
Host Responsibilities: Tool Calls, Cancellation and Concurrency
A flow can take actions in the outside world through CALL-TOOL — charge a card, send an SMS,
create a ticket. The session returned by updateActivity is the only record that those steps
already ran. The engine cannot protect that record once it leaves updateActivity; the host
owns it from there. Getting any of the rules below wrong lets a flow repeat an action the user
asked for once.
This is not hypothetical. In production a caller confirmed a payment and repeated "confirm" a
second later. The host cancelled the first turn while its charge was in flight and restored the
session it had snapshotted before the turn. The flow was back at "Say CONFIRM to pay", the
repeated word confirmed it again, and the card was charged twice.
tests/host-tool-calls.test.mjs reproduces it.
1. Never restore a pre-turn session after the turn attempted a tool.
engineSessionContext.lastTurnToolCalls lists every tool call the most recent updateActivity
attempted. A host may discard or roll back a turn's session only when that list is empty. When it
is not, persist exactly the session updateActivity returned, even if the turn's reply is never
delivered.
2. A cancelled turn is still running — wait for it. Cancelling on the host side (the user
barged in, a request was abandoned, a host timeout fired) does not stop the engine or a tool it
is calling. Always await the in-flight updateActivity before persisting or discarding the
session, and before passing that session to another updateActivity call.
3. One updateActivity at a time per conversation, and per engine instance. The engine keeps
a reference to the session it is processing. Two concurrent calls on the same conversation both
start from the pre-call state and both execute its next step, and concurrent calls on one engine
instance corrupt each other's session reference. Serialise per conversation (a lock, a queue),
and make a newer turn wait for an older turn's updateActivity to finish rather than proceed
after a fixed timeout.
4. Make external side effects idempotent. The rules above close every gap the engine and host can see. They cannot close one: a host process that dies after a tool took effect but before the session was persisted. For actions that must never repeat, such as payments, pass an idempotency key to the external service (for example a transaction id the flow generates once and stores in a variable) so a repeat is rejected at the source.
5. Log what updateActivity throws — the engine does not. An engine AI call that fails
(an aiTimeOut, a vendor error) reaches the host as AI task processing failed: AI communication
failed: <cause>, and every layer it passes through traces it at debug only, so one failure is
one line — the host's, at the level its outcome deserves (a fallback answer: warn; a
JSFEExecutionError the user hears: error). Since 0.9.94; before it, one timeout wrote six
warn/error lines inside the engine. tests/ai-failure-logging.test.mjs pins it.
lastTurnToolCalls
interface ToolCallRecord {
tool: string; // tool name
implementation: string; // 'local' | 'http' | ...
transactionId: string | null;
at: number; // epoch ms
}- One-shot. Reset at the start of every
updateActivitycall, user or assistant role, so it describes that call only. Read it before making the nextupdateActivitycall. - Attempts, not successes. A record is written before the tool is invoked, including retries and calls that then throw or time out, because those may still have taken effect remotely.
- Covers every
CALL-TOOLstep. Local functions (APPROVED_FUNCTIONS) and HTTP tools pass through the same entry point. - Only calls that were attempted. A call refused before invocation (argument validation, rate limiting) is not recorded, because nothing was sent.
- Does NOT cover functions called from expressions.
APPROVED_FUNCTIONSare also callable inside templates,SETvalues, conditions andcustomValidator, and those calls are not recorded. This is deliberate: expressions call formatting and matching helpers on nearly every turn, and recording them would make every turn look like it acted, so a host could never discard a turn at all. Anything with an external side effect must be invoked throughCALL-TOOL, never from an expression, or the host cannot see it.
const before = structuredClone(session);
// Awaited even when the turn is being cancelled: the engine and its tools keep running.
const after = await engine.updateActivity({ role: 'user', content: input }, session);
if (turnWasCancelled && after.lastTurnToolCalls.length === 0) {
session = before; // nothing happened in the world: safe to discard an unheard prompt
} else {
session = after; // a tool may have run: keep the record of it
}
await persist(session);Declared flow outcomes: lastFlowOutcome
A flow can say how it ended, not just that it ended. An optional outcome (and reason) on a step
is remembered on the flow and applied when the flow terminates. Put it on the step that ends the
flow: a RETURN, an END, a DISPATCH or a final SAY. Both may use {{…}} interpolation with
the flow's variables; an outcome that interpolates to empty declares nothing:
{ "id": "say_support_message", "type": "SAY", "value": "Sorry I couldn't help! ...",
"outcome": "unresolved", "reason": "contact_support_fallback" }"unresolved": the flow ended without resolving the user's need. Its transaction is marked failed (reason =reason, or"unresolved"), so audits count it as a failure, not a success."deflected": a decoy flow absorbed an intent-detection false positive by design. The transaction completes; do not treat it as a user struggle."resolved"or any other value: informational; the transaction completes.- A flow that declares nothing completes exactly as before.
The engine also stamps engineSessionContext.lastFlowOutcome for the host:
interface FlowOutcome {
flowName: string;
outcome: string;
reason?: string;
endedBy: 'return' | 'completion' | 'dispatch'; // RETURN ended all flows; completion = ran out of steps (incl. after END); dispatch = a DISPATCH ended them and no flow matched
}A DISPATCH that starts a flow swallows the ended flows' outcome; one that matches nothing keeps it.
It is one-shot: cleared at the start of every updateActivity, and set only when a flow that
declared an outcome terminated during that call. Hosts use it to run a recovery turn after an
"unresolved" ending. When a RETURN terminates several flows, only the flow that executed it
carries the RETURN's declaration; the others finalize with their own (usually none).
Architecture Overview
Stack-of-Stacks Design
The Flow Engine implements a sophisticated "stack-of-stacks" architecture that allows flows to be suspended and resumed, enabling users to naturally interrupt one workflow to handle another task, then return to their original workflow seamlessly.
Core Components
Multiple Independent Flow Execution Stacks
- Active stack index tracks current execution context
- Automatic stack switching for flow interruption/resumption
- Proper isolation between different workflow contexts
Flow Frame Structure (Runtime Execution Context) Each flow execution maintains a complete context frame:
interface FlowFrame { flowName: string; // Human-readable flow name (from FlowDefinition.name) flowId: string; // Unique flow identifier (from FlowDefinition.id) flowVersion: string; // Flow version for compatibility (from FlowDefinition.version) flowStepsStack: FlowStep[]; // Remaining steps (reversed for efficient pop) contextStack: ContextEntry[]; // Complete interaction history with role info inputStack: unknown[]; // Current input context for step execution variables: Record<string, unknown>; // Unified variable storage (shared across sub-flows) transaction: TransactionObj; // Comprehensive transaction and audit tracking userId: string; // User identifier for this flow session startTime: number; // Flow start timestamp for timing analysis pendingVariable?: string; // Variable name awaiting user input (SAY-GET steps) lastSayMessage?: string; // Last SAY step output for context pendingInterruption?: Record<string, unknown>; // Interruption state management accumulatedMessages?: string[]; // Accumulated SAY messages for batching declaredOutcome?: { outcome: string; reason?: string }; // From a step's `outcome`, applied when the frame terminates parentTransaction?: string; // Parent transaction ID for sub-flow tracking justResumed?: boolean; // Flag indicating flow was just resumed }Technical Implementation Details:
- Flow Identity:
flowName,flowId,flowVersionare copied from the FlowDefinition - Dynamic Properties: Flow
prompt,description, and localized prompts are accessed dynamically from FlowDefinition - flowStepsStack: Steps stored in reverse order for efficient pop operations
- contextStack: Enhanced with role information for complete conversation context
- variables: Flat storage shared across sub-flows for seamless data passing
- accumulatedMessages: SAY steps are batched for efficient output
- justResumed: Helps engine provide appropriate resumption messages
- Flow Identity:
Helper Function Architecture All stack operations go through centralized helper functions:
initializeFlowStacks(engine): Ensures proper structuregetCurrentStack(engine): Gets currently active stackpushToCurrentStack(engine, frame): Adds flow to active stackpopFromCurrentStack(engine): Removes flow from active stack
Tool Definition Structure (Runtime Integration Context) Tools provide external capabilities with comprehensive security and validation:
interface ToolDefinition { id: string; // Unique tool identifier name: string; // Human-readable tool name description: string; // Tool functionality description parameters?: { // OpenAI Function Calling Standard schema type: string; // Parameter type structure properties?: Record<string, PropertySchema>; // Parameter validation rules required?: string[]; // Required parameter names additionalProperties?: boolean; // Additional parameter handling }; implementation?: { // Execution configuration type: 'local' | 'http'; // Implementation type function?: string; // Local function name (APPROVED_FUNCTIONS) url?: string; // HTTP endpoint with {param} placeholders method?: HttpMethod; // HTTP method pathParams?: string[]; // URL parameter substitution queryParams?: string[]; // Query string parameters responseMapping?: MappingConfig; // Response transformation config timeout?: number; // Request timeout retries?: number; // Retry attempts }; security?: { // Security controls rateLimit?: { // Rate limiting configuration requests: number; // Max requests per window window: number; // Time window in milliseconds }; }; apiKey?: string; // Authentication token riskLevel?: 'low' | 'medium' | 'high'; // Security classification returns?: object; // JSON Schema of the result the flow receives (optional) }Technical Implementation Details:
- Parameter Validation: JSON Schema validation with ajv for type safety
- Local Function Execution: Secure execution through APPROVED_FUNCTIONS registry
- HTTP Integration: Full REST API support with authentication and retries
- Response Mapping: Declarative transformation without code injection
- Security Controls: Rate limiting, risk classification, audit logging
- Error Handling: Automatic retry logic and graceful degradation
Response Mapping Configuration (MappingConfig Interface) The comprehensive response mapping system supports multiple transformation types:
export type MappingConfig = | JsonPathMappingConfig | ObjectMappingConfig | ArrayMappingConfig | TemplateMappingConfig | ConditionalMappingConfig | PathConfig | string | Record<string, unknown>; // JSONPath-based field extraction and transformation export type JsonPathMappingConfig = { type: 'jsonPath'; mappings: Record<string, { path: string; // JSONPath expression for data extraction transform?: ValueTransformConfig; // Optional value transformation fallback?: unknown; // Fallback value if path not found }>; strict?: boolean; // Strict mode validation }; // Object structure mapping with nested support export type ObjectMappingConfig = { type: 'object'; mappings: Record<string, string | PathConfig | MappingConfig | object>; strict?: boolean; // Strict mode validation }; // Array processing with filtering, sorting, and pagination export type ArrayMappingConfig = { type: 'array'; source?: string; // Source array path filter?: ConditionConfig; // Filtering conditions itemMapping?: MappingConfig; // Per-item transformation sort?: { field: string; order?: 'asc' | 'desc' }; // Sorting configuration offset?: number; // Pagination offset limit?: number; // Pagination limit fallback?: unknown[]; // Fallback array if source not found }; // Template-based string generation with variable substitution export type TemplateMappingConfig = { type: 'template'; template: string; // Template string with {{variable}} placeholders dataPath?: string; // Optional path to resolve template data from }; // Conditional logic-based mapping selection export type ConditionalMappingConfig = { type: 'conditional'; conditions: Array<{ if: ConditionConfig; // Condition evaluation then: MappingConfig; // Mapping to apply if condition true }>; else?: MappingConfig; // Default mapping if no conditions match }; // Path-based value extraction with transformation export type PathConfig = { path: string; // Data path for extraction transform?: ValueTransformConfig; // Optional value transformation fallback?: unknown; // Fallback value if path not found }; // Comprehensive value transformation system with 25+ transform types export interface ValueTransformConfig { type: 'parseInt' | 'parseFloat' | 'toLowerCase' | 'toUpperCase' | 'trim' | 'replace' | 'concat' | 'regex' | 'date' | 'default' | 'conditional' | 'substring' | 'split' | 'join' | 'abs' | 'round' | 'floor' | 'ceil' | 'template' | 'sum' | 'average' | 'count' | 'min' | 'max' | 'multiply' | 'divide' | 'percentage' | 'add' | 'subtract' | 'currentYear' | 'yearDifference' | 'handlebars' | 'custom'; // Common transformation parameters fallback?: unknown; // Default value for failed transforms prefix?: string; // String prefix for concat operations suffix?: string; // String suffix for concat operations pattern?: string; // Regex pattern for replace/match operations replacement?: string; // Replacement string for regex operations template?: string; // Template string for template transforms value?: unknown; // Static value for default transforms // Mathematical operation parameters precision?: number; // Decimal precision for rounding operations divisor?: number; // Divisor for division/percentage operations multiplier?: number; // Multiplier for multiplication operations addend?: number; // Value to add for addition operations subtrahend?: number; // Value to subtract for subtraction operations // Array and aggregation parameters field?: string; // Field name for array aggregations delimiter?: string; // Delimiter for join/split operations index?: number; // Array index for element selection // Conditional and date parameters condition?: ConditionConfig; // Condition for conditional transforms fromYear?: number; // Start year for year difference calculations dataPath?: string; // Path for accessing context data } // Flexible condition evaluation system export interface ConditionConfig { field: string; // Field path for evaluation operator: 'equals' | 'eq' | 'notEquals' | 'ne' | 'contains' | 'exists' | 'notExists' | 'greaterThan' | 'gt' | 'lessThan' | 'lt' | 'greaterThanOrEqual' | 'gte' | 'lessThanOrEqual' | 'lte' | 'startsWith' | 'endsWith' | 'matches' | 'in' | 'hasLength' | 'isArray' | 'isObject' | 'isString' | 'isNumber'; value?: unknown; // Comparison value for operators }Response Mapping Technical Features:
- Type Safety: Full TypeScript interface definitions with validation
- Declarative Configuration: No code injection - pure JSON configuration
- Comprehensive Transforms: 25+ built-in transformation types
- Mathematical Operations: Arithmetic, statistical, and precision control
- Date Processing: Dynamic date calculations and formatting
- Template System: Handlebars-style variable substitution with array iteration
- Conditional Logic: Complex branching and filtering capabilities
- Array Processing: Filtering, sorting, pagination, and aggregation
- Path Resolution: JSONPath support with fallback handling
- Security: Complete input sanitization and validation
Features & Capabilities
Flow Execution Modes
- ✅ Linear Flow Execution - Sequential step processing
- ✅ Sub-Flow Calls - Nested workflow execution
- ✅ Flow Interruption - Suspend current flow for new task
- ✅ Flow Resumption - Return to previously suspended flows
- ✅ Flow Replacement - Replace current flow with new flow
- ✅ Flow Reboot - Nuclear option: clear all flows and restart (SAY output accumulated before the reboot is kept and delivered with the new flow's output)
Step Types
- ✅ SAY - Non-blocking output messages (accumulated)
- ✅ SAY-GET - Blocking output with user input request
- ✅ SET - Variable assignment with interpolation support
- ✅ CALL-TOOL - External tool execution with error handling
- ✅ FLOW - Sub-flow execution with multiple call types
- ✅ CASE - Conditional branching with expressions
- ✅ SWITCH - Conditional branching based on single value matching
- ✅ RETURN - Terminate all flows and return evaluated expression value (an EXIT/abort)
- ✅ END - Return from the current flow only, resuming the parent flow — a functional
return(no value; variables are shared with the parent) - ✅ DISPATCH - End all flows and route this turn's input through intent detection, as if no flow had been active (a matched flow starts in the same turn; no match → no response, the host answers). Resets everything; the ended flows' declared outcome is kept only when no flow matches. Takes
id,outcome,reason. See the user guide's DISPATCH Steps
A terminal step (RETURN, END or a final SAY) may also carry outcome / reason; see
Declared flow outcomes.
Step Reliability: Validation & Retry
Any step can also declare:
inputValidation— regexpatterns(field/pattern/message) and/or a namedcustomValidatorfromAPPROVED_FUNCTIONS, applied to collected input before the step runs.- Automatic retry —
maxRetries(default 2),retryDelay,retryStrategy(immediate/exponential/linear/manual), andretryOnConditions(errorPattern→action), falling through toonFailonce exhausted.
Default Timeouts
- AI callback (
aiTimeOut): 2000 ms - HTTP / REST tool calls: 20000 ms
- Local function tool calls: 5000 ms
Each tool may override its own timeout.
Expression System
The JSFE engine features a powerful, unified JavaScript expression evaluator that provides consistent syntax and behavior across all contexts. This system supports the full range of JavaScript expressions while maintaining security through a trusted developer model and limiting user input usage as values.
Core Features
- ✅ Full JavaScript Expression Support - All standard operators, functions, and syntax
- ✅ Type Preservation - Numeric, boolean, and object types maintained correctly
- ✅ Template Interpolation -
{{expression}}syntax for string templates - ✅ Direct Expression Evaluation - Pure expressions return their native JavaScript types
- ✅ Unified Security Model - Same safety framework across all evaluation contexts
Expression Types
Single Expressions (preserve JavaScript types):
// SET steps with pure expressions return native types
{ type: "SET", variable: "count", value: "{{attempt_count + 1}}" } // Returns number
{ type: "SET", variable: "isValid", value: "{{age >= 18 && verified}}" } // Returns boolean
{ type: "SET", variable: "user", value: "{{api_response.user}}" } // Returns objectTemplate Strings (convert to strings for interpolation):
// String templates with embedded expressions
{ type: "SAY", value: "Attempt {{attempt_count + 1}}/{{max_attempts}}" } // String result
{ type: "SAY", value: "Welcome {{user.name}} ({{user.verified ? 'Verified' : 'Unverified'}})" }Supported JavaScript Features
All JavaScript features are supported within a sandboxed environment with access limited to explicitly exported variables and functions.
Security Model
- Developer Trust: Workflows authored by developers, not end users
- Parameter Filtering: Only valid JavaScript identifiers allowed as parameters
- No eval(): Direct Function constructor with parameter validation
- User Input Safety: User inputs are treated as values, not code
Internationalization and Localization
The JSFE engine supports multiple languages through localized properties in flow definitions and steps. This allows you to create workflows that automatically display messages in the user's preferred language.
Flow-Level Localization
Add localized prompts to your flow definitions using the prompt_xx pattern:
const flowsMenu = [
{
id: "payment-flow",
name: "ProcessPayment",
prompt: "Process a payment", // Default (English)
prompt_es: "Procesar un pago", // Spanish
prompt_fr: "Traiter un paiement", // French
prompt_de: "Eine Zahlung bearbeiten", // German
description: "Handle payment processing",
steps: [ /* ... */ ]
}
];Step-Level Localization
Add localized messages to individual steps using the value_xx pattern:
{
type: "SAY-GET",
variable: "amount",
value: "Please enter the payment amount:", // Default (English)
value_es: "Por favor ingrese el monto del pago:", // Spanish
value_fr: "Veuillez saisir le montant du paiement:", // French
value_de: "Bitte geben Sie den Zahlungsbetrag ein:" // German
}Language Selection
Set the user's preferred language when initializing the engine:
const engine = new WorkflowEngine(
logger,
aiCallback,
flowsMenu,
toolsRegistry,
APPROVED_FUNCTIONS,
globalVariables,
true, // validateOnInit
'es', // language - Spanish
1000, // aiTimeOut - 1 second for faster service
messageRegistry,
guidanceConfig
);Supported Language Codes
en- English (default)es- Spanish (Español)pt- Portuguese (Português)fr- French (Français)de- German (Deutsch)- Any ISO 639-1 language code
Language Fallback
If a localized property is not found for the specified language, the engine automatically falls
back to the default prompt or value property. This ensures your flows always work even if
some translations are missing.
// Example: User has language='es' but only English is available
{
type: "SAY",
value: "Welcome to our service!" // Will be used as fallback
// value_es is missing, so English value is used
}Expression Template System
The engine supports safe JavaScript expressions within {{}} templates:
// Simple variables
{{userName}}, {{account.balance}}
// Arithmetic
{{amount + fee}}, {{price * quantity}}
// Comparisons
{{age >= 18}}, {{status === 'active'}}
// Logical operations
{{isAdmin && hasAccess}}, {{retryCount < maxRetries}}
// Complex expressions
{{user.permissions.includes('admin') && creditScore > 700}}JavaScript Method and Function Calls
String Methods:
// Case conversion
{{userInput.toLowerCase().trim()}}
// Email validation
{{email.includes('@') && email.length > 5}}
// Text processing
{{text.substring(0, 10).padEnd(15, '...')}}Array Methods:
// Length and content checks
{{items.length > 0 && items.includes('premium')}}
// Array manipulation
{{categories.slice(0, 3).join(', ')}}Math Methods:
// Tax calculation
{{Math.round(price * 1.08)}}
// Ensure non-negative
{{Math.max(balance, 0)}}Functions:
// Type conversion and validation
{{Number(input) > 0 && !isNaN(Number(input))}}
{{Boolean(user.isActive && user.hasAccess)}}
// URI encoding for API calls
{{encodeURIComponent(searchTerm)}}
// User-defined approved functions
{{currentTime()}} // If registered as approved function
{{extractCryptoFromInput(userMessage)}} // Custom business logicDemo/Test Mode: Flow Matching Without AI
For demos, tests, or developer convenience, you can set aiCallback to null when constructing
the engine. In this mode, intent detection will:
- Match by Flow Name or ID (case-insensitive):
- If the user input exactly matches a flow's
nameorid, that flow is activated.
- If the user input exactly matches a flow's
- Partial Match Fallback:
- If no exact match is found, the engine will look for a flow whose
nameoridcontains the input (case-insensitive).
- If no exact match is found, the engine will look for a flow whose
- No Match:
- If no match is found, no flow is activated.
This makes it easy to run demos and tests without requiring a real AI intent detection function.
In production, always provide a real aiCallback for robust intent detection.
Test Suite Patterns
When creating test suites, follow these patterns to ensure session isolation and prevent state contamination:
Proper Test Isolation Pattern
// ❌ Dangerous - Shared session may cause contamination
const globalSession = engine.initSession('test-user', 'test-session');
for (const testCase of testCases) {
// This will cause session corruption!
await runTest(testCase, globalSession);
}
// ✅ Correct - Fresh session per test
for (let i = 0; i < testCases.length; i++) {
const testCase = testCases[i];
// Create fresh session for each test
let sessionContext = engine.initSession('test-user', `test-session-${i+1}`);
await runTest(testCase, sessionContext);
}Test Function Pattern
async function runTest(inputs, sessionContext) {
for (const input of inputs) {
// Always update session context
sessionContext = await engine.updateActivity({
role: 'user',
content: input,
timestamp: Date.now()
}, sessionContext);
// Check for workflow response
if (sessionContext.response) {
console.log('Workflow Response:', sessionContext.response);
}
}
}This pattern ensures that each test runs in complete isolation, preventing the session corruption that can occur when tests share state.
Security & Compliance
Expression Security
- ✅ Direct JavaScript Evaluation - Native JavaScript execution with security framework
- ✅ Controlled Access - Only exported variables and functions are accessable JavaScript identifiers allowed as parameters scope
- ✅ User Input Safety - User inputs treated as values, with protection against code injection
Transaction Management
- ✅ Comprehensive Audit Trail - Every action logged
- ✅ Transaction State Tracking - Success/failure/pending states
- ✅ Error Recovery Logging - Detailed failure analysis
- ✅ User Context Isolation - Prevent cross-user data leaks
Integration Capabilities
REST API Support
- ✅ HTTP Methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
- ✅ Content Types: JSON, Form-data, URL-encoded, XML/SOAP, Plain text, Multipart
- ✅ Authentication: Bearer tokens, Basic auth, API keys, HMAC signatures
- ✅ Parameter Handling: Path params, Query params, Request body, Headers
- ✅ Advanced Features: Retries with exponential backoff, Timeouts, Rate limiting
Declarative Response Mapping
The engine supports completely generic response transformation through declarative JSON configuration:
JSONPath Mapping
{
responseMapping: {
type: "jsonPath",
mappings: {
"output_field": {
path: "api.response.field[0].value",
transform: { type: "parseInt", fallback: 0 },
fallback: "$args.inputField"
}
}
}
}Object Mapping
{
responseMapping: {
type: "object",
mappings: {
"user_name": "name",
"contact": {
"email": "email",
"phone": "phone"
},
"metadata": {
"processed": true,
"timestamp": "{{new Date().toISOString()}}"
}
}
}
}Template Mapping
{
responseMapping: {
type: "template",
template: "User {{name}} ({{email}}) from {{$args.source}}"
}
}Core Features
- OpenAI Function Calling Standard schemas
- JSON Schema validation with ajv
- Secure function registry
- Comprehensive error handling & transaction management
- Audit logging for compliance
- Rate limiting and input validation
Value Transformations
Supported Transforms
- Number conversion: parseInt, parseFloat
- String manipulation: toLowerCase, toUpperCase, trim
- Text processing: replace (regex), concat (prefix/suffix)
- Date handling: Convert to ISO date string
- Fallback values: Default handling for missing data
Security & Best Practices
- ✅ No code injection possible - all transformations are declarative
- ✅ Secure path traversal with validation
- ✅ Fallback handling for missing data
- ✅ Type coercion with validation
- ✅ Error handling with graceful degradation
Example Tool Configurations
Simple GET Request
{
implementation: {
type: "http",
url: "https://api.example.com/users",
method: "GET"
}
}Authenticated POST with JSON
{
implementation: {
type: "http",
url: "https://api.example.com/users",
method: "POST",
contentType: "application/json"
},
apiKey: "your-bearer-token"
}Path Parameters with Response Mapping
{
implementation: {
type: "http",
url: "https://api.example.com/users/{userId}",
method: "GET",
pathParams: ["userId"],
responseMapping: {
type: "object",
mappings: {
"user_id": "id",
"full_name": "name",
"contact_email": "email"
}
}
}
}Troubleshooting
Common Session Management Issues
Problem: "engineSessionContext.flowStacks was invalid, initialized fresh flowStacks"
Cause: Session context corruption due to improper session management.
Solutions:
Always update session reference:
// ❌ Wrong await engine.updateActivity(entry, sessionContext); // ✅ Correct sessionContext = await engine.updateActivity(entry, sessionContext);Use unique sessions per user/conversation:
// ❌ Wrong - sharing sessions const sharedSession = engine.initSession('shared', 'shared'); // ✅ Correct - isolated sessions const userSession = engine.initSession(`user-${userId}`, `session-${sessionId}`);Initialize fresh sessions for testing:
// For each test case let sessionContext = engine.initSession('test-user', `test-session-${testIndex}`);
Problem: Workflows not triggering or mock responses
Cause: Corrupted session state or improper context handling.
**Solution
