Skip to content

@rcs-lang/csm

A lightweight, TypeScript-first state machine library designed specifically for RCS conversational agents. CSM provides a simple, performant way to manage conversation flow state across stateless HTTP requests.

  • 🪶 Lightweight: ~3KB minified, designed for serverless
  • 🔄 State Persistence: Serialize/deserialize state between requests
  • 🔗 URL-Safe: Compact state representation for URL parameters
  • 🎯 Simple API: Single callback for all state transitions
  • 🧩 Composable: Connect multiple flows into complex agents
  • 🌊 Flow Invocations: Flows can invoke other flows with result handling
  • 🔧 JSON Logic Conditions: Safe, serializable conditions with JS fallback
  • 📊 Scoped Context: Three-level context system (conversation/flow/params)
  • 🎚️ Multi-Flow Machines: Single machine definition with multiple flows
  • 📦 Minimal Dependencies: Only neverthrow and json-logic-js
  • 🏃 Fast: Optimized for request/response cycle performance
Terminal window
npm install @rcs-lang/csm
import { ConversationalAgent, type MachineDefinitionJSON } from '@rcs-lang/csm';
// Define your machine (usually generated from RCL)
const coffeeShopMachine: MachineDefinitionJSON = {
id: 'CoffeeShopBot',
initialFlow: 'OrderFlow',
flows: {
OrderFlow: {
id: 'OrderFlow',
initial: 'Welcome',
states: {
Welcome: {
transitions: [
{ pattern: 'Order Coffee', target: 'ChooseSize' },
{ pattern: 'View Menu', target: 'ShowMenu' }
]
},
ChooseSize: {
transitions: [
{ pattern: 'Small', target: 'ChooseDrink', context: { size: 'small', price: 3.50 } },
{ pattern: 'Medium', target: 'ChooseDrink', context: { size: 'medium', price: 4.50 } }
]
}
}
}
}
};
// Create agent with state change handler
const agent = new ConversationalAgent({
id: 'CoffeeBot',
onStateChange: async (event) => {
console.log(`Entering state: ${event.state}`);
// Send message, log analytics, etc.
await sendMessage(event.context.userId, messages[event.state]);
}
});
// Add machine (supports both single-flow and multi-flow machines)
agent.addMachine(coffeeShopMachine);
// Process user input
const response = await agent.processInput('Order Coffee');
// response.state = 'ChooseSize'
// response.machine = 'OrderFlow'
// Serialize for next request
const stateHash = agent.toURLHash();
// "Q29mZmVlQm90Ok9yZGVyRmxvdzpDaG9vc2VTaXplOnt9"
// Restore in next request
const restoredAgent = ConversationalAgent.fromURLHash(stateHash, {
id: 'CoffeeBot',
onStateChange: async (event) => {
await sendMessage(event.context.userId, messages[event.state]);
}
});

The core of the CSM package is the MachineDefinitionJSON interface, which defines the structure for conversational state machines.

A machine definition consists of:

interface MachineDefinitionJSON {
id: string; // Unique identifier for the machine
initial: string; // ID of the starting state
states: Record<string, StateDefinitionJSON>; // Map of state definitions
meta?: MachineMetadata; // Optional metadata
}

Each state in the machine is defined by:

interface StateDefinitionJSON {
transitions: TransitionJSON[]; // Array of possible transitions
meta?: StateMetadata; // Optional state metadata
}

Transitions define how to move between states:

interface TransitionJSON {
pattern?: string; // Pattern to match user input (optional for auto-transitions)
target?: string; // Target reference using type:ID format (state:Name, flow:Name, @variable, :end/:cancel/:error)
context?: Record<string, any>; // Context updates to apply
condition?: string | ConditionObject; // Condition for this transition (see Conditions section)
priority?: number; // Priority for pattern matching (higher = first)
flowInvocation?: { // Flow invocation with result handling
flowId: string; // ID of the flow to invoke
parameters?: Record<string, any>; // Parameters to pass to the flow
onResult: { // Handlers for different flow outcomes
end?: {
operations?: Array<ContextOperation>; // Operations before transitioning
target: string; // Target after successful completion
};
cancel?: {
operations?: Array<ContextOperation>; // Operations before transitioning
target: string; // Target if user cancels
};
error?: {
operations?: Array<ContextOperation>; // Operations before transitioning
target: string; // Target if error occurs
};
};
};
}
// Condition types
type ConditionObject =
| { type: "code"; expression: string } // JavaScript code
| { type: "jsonlogic"; rule: JSONLogicRule }; // JSON Logic rule

