Conditional Approval with CEL¶
Learn how to use Common Expression Language (CEL) to create smart, conditional approval workflows.
Availability
Conditional approvals using CEL are an Enterprise feature.
What is CEL?¶
CEL (Common Expression Language) is a simple, fast, and safe expression language designed for evaluating conditions.
Why CEL for Approval Workflows?¶
Instead of approving everything, use CEL to approve only when conditions match:
Without CEL:
Every deploy requires approval, even to dev/staging.With CEL:
tool: deploy
condition: args.environment == "production"
approval: required # ONLY require approval for production
Access Rule CEL Examples¶
Common CEL conditions for access rules:
| Expression | Use case |
|---|---|
args.environment == "production" |
Match production deployments |
args.amount > 1000 && args.currency == "USD" |
Match high-value USD transactions |
args.command.contains("rm") \|\| args.command.contains("delete") |
Match destructive commands |
OSS vs Enterprise
In the open-source edition, only a single simple condition per rule is supported. The Enterprise Edition adds multiple conditions with AND/OR operators and a CEL editor for manual expression editing.
Basic Syntax¶
Comparison Operators¶
| Operator | Meaning | Example |
|---|---|---|
== |
Equals | args.amount == 1000 |
!= |
Not equals | args.environment != "dev" |
> |
Greater than | args.amount > 1000 |
>= |
Greater or equal | args.amount >= 1000 |
< |
Less than | args.amount < 1000 |
<= |
Less or equal | args.amount <= 1000 |
Logical Operators¶
| Operator | Meaning | Example |
|---|---|---|
&& |
AND | args.amount > 1000 && args.environment == "production" |
\|\| |
OR | args.priority == "critical" \|\| args.priority == "high" |
! |
NOT | !args.dry_run |
Membership Operators¶
| Operator | Meaning | Example |
|---|---|---|
in |
Member of list | args.environment in ["production", "staging"] |
has() |
Has property | has(args.rollback_on_failure) |
Common Patterns¶
Pattern 1: Environment-Based Approval¶
Approve only production deploys:
Approve production OR staging:
Approve anything except dev:
Pattern 2: Amount-Based Approval¶
Approve payments over $1000:
Approve payments between $1000 and $10000:
Approve large OR international payments:
Pattern 3: Priority-Based Approval¶
Approve critical or high priority:
Approve if priority is set and critical:
Pattern 4: Time-Based Approval¶
Approve outside business hours:
Approve on weekends:
Approve during maintenance window:
Pattern 5: String Matching¶
Approve if contains substring:
Approve if starts with prefix:
Approve if matches pattern:
Pattern 6: List Operations¶
Approve if list is non-empty:
Approve if specific item in list:
Approve if list exceeds size:
Real-World Examples¶
Example 1: Safe Production Deployments¶
Requirement: Require approval for production deploys, except during maintenance windows
tool: deploy
condition: |
args.environment == "production" &&
!(timestamp(now()).getHours() >= 2 && timestamp(now()).getHours() <= 4)
approval: required
approvers: [sre_team]
Explanation:
args.environment == "production"- Must be production!(...)- NOT during...timestamp(now()).getHours() >= 2 && timestamp(now()).getHours() <= 4- Between 2-4 AM- Result: Approve production deploys EXCEPT 2-4 AM (maintenance window)
Example 2: Tiered Payment Approval¶
Requirement: Different approval levels based on amount
# Tier 1: $1000-$10000 → Finance team
tool: pay
condition: args.amount >= 1000 && args.amount < 10000
approval: required
approvers: [finance_team]
quorum: 1
---
# Tier 2: $10000+ → Finance + CEO
tool: pay
condition: args.amount >= 10000
approval: required
approvers: [finance_team, ceo@acme.com]
quorum: 2
Example 3: Database Operations¶
Requirement: Approve destructive operations on production databases
tool: drop_table
condition: args.database.startsWith("prod_")
approval: required
approvers: [database_team]
quorum: 2
Example 4: Critical Bug Fixes¶
Requirement: Fast-track critical bugs, approve normal priority changes
tool: update_issue
condition: |
has(args.priority) &&
args.priority in ["critical", "high"] &&
args.status == "in_progress"
approval: required
approvers: [team_lead@acme.com]
timeout: 300 # 5 minutes for fast response
Example 5: Bulk Operations¶
Requirement: Approve operations affecting many items
tool: send_email
condition: size(args.recipients) > 50
approval: required
approvers: [marketing_team]
Example 6: Off-Hours Deployments¶
Requirement: Extra approval for deployments outside business hours
tool: deploy
condition: |
args.environment == "production" &&
(timestamp(now()).getHours() < 9 ||
timestamp(now()).getHours() > 17 ||
timestamp(now()).getDayOfWeek() in [0, 6])
approval: required
approvers: [sre_team, cto@acme.com]
escalation:
enabled: true
escalate_to: [ceo@acme.com]
escalation_delay: 600
Available Variables¶
args - Tool Arguments¶
Access any argument passed to the tool:
args.amount # Numeric argument
args.environment # String argument
args.dry_run # Boolean argument
args.recipients # List argument
args.metadata.key # Nested object
user - Current User¶
Information about who is calling the tool:
user.email # User's email
user.role # User's role (owner, admin, editor, viewer)
user.teams # List of teams user belongs to
user.id # User ID
Example:
tool - Tool Information¶
Metadata about the tool being called:
Example:
timestamp() - Time Functions¶
Current time and date functions:
timestamp(now()).getHours() # 0-23
timestamp(now()).getMinutes() # 0-59
timestamp(now()).getDayOfWeek() # 0=Sunday, 6=Saturday
timestamp(now()).getDayOfMonth() # 1-31
timestamp(now()).getMonth() # 0-11 (0=January)
timestamp(now()).getFullYear() # 2025
Example:
# Approve only on weekdays during business hours
timestamp(now()).getDayOfWeek() >= 1 &&
timestamp(now()).getDayOfWeek() <= 5 &&
timestamp(now()).getHours() >= 9 &&
timestamp(now()).getHours() <= 17
Advanced Techniques¶
Multi-Condition Policies¶
Use multiple policies for the same tool with different conditions:
# Policy 1: Small amounts, anyone can approve
tool: pay
condition: args.amount < 1000
approval: required
approvers: [finance_team]
quorum: 1
---
# Policy 2: Large amounts, need 2 approvals
tool: pay
condition: args.amount >= 1000 && args.amount < 10000
approval: required
approvers: [finance_team]
quorum: 2
---
# Policy 3: Very large amounts, need CFO
tool: pay
condition: args.amount >= 10000
approval: required
approvers: [cfo@acme.com]
Evaluation order: First matching policy wins.
Nested Conditions¶
Access nested object properties:
tool: deploy
condition: |
args.config.replicas > 10 &&
args.config.resources.cpu > "2000m"
approval: required
List Comprehensions¶
Check if all/any items match condition:
# Approve if ANY recipient is external
args.recipients.exists(r, !r.endsWith("@acme.com"))
# Approve if ALL amounts are over $100
args.items.all(i, i.amount > 100)
Default Values¶
Handle missing arguments:
# Use default value if argument missing
args.dry_run || false
# Check if argument exists first
has(args.environment) && args.environment == "production"
Testing CEL Expressions¶
Expression Tester (Web UI)¶
- Navigate to Approvals → Policies
- Click Test Expression
- Enter your CEL expression
- Provide sample arguments
- Click Evaluate
Example test:
Expression: args.amount > 1000 && args.environment == "production"
Sample Arguments:
{
"amount": 5000,
"environment": "production"
}
Result: ✅ true (approval required)
📸 Screenshot needed: cel-expression-tester.png
Command Line Testing¶
Use the Preloop CLI:
Common Test Cases¶
Always test these scenarios:
- Boundary values - Test exactly at thresholds (amount == 1000)
- Missing arguments - Test with required args missing
- Edge cases - Test with empty lists, null values
- Type mismatches - Test with wrong types (string instead of number)
Common Mistakes¶
Mistake 1: Using assignment instead of comparison¶
❌ Wrong:
✅ Correct:
Mistake 2: Not checking if argument exists¶
❌ Wrong:
✅ Correct:
Mistake 3: String comparison with wrong case¶
❌ Wrong:
✅ Correct:
Mistake 4: Confusing OR and AND¶
❌ Wrong:
✅ Correct:
args.environment in ["production", "staging"]
# OR
args.environment == "production" || args.environment == "staging"
Mistake 5: Forgetting parentheses with complex logic¶
❌ Wrong:
args.amount > 1000 && args.environment == "production" || args.priority == "critical"
# Evaluates as: (amount > 1000 && env == prod) || priority == critical
✅ Correct:
CEL Function Reference¶
String Functions¶
| Function | Description | Example |
|---|---|---|
contains(str) |
Check if contains substring | args.message.contains("urgent") |
startsWith(str) |
Check if starts with | args.branch.startsWith("hotfix/") |
endsWith(str) |
Check if ends with | args.email.endsWith("@acme.com") |
matches(regex) |
Match regex pattern | args.email.matches("^[a-z]+@") |
lowerAscii() |
Convert to lowercase | args.env.lowerAscii() == "production" |
upperAscii() |
Convert to uppercase | args.priority.upperAscii() == "HIGH" |
size() |
Length of string | size(args.message) > 100 |
List Functions¶
| Function | Description | Example |
|---|---|---|
size(list) |
Length of list | size(args.recipients) > 10 |
in |
Check membership | "admin" in args.roles |
has(list) |
Check if list exists | has(args.tags) |
exists(var, cond) |
Check if any match | args.items.exists(i, i.amount > 1000) |
all(var, cond) |
Check if all match | args.items.all(i, i.verified == true) |
Numeric Functions¶
| Function | Description | Example |
|---|---|---|
int() |
Convert to integer | int(args.amount) > 1000 |
double() |
Convert to double | double(args.percentage) > 0.5 |
abs() |
Absolute value | abs(args.delta) > 100 |
Timestamp Functions¶
| Function | Description | Example |
|---|---|---|
timestamp(now()) |
Current timestamp | timestamp(now()).getHours() |
getHours() |
Hour (0-23) | timestamp(now()).getHours() >= 9 |
getMinutes() |
Minutes (0-59) | timestamp(now()).getMinutes() < 30 |
getDayOfWeek() |
Day (0=Sun, 6=Sat) | timestamp(now()).getDayOfWeek() >= 1 |
getMonth() |
Month (0-11) | timestamp(now()).getMonth() == 11 |
getFullYear() |
Year | timestamp(now()).getFullYear() == 2025 |