
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.
ESM TypeScript library for workflow + tool orchestration.
```bash npm i jsfe ```
For detailed tutorials, step-by-step examples, and comprehensive workflow patterns, see the User Guide.
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;
The WorkflowEngine constructor accepts the following parameters in order, each serving a specific purpose in the engine's operation:
1. hostLogger (Logger | null)
.debug(), .info(), .warn(), .error() methodsnull to disable host application logging2. aiCallback (Function)
async (systemInstruction: string, userMessage: string) => string3. flowsMenu (FlowDefinition[])
primary: true to mark user-facing entry point flows vs. helper sub-flows4. toolsRegistry (ToolDefinition[])
5. APPROVED_FUNCTIONS (Object)
6. globalVariables (Record<string, unknown>, optional)
hostLogger (Logger)
.debug(), .info(), .warn(), .error() methodsconst 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;
cargo PropertyUnlike 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: API keys, system config, app metadatacargo: User data, conversation state, temporary values, personalization7. validateOnInit (boolean, optional)
true - recommended for development and productionfalse only in high-performance scenarios with pre-validated flowsprimary: true are validated as top-level workflows
8. language (string, optional)
9. aiTimeOut (number, optional)
0 to disable timeout (no time limit on AI calls)// 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 timeout
10. messageRegistry (MessageRegistry, optional)
11. guidanceConfig (GuidanceConfig, optional)
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
async function aiCallback(systemInstruction, userMessage) {
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:
systemInstruction (string), userMessage (string)How the Engine Generates Input Arguments:
systemInstruction: Dynamically generated by the engine based on:
userMessage: Intelligently composed by the engine, including:
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.
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.
The engine operates through four primary registries that define its capabilities:
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", }
]
}
];
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 flows
true: Primary flows are standalone workflows that users can directly triggerfalse or omitted: Helper/sub-flows called by other flows, not directly accessiblesteps (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")The primary property enables a clean separation between:
Primary Flows (primary: true):
Sub-Flows (no primary property or primary: false):
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.
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}}."
}
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.
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"
}
}
];
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;
const globalVariables = {
caller_id: "(555) 123-4567",
caller_name: "John Doe",
thread_id: "conversation-123"
};
initSession(userId, sessionId)updateActivity() returns an updated EngineSessionContextEngineSessionContext object should be persisted by your applicationupdateActivity for conversation continuityEach 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);
// 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);
// WRONG - This will cause session corruption
const sessionContext = engine.initSession('user-123', 'session-456');
await engine.updateActivity(userEntry, sessionContext); // Session state lost!
// CORRECT - Session state maintained
let sessionContext = engine.initSession('user-123', 'session-456');
sessionContext = await engine.updateActivity(userEntry, sessionContext); // Session updated
contextEntry.role = 'user' - analyzed and may trigger flow executioncontextEntry.role = 'assistant' - added to context for awarenessinterface 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
}
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.
async updateActivity(
contextEntry: ContextEntry,
engineSessionContext: EngineSessionContext
): Promise<EngineSessionContext>
The method returns an updated EngineSessionContext containing:
sessionContext.response contains the result// 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;
sessionContext.response to determine if a workflow handled the inputThe 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.
Multiple Independent Flow Execution Stacks
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
parentTransaction?: string; // Parent transaction ID for sub-flow tracking
justResumed?: boolean; // Flag indicating flow was just resumed
}
Technical Implementation Details:
flowName, flowId, flowVersion are copied from the FlowDefinitionprompt, description, and localized prompts are accessed dynamically from FlowDefinition
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 stackTool 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
}
Technical Implementation Details:
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:
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.
{{expression}} syntax for string templatesSingle 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 object
Template 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'}})" }
All JavaScript features are supported within a sandboxed environment with access limited to explicitly exported variables and functions.
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.
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: [ /* ... */ ]
}
];
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
}
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
);
en - English (default)es - Spanish (EspaΓ±ol)pt - Portuguese (PortuguΓͺs)fr - French (FranΓ§ais)de - German (Deutsch)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
}
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}}
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 logic
For demos, tests, or developer convenience, you can set aiCallback to null when constructing
the engine. In this mode, intent detection will:
name or id, that flow is activated.name or id contains the input (case-insensitive).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.
When creating test suites, follow these patterns to ensure session isolation and prevent state contamination:
// β 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);
}
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.
The engine supports completely generic response transformation through declarative JSON configuration:
{
responseMapping: {
type: "jsonPath",
mappings: {
"output_field": {
path: "api.response.field[0].value",
transform: { type: "parseInt", fallback: 0 },
fallback: "$args.inputField"
}
}
}
}
{
responseMapping: {
type: "object",
mappings: {
"user_name": "name",
"contact": {
"email": "email",
"phone": "phone"
},
"metadata": {
"processed": true,
"timestamp": "{{new Date().toISOString()}}"
}
}
}
}
{
responseMapping: {
type: "template",
template: "User {{name}} ({{email}}) from {{$args.source}}"
}
}
{
implementation: {
type: "http",
url: "https://api.example.com/users",
method: "GET"
}
}
{
implementation: {
type: "http",
url: "https://api.example.com/users",
method: "POST",
contentType: "application/json"
},
apiKey: "your-bearer-token"
}
{
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"
}
}
}
}
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}`);
Cause: Corrupted session state or improper context handling.
Solution: Ensure proper session lifecycle:
// Initialize once per user/conversation
let sessionContext = engine.initSession(userId, sessionId);
// Update for every interaction
sessionContext = await engine.updateActivity(userInput, sessionContext);
// Check workflow response
if (sessionContext.response) {
return sessionContext.response;
}
// Continue with regular conversation
sessionContext = await engine.updateActivity(assistantResponse, sessionContext);
Cause: Shared session contexts between different users or conversations.
Solution: Maintain strict session isolation:
// Create session per user
const userSessions = new Map();
function getOrCreateSession(userId, sessionId) {
const key = `${userId}-${sessionId}`;
if (!userSessions.has(key)) {
userSessions.set(key, engine.initSession(userId, sessionId));
}
return userSessions.get(key);
}
// Update and store session
let sessionContext = getOrCreateSession(userId, sessionId);
sessionContext = await engine.updateActivity(entry, sessionContext);
userSessions.set(`${userId}-${sessionId}`, sessionContext);
FAQs
TypeScript workflow engine for conversational/automation apps: multi-flow orchestration, interrupt/resume, stack-based control, robust errors, and flexible tool/REST integration.
We found that jsfe demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago.Β It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.