Both machines and states can have optional metadata:

// Machine metadata
interface MachineMetadata {
name?: string; // Display name
description?: string; // Description
version?: string; // Version
tags?: string[]; // Categorization tags
custom?: Record<string, any>; // Custom properties
}
// State metadata
interface StateMetadata {
messageId?: string; // Message to send when entering state
transient?: boolean; // Auto-transition without user input
tags?: string[]; // Categorization tags
custom?: Record<string, any>; // Custom properties
}

Here’s a basic machine definition for a greeting flow:

{
"id": "GreetingFlow",
"initial": "welcome",
"states": {
"welcome": {
"transitions": [
{
"pattern": "hello|hi|hey",
"target": "greeting_response"
},
{
"pattern": ":default",
"target": "help"
}
],
"meta": {
"messageId": "welcome_message"
}
},
"greeting_response": {
"transitions": [
{
"target": "end"
}
],
"meta": {
"messageId": "greeting_reply",
"transient": true
}
},
"help": {
"transitions": [
{
"target": "welcome"
}
],
"meta": {
"messageId": "help_message",
"transient": true
}
},
"end": {
"transitions": [],
"meta": {
"messageId": "goodbye"
}
}
},
"meta": {
"name": "Greeting Flow",
"description": "Handles basic greetings and help",
"version": "1.0.0",
"tags": ["greeting", "basic"]
}
}
{
"id": "UserProfileFlow",
"initial": "collect_name",
"states": {
"collect_name": {
"transitions": [
{
"pattern": ".*",
"target": "collect_email",
"context": {
"name": "$input"
}
}
],
"meta": {
"messageId": "ask_name"
}
},
"collect_email": {
"transitions": [
{
"pattern": "\\S+@\\S+\\.\\S+",
"target": "confirmation",
"context": {
"email": "$input"
}
},
{
"pattern": ".*",
"target": "invalid_email"
}
],
"meta": {
"messageId": "ask_email"
}
},
"invalid_email": {
"transitions": [
{
"target": "collect_email"
}
],
"meta": {
"messageId": "invalid_email_message",
"transient": true
}
},
"confirmation": {
"transitions": [
{
"pattern": "yes|confirm|ok",
"target": "complete"
},
{
"pattern": "no|cancel",
"target": "collect_name"
}
],
"meta": {
"messageId": "confirm_details"
}
},
"complete": {
"transitions": [],
"meta": {
"messageId": "profile_saved"
}
}
}
}

CSM supports several target reference formats:

  • State references: state:StateName - Navigate to a state in the current flow
  • Flow references: flow:FlowName - Navigate to another flow
  • Message references: message:MessageName - Jump to a specific message
  • Context variables: @variableName - Dynamic targets from context
  • Flow termination: :ok, :cancel, :error - End flow with result

Use flowInvocation for complex flow control with explicit result handling:

{
"id": "TopFlow",
"initial": "Welcome",
"states": {
"Welcome": {
"transitions": [
{
"pattern": "Start Order",
"flowInvocation": {
"flowId": "CreateOrder",
"onResult": {
"ok": {
"operations": [
{
"append": {
"to": "orders",
"value": {"var": "result"}
}
}
],
"target": "state:ConfirmAllOrders"
},
"cancel": {
"target": "state:Welcome"
},
"error": {
"target": "state:OrderError"
}
}
}
}
]
}
}
}

Operations allow you to manipulate context data when handling flow results:

  • Set: {"set": {"variable": "name", "value": {...}}}
  • Append: {"append": {"to": "arrayName", "value": {...}}}
  • Merge: {"merge": {"into": "objectName", "value": {...}}}

Values support JSONLogic expressions, including {"var": "result"} to access the flow’s return value.

CSM supports dynamic context resolution using the @variable syntax:

