Conditional Execution
Problem: You want certain commands to execute only when specific conditions are met at runtime.
Solution: Use the when attribute with a Tera expression. If it evaluates to a falsy value, the command is skipped.
How the when Attribute Works
Every command in Panopticon supports an optional when attribute:
- Before executing the command, the runtime evaluates the
whenexpression - If the result is falsy (
false,null, empty string,0), the command is skipped - Skipped commands have
status = "skipped"in their metadata - Data results are absent for skipped commands
Basic Pattern: Feature Flags
The most common use case is feature-flagging parts of your pipeline.
use panopticon_core::prelude::*; async fn run_with_feature_flag(enabled: bool) -> anyhow::Result<()> { let mut pipeline = Pipeline::with_services(PipelineServices::defaults()); // Static namespace with configuration pipeline .add_namespace( NamespaceBuilder::new("inputs") .static_ns() .insert("feature_enabled", ScalarValue::Bool(enabled)) .insert("user_name", ScalarValue::String("Alice".to_string())), ) .await?; // Command with `when` guard let attrs = ObjectBuilder::new() .insert("when", "inputs.feature_enabled") // <-- The condition .insert( "branches", ScalarValue::Array(vec![ ObjectBuilder::new() .insert("name", "greeting") .insert("if", "true") .insert("then", "Hello, {{ inputs.user_name }}! Feature is active.") .build_scalar(), ]), ) .insert("default", "Fallback message") .build_hashmap(); pipeline .add_namespace(NamespaceBuilder::new("example")) .await? .add_command::<ConditionCommand>("greeting", &attrs) .await?; // Execute let completed = pipeline.compile().await?.execute().await?; let results = completed.results(ResultSettings::default()).await?; // Check the status let source = StorePath::from_segments(["example", "greeting"]); let cmd_results = results .get_by_source(&source) .expect("Expected results"); let status = cmd_results .meta_get(&source.with_segment("status")) .expect("Expected status"); println!("status = {}", status); // Data is only present when the command executed if let Some(result) = cmd_results .data_get(&source.with_segment("result")) .and_then(|r| r.as_scalar()) { println!("result = {}", result.1); } else { println!("(no data - command was skipped)"); } Ok(()) } #[tokio::main] async fn main() -> anyhow::Result<()> { println!("=== Feature flag: TRUE ==="); run_with_feature_flag(true).await?; println!("\n=== Feature flag: FALSE ==="); run_with_feature_flag(false).await?; Ok(()) }
Output:
=== Feature flag: TRUE ===
status = "completed"
result = Hello, Alice! Feature is active.
=== Feature flag: FALSE ===
status = "skipped"
(no data - command was skipped)
Tera Expression Syntax
The when value is a Tera expression that has access to the entire scalar store. Common patterns:
Boolean Checks
#![allow(unused)] fn main() { // Direct boolean value .insert("when", "config.debug_mode") // Negation .insert("when", "not config.production") // Comparison .insert("when", "config.log_level == \"debug\"") }
Numeric Comparisons
#![allow(unused)] fn main() { // Greater than .insert("when", "stats.row_count > 0") // Range check .insert("when", "inputs.threshold >= 10 and inputs.threshold <= 100") }
String Tests
#![allow(unused)] fn main() { // Equality .insert("when", "config.environment == \"production\"") // Prefix check .insert("when", "region is starting_with(\"us-\")") // Contains .insert("when", "config.tags is containing(\"important\")") }
Existence Checks
#![allow(unused)] fn main() { // Check if a value is defined .insert("when", "optional_config is defined") // Check for null .insert("when", "maybe_value is not none") }
Combined Conditions
#![allow(unused)] fn main() { // AND .insert("when", "config.enabled and stats.count > 0") // OR .insert("when", "config.mode == \"full\" or config.force") // Complex .insert("when", "(env == \"prod\" or env == \"staging\") and feature_flags.new_flow") }
Handling Skipped Commands
When a command is skipped, you need to handle its absence in downstream commands.
Check Status in Results
#![allow(unused)] fn main() { let status = cmd_results .meta_get(&source.with_segment("status")) .expect("Expected status"); match status.as_str() { Some("completed") => { // Process data results } Some("skipped") => { // Handle skipped case } _ => { // Unexpected status } } }
Use Tera Defaults in Templates
When referencing potentially-skipped command outputs in templates:
#![allow(unused)] fn main() { // Use default filter to handle missing values .insert("message", "Count: {{ stats.count | default(value=0) }}") }
Chain Conditions
If command B depends on command A's output, and A might be skipped:
#![allow(unused)] fn main() { // Command A: might be skipped let a_attrs = ObjectBuilder::new() .insert("when", "config.run_expensive_query") // ... .build_hashmap(); handle.add_command::<SqlCommand>("query_a", &a_attrs).await?; // Command B: only runs if A ran successfully let b_attrs = ObjectBuilder::new() .insert("when", "ns.query_a.status == \"completed\"") // ... .build_hashmap(); handle.add_command::<AggregateCommand>("aggregate_b", &b_attrs).await?; }
Use Cases
Debug-Only Commands
Skip verbose logging in production:
#![allow(unused)] fn main() { .insert("when", "config.debug") }
Empty Data Guards
Skip processing when there is no data:
#![allow(unused)] fn main() { .insert("when", "data.load.rows > 0") }
Environment-Specific Logic
Run different commands per environment:
#![allow(unused)] fn main() { // Production-only .insert("when", "config.env == \"production\"") // Development-only .insert("when", "config.env == \"development\"") }
Conditional Exports
Only export when there are results worth saving:
#![allow(unused)] fn main() { .insert("when", "stats.significant_findings > 0") }
Iteration Guards
Within an iterative namespace, skip certain items:
#![allow(unused)] fn main() { // Skip disabled regions .insert("when", "region is not starting_with(\"disabled-\")") // Only process items meeting criteria .insert("when", "item.status == \"active\"") }
Best Practices
Keep Conditions Simple
Complex conditions are hard to debug. Prefer:
#![allow(unused)] fn main() { // Good: simple, readable .insert("when", "config.feature_enabled") // Avoid: complex nested logic .insert("when", "((a and b) or (c and not d)) and (e or f)") }
If you need complex logic, compute a boolean in an earlier command and reference it.
Document Skip Behavior
When a command might be skipped, document what happens:
#![allow(unused)] fn main() { // This command is skipped when feature_x is disabled. // Downstream commands that reference its output use default values. let attrs = ObjectBuilder::new() .insert("when", "config.feature_x") // ... }
Test Both Paths
Always test your pipeline with conditions evaluating to both true and false to ensure proper handling.
Use Status Checks for Dependencies
Instead of duplicating conditions, check the upstream command's status:
#![allow(unused)] fn main() { // Instead of repeating the condition: // .insert("when", "config.feature_x") // Check if the dependency actually ran: .insert("when", "upstream.cmd.status == \"completed\"") }
This ensures consistency even if the original condition logic changes.