Skip to main content

Overview

CEL (Common Expression Language) enables dynamic decision-making in your Inworld Agent Runtime graphs. Use CEL expressions to control when edges fire, creating intelligent routing based on data content, user properties, confidence scores, and more.

Basic Syntax

CEL expressions are used in the conditionExpression parameter when adding edges to your graph:
.addEdge(sourceNode, targetNode, {
  conditionExpression: 'input.confidence > 0.8'
})

Understanding the Input Object

In CEL expressions, input refers to the data output from the source node that the edge is connected to. When an edge is evaluated, CEL receives the previous node’s output as the input object.

Example flow

// UserAuthNode outputs: { user: { id: "123", tier: "premium" }, isAuthenticated: true }
// The edge condition receives this as 'input'
.addEdge(userAuthNode, premiumFeatureNode, {
  conditionExpression: 'input.user.tier == "premium" && input.isAuthenticated == true'
})

Key Points

  • input = output data from the source node of the edge
  • Structure varies based on what the source node produces
  • Always verify the source node’s output format before writing CEL expressions

The Data Store Object

In addition to input, CEL expressions also have access to a data_store object that provides persistent storage across graph execution. The data store allows you to share state between different nodes and maintain context throughout the graph’s lifecycle.

Data Store Operations

Checking if a Variable Exists

data_store.contains('variable_name')

Getting a Variable Value

data_store.get('variable_name')
Note: Data store values are stored as objects with a text property, so you need to access .text to get the actual value:
data_store.get('my_bool_var').text == 'true'

Data Store vs Input

Aspectinputdata_store
ScopeCurrent edge onlyEntire graph execution
SourcePrevious node’s outputPersistent storage across nodes
LifecyclePer edge evaluationGraph lifetime
Use CaseNode-to-node data flowCross-node state management

Data Type Limitations

Important: The Inworld Agent Runtime only supports simple data types in node inputs and outputs. Complex JavaScript objects and class instances are not supported.

Supported Types:

  • Primitives: string, number, boolean
  • Arrays of primitives: string[], number[], boolean[]
  • Plain objects with primitive properties: { name: string, age: number }
  • Nested plain objects: { user: { id: string, tier: string } }

Unsupported Types:

  • Class instances (e.g., new Date(), custom classes)
  • Functions
  • Symbols
  • Complex objects with methods

1. Fundamental Operations

Boolean Comparisons

Equality Checks

Expression: input.is_safe == true
Use Case: Safety Gate - Route conversation through safety checker before proceeding to main chat flow. If content is flagged as unsafe, redirect to the safety response node.
.addEdge(safetyNode, mainChatNode, {
  conditionExpression: 'input.is_safe == true'
})
.addEdge(safetyNode, safetyResponseNode, {
  conditionExpression: 'input.is_safe == false'
})
Expression: input.status == "approved"
Use Case: Content Moderation - Only allow approved content to proceed to LLM generation. Rejected content gets routed to the moderation review queue.

Inequality Checks

Expression: input.user_tier != "banned"
Use Case: User Access Control - Block banned users from accessing premium features. Route them to account restriction notice instead.
Expression: input.language != "en"
Use Case: Multi-language Routing - Route non-English inputs to translation service first, then proceed to the main processing pipeline.

Numeric Comparisons

Greater Than / Less Than

Expression: input.confidence_score > 0.8
Use Case: High-Confidence Intent Routing - Only route to specialized handlers if confidence is high enough. Low confidence goes to the general fallback handler.
.addEdge(intentNode, specializedHandler, {
  conditionExpression: 'input.confidence_score > 0.8'
})
.addEdge(intentNode, fallbackHandler, {
  conditionExpression: 'input.confidence_score <= 0.8'
})
Expression: input.token_count < 100
Use Case: Token Limit Enforcement - Short queries go to fast processing pipeline. Long queries get chunked or use different LLM settings.

Greater/Less Than or Equal

Expression: input.user_credits >= 10
Use Case: Premium Feature Access - Users with sufficient credits access premium LLM models. Others get routed to standard models.
Expression: input.response_time_ms <= 5000
Use Case: Performance SLA Monitoring - Fast responses proceed normally. Slow responses trigger performance alerts and fallback routes.

String Operations

String Contains/StartsWith/EndsWith

Expression: input.message.startsWith("Hello")
Use Case: Greeting Detection - Route greeting messages to warm welcome flow. Other messages go to standard conversation flow.
Expression: input.email.endsWith("@company.com")
Use Case: Internal User Detection - Company employees get access to internal features. External users follow standard customer journey.
Expression: input.query.contains("urgent")
Use Case: Priority Request Handling - Urgent requests bypass normal queue and go to fast-track processing. Regular requests follow a standard processing timeline.

Boolean Logic

AND Operations