{
"transitions": [
{
"pattern": "go",
"target": "@nextState",
"context": {
"message": "Going to #{@nextState}"
}
}
]
}

Context variables are resolved at runtime, and string interpolation supports #{variable} syntax for dynamic message content.

Use the validateMachineDefinition function to validate machine definitions at runtime:

import { validateMachineDefinition, type MachineDefinitionJSON } from '@rcs-lang/csm';
const definition: MachineDefinitionJSON = {
// ... your machine definition
};
try {
if (validateMachineDefinition(definition)) {
console.log('Machine definition is valid');
}
} catch (error) {
console.error('Validation failed:', error.message);
}

Note: Transitions must have either a target OR a flowInvocation - not both. This allows for both simple state transitions and complex flow invocations with result handling.

CSM supports multi-flow machines, where a single machine definition contains multiple flows that can invoke each other:

interface MultiFlowMachineDefinitionJSON {
id: string;
initialFlow: string; // ID of the starting flow
flows: Record<string, SingleFlowMachineDefinitionJSON>; // Map of flow definitions
meta?: MachineMetadata;
}
{
"id": "CoffeeShopBot",
"initialFlow": "TopFlow",
"flows": {
"TopFlow": {
"id": "TopFlow",
"initial": "Welcome",
"states": {
"Welcome": {
"transitions": [
{
"pattern": "Start Order",
"flowInvocation": {
"flowId": "CreateOrder",
"parameters": {"source": "welcome"},
"onResult": {
"end": {
"operations": [
{"append": {"to": "orders", "value": {"var": "result"}}}
],
"target": "ConfirmAllOrders"
},
"cancel": {"target": "Welcome"},
"error": {"target": "OrderError"}
}
}
}
]
}
}
},
"CreateOrder": {
"id": "CreateOrder",
"initial": "ChooseSize",
"states": {
"ChooseSize": {
"transitions": [
{"pattern": "small", "target": "ChooseDrink", "context": {"size": "small"}},
{"pattern": "cancel", "target": ":cancel"}
]
},
"ChooseDrink": {
"transitions": [
{"pattern": "coffee", "target": ":end", "context": {"drink": "coffee"}}
]
}
}
}
}
}

CSM supports three types of conditions for controlling transitions:

// Simple JavaScript expressions (generates deprecation warning)
condition: "context.verified === true"
condition: "context.user && context.user.age >= 18"
// Recommended for complex JavaScript logic
condition: {
type: "code",
expression: "context.user && context.user.points > 100 && context.user.verified"
}
// Safe, serializable conditions using JSON Logic
condition: {
type: "jsonlogic",
rule: {
"and": [
{"==": [{"var": "user.verified"}, true]},
{">": [{"var": "user.points"}, 100]}
]
}
}
{
"states": {
"CheckAccess": {
"transitions": [
{
"pattern": "premium feature",
"target": "PremiumContent",
"condition": {
"type": "jsonlogic",
"rule": {
"and": [
{"==": [{"var": "membership"}, "premium"]},
{">": [{"var": "points"}, 100]}
]
}
}
},
{
"pattern": "basic feature",
"target": "BasicContent",
"condition": {
"type": "code",
"expression": "context.membership === 'basic' || context.membership === 'premium'"
}
}
]
}
}
}

CSM implements a three-level context system for proper variable isolation:

  • Conversation Context: Persists across the entire agent session
  • Flow Context: Isolated per individual flow execution
  • Parameters Context: Temporary variables for current state/transition
interface ScopedContext {
conversation: Record<string, any>; // Persists for entire session
flow: Record<string, any>; // Isolated per flow
params: Record<string, any>; // Current state parameters
}

This allows flows to maintain their own state while sharing conversation-level data.

Flows can terminate with three types of results:

  • :end - Successful completion with return value
  • :cancel - User-initiated cancellation
  • :error - Error condition occurred

Each result type can have its own operations and target state in the parent flow.

The CSM package supports several pattern types:

  • Literal strings: Match exact text
  • Regular expressions: Full regex support
  • Special patterns:
    • :default - Fallback pattern (lowest priority)
    • .* - Match any input
    • $input - Capture user input in context

The package provides full TypeScript support:

import {
type MachineDefinitionJSON,
type AgentDefinitionJSON,
validateMachineDefinition
} from '@rcs-lang/csm';
// Type-safe machine definition
const machine: MachineDefinitionJSON = {
id: 'MyFlow',
initial: 'start',
states: {
start: {
transitions: [
{ pattern: 'begin', target: 'processing' }
]
},
processing: {
transitions: [
{ target: 'end' }
],
meta: { transient: true }
},
end: {
transitions: []
}
}
};
// Validation with type checking
if (validateMachineDefinition(machine)) {
// Machine is valid and type-safe
}

Here’s a complete machine definition for a coffee shop ordering system with multiple flows:

{
"id": "CoffeeShopAgent",
"initial": "main_menu",
"states": {
"main_menu": {
"transitions": [
{
"pattern": "order|coffee|buy",
"target": "machine:OrderFlow"
},
{
"pattern": "menu|options|what",
"target": "show_menu"
},
{
"pattern": "help|support",
"target": "machine:HelpFlow"
},
{
"pattern": ":default",
"target": "welcome"
}
],
"meta": {
"messageId": "main_menu"
}
},
"welcome": {
"transitions": [
{
"target": "main_menu"
}
],
"meta": {
"messageId": "welcome_message",
"transient": true
}
},
"show_menu": {
"transitions": [
{
"pattern": "order",
"target": "machine:OrderFlow"
},
{
"target": "main_menu"
}
],
"meta": {
"messageId": "menu_display",
"transient": true
}
}
},
"meta": {
"name": "Coffee Shop Main Agent",
"description": "Main entry point for coffee shop interactions",
"version": "2.0.0"
}
}

And the OrderFlow machine:

{
"id": "OrderFlow",
"initial": "choose_size",
"states": {
"choose_size": {
"transitions": [
{
"pattern": "small|s",
"target": "choose_drink",
"context": {
"size": "small",
"price": 3.50
}
},
{
"pattern": "medium|m",
"target": "choose_drink",
"context": {
"size": "medium",
"price": 4.00
}
},
{
"pattern": "large|l",
"target": "choose_drink",
"context": {
"size": "large",
"price": 4.50
}
},
{
"pattern": ".*",
"target": "invalid_size"
}
],
"meta": {
"messageId": "choose_size"
}
},
"invalid_size": {
"transitions": [
{
"target": "choose_size"
}
],
"meta": {
"messageId": "invalid_size_message",
"transient": true
}
},
"choose_drink": {
"transitions": [
{
"pattern": "coffee|americano",
"target": "customize",
"context": {
"drink": "coffee"
}
},
{
"pattern": "latte",
"target": "customize",
"context": {
"drink": "latte"
}
},
{
"pattern": "cappuccino",
"target": "customize",
"context": {
"drink": "cappuccino"
}
},
{
"pattern": ".*",
"target": "invalid_drink"
}
],
"meta": {
"messageId": "choose_drink"
}
},
"invalid_drink": {
"transitions": [
{
"target": "choose_drink"
}
],
"meta": {
"messageId": "invalid_drink_message",
"transient": true
}
},
"customize": {
"transitions": [
{
"pattern": "regular|whole",
"target": "confirm_order",
"context": {
"milk": "regular milk",
"extraCharge": 0
}
},
{
"pattern": "almond|soy|oat",
"target": "confirm_order",
"context": {
"milk": "$input milk",
"extraCharge": 0.60
}
},
{
"pattern": "skip|no|none",
"target": "confirm_order",
"context": {
"milk": "none",
"extraCharge": 0
}
},
{
"pattern": ".*",
"target": "invalid_milk"
}
],
"meta": {
"messageId": "customize_message"
}
},
"invalid_milk": {
"transitions": [
{
"target": "customize"
}
],
"meta": {
"messageId": "invalid_milk_message",
"transient": true
}
},
"confirm_order": {
"transitions": [
{
"pattern": "yes|confirm|ok",
"target": "place_order"
},
{
"pattern": "no|cancel|change",
"target": "choose_size",
"context": {
"size": null,
"drink": null,
"milk": null,
"price": 0,
"extraCharge": 0
}
}
],
"meta": {
"messageId": "confirm_order"
}
},
"place_order": {
"transitions": [
{
"target": "machine:CoffeeShopAgent",
"context": {
"orderComplete": true,
"orderId": "$generateOrderId"
}
}
],
"meta": {
"messageId": "order_placed",
"transient": true
}
}
},
"meta": {
"name": "Coffee Order Flow",
"description": "Handles the complete coffee ordering process",
"version": "1.2.0",
"tags": ["ordering", "coffee", "ecommerce"]
}
}