Expression: input.verified == true && input.age >= 18
Use Case: Age-Gated Content Access - Only verified adult users can access mature content. Others get redirected to age-appropriate alternatives.
Expression: input.is_premium && input.feature_enabled
Use Case: Feature Flag + Subscription Check - Premium users with beta features enabled get new functionality. Others continue with the standard feature set.

OR Operations

Expression: input.role == "admin" || input.role == "moderator"
Use Case: Administrative Access - Admins and moderators get access to management tools. Regular users follow standard user flow.
Expression: input.emergency == true || input.priority == "high"
Use Case: Emergency Response Routing - Emergency or high-priority requests bypass normal processing. Route directly to rapid response handlers.

NOT Operations

Expression: !input.maintenance_mode
Use Case: Maintenance Mode Check - During maintenance, route to “service unavailable” message. Normal operations proceed when not in maintenance.

2. Data Structure Navigation

Object Property Access

Simple Property Access

Expression: input.user.tier
Use Case: User Tier Routing - Route based on subscription level: free, premium, enterprise. Each tier gets different LLM models and response limits.
.addEdge(inputNode, premiumLLMNode, {
  conditionExpression: 'input.user.tier == "premium"'
})
.addEdge(inputNode, standardLLMNode, {
  conditionExpression: 'input.user.tier == "free"'
})
Expression: input.session.language
Use Case: Language Preference - Route to language-specific LLM models and response templates. Maintain conversation context in user’s preferred language.

Nested Property Access

Expression: input.user.profile.preferences.voice_enabled
Use Case: Voice Feature Toggle - Users who enabled voice in their profile settings get TTS output. Others receive text-only responses.
Expression: input.conversation.metadata.sentiment.score
Use Case: Sentiment-Based Response Adjustment - Negative sentiment routes to empathetic response templates. Positive sentiment continues with standard conversation flow.

Array Operations

Array Size

Expression: size(input.intent_matches) > 0
Use Case: Intent Detection Success - If intents were detected, route to intent-specific handlers. No intents detected routes to general conversation flow.
.addEdge(intentDetectionNode, intentHandlerNode, {
  conditionExpression: 'size(input.intent_matches) > 0'
})
.addEdge(intentDetectionNode, generalChatNode, {
  conditionExpression: 'size(input.intent_matches) == 0'
})
Expression: size(input.knowledge_results) >= 3
Use Case: Knowledge Base Confidence - Multiple knowledge matches indicate high-confidence answers. Route to direct answer generation vs. “I’m not sure” response.

Array Element Access

Expression: input.intents[0].name == "booking"
Use Case: Primary Intent Classification - Top-ranked intent determines conversation flow. Booking intents route to reservation system integration.
Expression: input.search_results[0].score > 0.9
Use Case: High-Confidence Search Results - Highly relevant results get featured answer treatment. Lower confidence results trigger clarification questions.

Array Contains/Membership

Expression: "admin" in input.user.roles
Use Case: Role-Based Access Control - Users with admin role get access to system management features. Regular users are limited to standard functionality.
Expression: input.detected_language in ["en", "es", "fr"]
Use Case: Supported Language Check - Supported languages proceed with native processing. Unsupported languages route to translation service first.

Complex Data Filtering

Filter with Conditions

Expression: size(input.intent_matches.filter(x, x.score >= 0.8)) > 0
Use Case: High-Confidence Intent Filtering - Only consider intents with high confidence scores. Route to specialized handlers vs. general conversation.
.addEdge(intentNode, highConfidenceHandler, {
  conditionExpression: 'size(input.intent_matches.filter(x, x.score >= 0.8)) > 0'
})
.addEdge(intentNode, lowConfidenceHandler, {
  conditionExpression: 'size(input.intent_matches.filter(x, x.score >= 0.8)) == 0'
})
Expression: size(input.knowledge_records.filter(r, r.verified == true)) >= 2
Use Case: Verified Knowledge Validation - Only use verified knowledge sources for answers. Insufficient verified sources trigger “needs verification” flow.

Complex Object Filtering

Expression: input.messages.filter(m, m.role == "user" && m.timestamp > input.last_hour)
Use Case: Recent User Message Analysis - Analyze only recent user messages for context. Ignore older conversation history for focused responses.
Expression: input.messages.filter(m, m.role == "user" && m.timestamp.seconds > input.current_time.seconds)
Use Case: Recent User Message Analysis - Analyze only messages sent after session start. Requires timestamp objects with seconds/nanos fields.
Expression: input.features.filter(f, f.enabled == true && f.beta == false)
Use Case: Stable Feature Selection - Only route to production-ready features. Beta features require special opt-in handling.

Existence Checks

Property Existence

Expression: has(input.user.subscription)
Use Case: Subscription Status Check - Subscribed users get premium features. Non-subscribers see upgrade prompts and limited functionality.
Expression: has(input.conversation.context)
Use Case: Conversation Context Availability - Continue existing conversation if context exists. Start a fresh conversation flow if there is no prior context.