For complex agents with multiple flows, use AgentDefinitionJSON:

{
"id": "CoffeeShopBot",
"initial": "CoffeeShopAgent",
"machines": {
"CoffeeShopAgent": {
"id": "CoffeeShopAgent",
"initial": "main_menu",
"states": {
// ... main agent states
}
},
"OrderFlow": {
"id": "OrderFlow",
"initial": "choose_size",
"states": {
// ... order flow states
}
},
"HelpFlow": {
"id": "HelpFlow",
"initial": "help_menu",
"states": {
"help_menu": {
"transitions": [
{
"pattern": "hours|time",
"target": "show_hours"
},
{
"pattern": "location|address",
"target": "show_location"
},
{
"pattern": "back|menu",
"target": "machine:CoffeeShopAgent"
}
],
"meta": {
"messageId": "help_options"
}
},
"show_hours": {
"transitions": [
{
"target": "help_menu"
}
],
"meta": {
"messageId": "store_hours",
"transient": true
}
},
"show_location": {
"transitions": [
{
"target": "help_menu"
}
],
"meta": {
"messageId": "store_location",
"transient": true
}
}
}
}
},
"meta": {
"name": "Coffee Shop Bot",
"description": "Complete coffee shop ordering and support system",
"version": "2.0.0"
}
}

Perfect for AWS Lambda, Vercel, Netlify Functions, or similar:

export async function handleMessage(request: Request) {
const { stateHash, userInput } = await request.json();
// Restore agent state
const agent = stateHash
? ConversationalAgent.fromURLHash(stateHash, {
id: 'CoffeeBot',
onStateChange: async (event) => {
// Log state change
await logAnalytics(event);
// Get message for state
const message = getMessageForState(event.state);
// Store response to send back
response.message = message;
}
})
: createNewAgent();
// Process input
const result = await agent.processInput(userInput);
// Return response with new state
return Response.json({
message: response.message,
stateHash: agent.toURLHash(),
suggestions: getSuggestionsForState(result.state)
});
}
app.post('/conversation', async (req, res) => {
const agent = createOrRestoreAgent(req.body.stateHash);
const result = await agent.processInput(req.body.input);
res.json({
state: result,
hash: agent.toURLHash()
});
});
// Import reusable flows
import { ContactSupportFlow } from '@rcs-lang/common-flows';
// Define custom flow
const customFlow: FlowDefinition = {
id: 'MainMenu',
initial: 'Welcome',
states: {
Welcome: {
transitions: [
{ pattern: 'Support', target: 'machine:ContactSupport' }
]
}
}
};
// Compose agent
const agent = new ConversationalAgent({ id: 'MyBot', onStateChange });
agent.addFlow(customFlow);
agent.addFlow(ContactSupportFlow);

The ConversationalAgent class provides the main interface:

class ConversationalAgent {
constructor(options: AgentOptions);
// Machine management (supports both single-flow and multi-flow machines)
addMachine(machine: MachineDefinitionJSON): void;
addFlow(flow: FlowDefinition): void; // Legacy support for single flows
removeFlow(flowId: string): void;
// State processing
processInput(input: string): Promise<ProcessResult>;
// Serialization
toURLHash(): string;
static fromURLHash(hash: string, options: AgentOptions): ConversationalAgent;
// State access
getCurrentState(): { machine: string; state: string };
getContext(): Context;
updateContext(updates: Partial<Context>): void;
setState(machineId: string, stateId: string): void; // For restoration
}
  • Minimal Overhead: ~1ms to process typical state transition
  • Compact State: Average URL hash ~100-200 characters
  • Memory Efficient: No persistence between requests
  • Fast Serialization: Optimized JSON encoding
  • Pattern Caching: Compiled patterns cached per flow

For detailed API documentation, see the CSM package README and source code.