Complex Existence Patterns

Expression: has(input.user.preferences) && has(input.user.preferences.notifications)
Use Case: Notification Preference Check - Users with notification preferences get personalized alerts. Others receive default notification settings.
Expression: has(input.session.auth_token) && input.session.auth_token != ""
Use Case: Authentication Validation - Authenticated users access full functionality. Unauthenticated users limited to public features only.

Production Examples from Character Engine

Safety Pipeline Routing

// Route based on safety check results
.addEdge(safetySubgraphNode, safetyResultTransformNode, {
  conditionExpression: 'input.is_safe == true'
})
.addEdge(safetySubgraphNode, safetyResponseNode, {
  conditionExpression: 'input.is_safe == false'
})

Intent Confidence Thresholding

// High confidence intent matches
.addEdge(strictMatchNode, topNFilterNode, {
  conditionExpression: 'size(input.intent_matches) >= 1',
  optional: true
})
// Low confidence fallback to LLM
.addEdge(intentEmbeddingMatchesAggregatorNode, llmPromptVarsNode, {
  conditionExpression: 'size(input.intent_matches.filter(x, x.score >= 0.88)) < 1'})

Behavior Action Discrimination

// Different behavior types
.addEdge(behaviorProducerNode, dialogPromptVariablesNode, {
  conditionExpression: 'input.type == "SAY_INSTRUCTED"'
})
.addEdge(behaviorProducerNode, behaviorActionTransformNode, {
  conditionExpression: 'input.type == "SAY_VERBATIM"'
})

Message Validation

// Ensure messages exist before processing
.addEdge(flashMemoryPromptBuilder, flashMemoryLLMChat, {
  conditionExpression: 'size(input.messages) > 0'
})

Best Practices

  1. Keep expressions simple and readable, Complex logic should be broken into multiple edges when possible
  2. Use meaningful variable names, input.user.tier is clearer than input.t
  3. Handle edge cases, Always provide fallback routes for when conditions aren’t met
  4. Test thoroughly, Verify your expressions work with different input data types
  5. Performance considerations, Avoid expensive operations in frequently evaluated expressions
  6. Understand your data flow - Always know what structure the source node outputs before writing CEL expressions
  7. Document expected input formats - Comment your edge conditions with expected input structure for maintainability
  8. Use only simple data types - Ensure all node outputs use primitives and plain objects, not class instances
  9. Convert complex types - Transform timestamps, custom classes, and complex objects to plain object representations
  10. Validate data serialization - Test that your data can be JSON.stringify’d without loss of information
  11. Always check data_store.contains() first - Prevent runtime errors by verifying variables exist before accessing them
  12. Remember the .text property - Data store values are objects; always access .text to get the actual stored value
  13. Use data_store for cross-node communication - When you need to share state between non-adjacent nodes, use data_store instead of passing through intermediate nodes

Common Patterns

Confidence-Based Routing

// High confidence → specialized handler
// Medium confidence → general handler  
// Low confidence → fallback handler
conditionExpression: 'input.confidence >= 0.9'  // High
conditionExpression: 'input.confidence >= 0.7 && input.confidence < 0.9'  // Medium
conditionExpression: 'input.confidence < 0.7'  // Low

Multi-Condition Validation

// Ensure all requirements are met
conditionExpression: 'has(input.user) && input.user.verified && input.user.credits > 0'

Array Processing

// Check if any items meet criteria
conditionExpression: 'size(input.items.filter(x, x.active)) > 0'
// Check if all items meet criteria  
conditionExpression: 'size(input.items.filter(x, x.active)) == size(input.items)'

Data Store State Management

// Safe data store access pattern
conditionExpression: "data_store.contains('flag_name') && data_store.get('flag_name').text == 'expected_value'"
// Multiple data store conditions
conditionExpression: "data_store.contains('var1') && data_store.contains('var2') && data_store.get('var1').text == 'true' && data_store.get('var2').text != 'disabled'"

Debugging Tips

  1. Use simple expressions first, Start with basic comparisons and add complexity gradually
  2. Check data types, Ensure your input data structure matches your CEL expression expectations
  3. Test with sample data, Create test cases with known input values to verify expression behavior
  4. Use parentheses, Make complex boolean logic explicit with parentheses: (A && B) || (C && D)
  5. Inspect actual input data - Log or debug the actual input object structure before writing complex expressions
  6. Validate source node outputs - Ensure source nodes produce the expected data structure that your CEL expressions assume
  7. Check for unsupported types - If CEL expressions fail unexpectedly, verify that input data contains only supported primitive types
  8. Use JSON.stringify for validation - Test your node outputs with JSON.stringify() - if it fails or loses data, the types aren’t supported
  9. Debug data store state - Log data_store contents to understand what variables are available and their current values
  10. Test data store persistence - Verify that data store variables persist correctly across different graph execution paths